diff options
330 files changed, 8588 insertions, 2883 deletions
diff --git a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java index 452bb0ab5909..06207215b7be 100644 --- a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java +++ b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java @@ -19,6 +19,7 @@ package android.wm; import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; +import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemClock; import android.perftests.utils.ManualBenchmarkState; @@ -86,6 +87,7 @@ public class WindowAddRemovePerfTest extends WindowManagerPerfTestBase final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); final InsetsState mOutInsetsState = new InsetsState(); final InsetsSourceControl[] mOutControls = new InsetsSourceControl[0]; + final Rect mOutAttachedFrame = new Rect(); TestWindow() { mLayoutParams.setTitle(TestWindow.class.getName()); @@ -104,7 +106,7 @@ public class WindowAddRemovePerfTest extends WindowManagerPerfTestBase long startTime = SystemClock.elapsedRealtimeNanos(); session.addToDisplay(this, mLayoutParams, View.VISIBLE, Display.DEFAULT_DISPLAY, mRequestedVisibilities, inputChannel, - mOutInsetsState, mOutControls); + mOutInsetsState, mOutControls, mOutAttachedFrame); final long elapsedTimeNsOfAdd = SystemClock.elapsedRealtimeNanos() - startTime; state.addExtraResult("add", elapsedTimeNsOfAdd); diff --git a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java index 2fcab59cdec7..78214dc27a9f 100644 --- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java +++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java @@ -385,6 +385,12 @@ public class PowerExemptionManager { */ public static final int REASON_ACTIVE_DEVICE_ADMIN = 324; + /** + * Media notification re-generate during transferring. + * @hide + */ + public static final int REASON_MEDIA_NOTIFICATION_TRANSFER = 325; + /** @hide The app requests out-out. */ public static final int REASON_OPT_OUT_REQUESTED = 1000; @@ -465,6 +471,7 @@ public class PowerExemptionManager { REASON_DPO_PROTECTED_APP, REASON_DISALLOW_APPS_CONTROL, REASON_ACTIVE_DEVICE_ADMIN, + REASON_MEDIA_NOTIFICATION_TRANSFER, }) @Retention(RetentionPolicy.SOURCE) public @interface ReasonCode {} @@ -830,6 +837,8 @@ public class PowerExemptionManager { return "ACTIVE_DEVICE_ADMIN"; case REASON_OPT_OUT_REQUESTED: return "REASON_OPT_OUT_REQUESTED"; + case REASON_MEDIA_NOTIFICATION_TRANSFER: + return "REASON_MEDIA_NOTIFICATION_TRANSFER"; default: return "(unknown:" + reasonCode + ")"; } diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java index c43c832992cf..9b64edf53d8c 100644 --- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java @@ -164,6 +164,13 @@ public interface AppStandbyInternal { @ElapsedRealtimeLong long elapsedRealtime); /** + * Puts the list of apps in the {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RARE} + * bucket. + * @param restoredApps the list of restored apps + */ + void restoreAppsToRare(@NonNull Set<String> restoredApps, int userId); + + /** * Put the specified app in the * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} * bucket. If it has been used by the user recently, the restriction will delayed until an diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index 9e3e3553c125..5d9f3357125a 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -23,6 +23,7 @@ import static android.app.usage.UsageStatsManager.REASON_MAIN_MASK; import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED; import static android.app.usage.UsageStatsManager.REASON_MAIN_TIMEOUT; import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE; +import static android.app.usage.UsageStatsManager.REASON_SUB_DEFAULT_APP_RESTORED; import static android.app.usage.UsageStatsManager.REASON_SUB_DEFAULT_APP_UPDATE; import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY; import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_USER_FLAG_INTERACTION; @@ -1605,6 +1606,26 @@ public class AppStandbyController } @Override + public void restoreAppsToRare(Set<String> restoredApps, int userId) { + final int reason = REASON_MAIN_DEFAULT | REASON_SUB_DEFAULT_APP_RESTORED; + final long nowElapsed = mInjector.elapsedRealtime(); + for (String packageName : restoredApps) { + // If the package is not installed, don't allow the bucket to be set. + if (!mInjector.isPackageInstalled(packageName, 0, userId)) { + Slog.e(TAG, "Tried to restore bucket for uninstalled app: " + packageName); + continue; + } + + final int standbyBucket = getAppStandbyBucket(packageName, userId, nowElapsed, false); + // Only update the standby bucket to RARE if the app is still in the NEVER bucket. + if (standbyBucket == STANDBY_BUCKET_NEVER) { + setAppStandbyBucket(packageName, userId, STANDBY_BUCKET_RARE, reason, + nowElapsed, false); + } + } + } + + @Override public void setAppStandbyBucket(@NonNull String packageName, int bucket, int userId, int callingUid, int callingPid) { setAppStandbyBuckets( diff --git a/boot/Android.bp b/boot/Android.bp index 5b265a5dc9de..3f14ebc4fd92 100644 --- a/boot/Android.bp +++ b/boot/Android.bp @@ -60,8 +60,8 @@ platform_bootclasspath { module: "art-bootclasspath-fragment", }, { - apex: "com.android.bluetooth", - module: "com.android.bluetooth-bootclasspath-fragment", + apex: "com.android.btservices", + module: "com.android.btservices-bootclasspath-fragment", }, { apex: "com.android.conscrypt", diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 802458b1793c..546efaad85ac 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -274,11 +274,6 @@ public final class ActivityThread extends ClientTransactionHandler public static final boolean DEBUG_ORDER = false; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; /** - * If the activity doesn't become idle in time, the timeout will ensure to apply the pending top - * process state. - */ - private static final long PENDING_TOP_PROCESS_STATE_TIMEOUT = 1000; - /** * The delay to release the provider when it has no more references. It reduces the number of * transactions for acquiring and releasing provider if the client accesses the provider * frequently in a short time. @@ -367,8 +362,6 @@ public final class ActivityThread extends ClientTransactionHandler private final AtomicInteger mNumLaunchingActivities = new AtomicInteger(); @GuardedBy("mAppThread") private int mLastProcessState = PROCESS_STATE_UNKNOWN; - @GuardedBy("mAppThread") - private int mPendingProcessState = PROCESS_STATE_UNKNOWN; ArrayList<WeakReference<AssistStructure>> mLastAssistStructures = new ArrayList<>(); private int mLastSessionId; final ArrayMap<IBinder, CreateServiceData> mServicesData = new ArrayMap<>(); @@ -2384,7 +2377,6 @@ public final class ActivityThread extends ClientTransactionHandler if (stopProfiling) { mProfiler.stopProfiling(); } - applyPendingProcessState(); return false; } } @@ -3438,16 +3430,7 @@ public final class ActivityThread extends ClientTransactionHandler return; } mLastProcessState = processState; - // Defer the top state for VM to avoid aggressive JIT compilation affecting activity - // launch time. - if (processState == ActivityManager.PROCESS_STATE_TOP - && mNumLaunchingActivities.get() > 0) { - mPendingProcessState = processState; - mH.postDelayed(this::applyPendingProcessState, PENDING_TOP_PROCESS_STATE_TIMEOUT); - } else { - mPendingProcessState = PROCESS_STATE_UNKNOWN; - updateVmProcessState(processState); - } + updateVmProcessState(processState); if (localLOGV) { Slog.i(TAG, "******************* PROCESS STATE CHANGED TO: " + processState + (fromIpc ? " (from ipc" : "")); @@ -3465,20 +3448,6 @@ public final class ActivityThread extends ClientTransactionHandler VMRuntime.getRuntime().updateProcessState(state); } - private void applyPendingProcessState() { - synchronized (mAppThread) { - if (mPendingProcessState == PROCESS_STATE_UNKNOWN) { - return; - } - final int pendingState = mPendingProcessState; - mPendingProcessState = PROCESS_STATE_UNKNOWN; - // Only apply the pending state if the last state doesn't change. - if (pendingState == mLastProcessState) { - updateVmProcessState(pendingState); - } - } - } - @Override public void countLaunchingActivities(int num) { mNumLaunchingActivities.getAndAdd(num); diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 33cf71256d51..de0f7522f36d 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -1482,8 +1482,6 @@ public class Dialog implements DialogInterface, Window.Callback, /** * Returns the {@link OnBackInvokedDispatcher} instance associated with the window that this * dialog is attached to. - * - * Returns null if the dialog is not attached to a window with a decor. */ @NonNull public OnBackInvokedDispatcher getOnBackInvokedDispatcher() { diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 0e1a5461840e..34c91c360dbe 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -770,6 +770,10 @@ public class DevicePolicyManager { * <p>If this extra is set to {@code true}, the provisioning flow will still try to connect to * the internet, but if it fails it will start the offline provisioning flow. * + * <p>For T if this extra is set to {@code true}, the provisioning flow will be forced through + * the platform and there will be no attempt to download and install the device policy + * management role holder. + * * <p>The default value is {@code false}. * * <p>This extra is respected when provided via the provisioning intent actions such as {@link diff --git a/core/java/android/app/usage/UsageStats.java b/core/java/android/app/usage/UsageStats.java index d61abc64fae5..e213c934e95a 100644 --- a/core/java/android/app/usage/UsageStats.java +++ b/core/java/android/app/usage/UsageStats.java @@ -293,6 +293,17 @@ public final class UsageStats implements Parcelable { } /** + * Returns the last time the package was used - defined by the latest of + * mLastTimeUsed, mLastTimeVisible, mLastTimeForegroundServiceUsed, or mLastTimeComponentUsed. + * @hide + */ + public long getLastTimePackageUsed() { + return Math.max(mLastTimeUsed, + Math.max(mLastTimeVisible, + Math.max(mLastTimeForegroundServiceUsed, mLastTimeComponentUsed))); + } + + /** * Returns the number of times the app was launched as an activity from outside of the app. * Excludes intra-app activity transitions. * @hide diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index c013fcd5adb9..1dfc7d48640e 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -220,6 +220,11 @@ public final class UsageStatsManager { */ public static final int REASON_SUB_DEFAULT_APP_UPDATE = 0x0001; /** + * The app was restored. + * @hide + */ + public static final int REASON_SUB_DEFAULT_APP_RESTORED = 0x0002; + /** * The app was interacted with in some way by the system. * @hide */ @@ -1209,6 +1214,9 @@ public final class UsageStatsManager { case REASON_SUB_DEFAULT_APP_UPDATE: sb.append("-au"); break; + case REASON_SUB_DEFAULT_APP_RESTORED: + sb.append("-ar"); + break; } break; case REASON_MAIN_FORCED_BY_SYSTEM: diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index f7f0235cd508..93748f81ffa1 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -55,6 +55,14 @@ public final class AssociationInfo implements Parcelable { private final boolean mSelfManaged; private final boolean mNotifyOnDeviceNearby; + + /** + * Indicates that the association has been revoked (removed), but we keep the association + * record for final clean up (e.g. removing the app from the list of the role holders). + * + * @see CompanionDeviceManager#disassociate(int) + */ + private final boolean mRevoked; private final long mTimeApprovedMs; /** * A long value indicates the last time connected reported by selfManaged devices @@ -71,7 +79,7 @@ public final class AssociationInfo implements Parcelable { public AssociationInfo(int id, @UserIdInt int userId, @NonNull String packageName, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String deviceProfile, boolean selfManaged, boolean notifyOnDeviceNearby, - long timeApprovedMs, long lastTimeConnectedMs) { + boolean revoked, long timeApprovedMs, long lastTimeConnectedMs) { if (id <= 0) { throw new IllegalArgumentException("Association ID should be greater than 0"); } @@ -91,6 +99,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = selfManaged; mNotifyOnDeviceNearby = notifyOnDeviceNearby; + mRevoked = revoked; mTimeApprovedMs = timeApprovedMs; mLastTimeConnectedMs = lastTimeConnectedMs; } @@ -176,6 +185,14 @@ public final class AssociationInfo implements Parcelable { } /** + * @return if the association has been revoked (removed). + * @hide + */ + public boolean isRevoked() { + return mRevoked; + } + + /** * @return the last time self reported disconnected for selfManaged only. * @hide */ @@ -244,6 +261,7 @@ public final class AssociationInfo implements Parcelable { + ", mDeviceProfile='" + mDeviceProfile + '\'' + ", mSelfManaged=" + mSelfManaged + ", mNotifyOnDeviceNearby=" + mNotifyOnDeviceNearby + + ", mRevoked=" + mRevoked + ", mTimeApprovedMs=" + new Date(mTimeApprovedMs) + ", mLastTimeConnectedMs=" + ( mLastTimeConnectedMs == Long.MAX_VALUE @@ -260,6 +278,7 @@ public final class AssociationInfo implements Parcelable { && mUserId == that.mUserId && mSelfManaged == that.mSelfManaged && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby + && mRevoked == that.mRevoked && mTimeApprovedMs == that.mTimeApprovedMs && mLastTimeConnectedMs == that.mLastTimeConnectedMs && Objects.equals(mPackageName, that.mPackageName) @@ -271,7 +290,7 @@ public final class AssociationInfo implements Parcelable { @Override public int hashCode() { return Objects.hash(mId, mUserId, mPackageName, mDeviceMacAddress, mDisplayName, - mDeviceProfile, mSelfManaged, mNotifyOnDeviceNearby, mTimeApprovedMs, + mDeviceProfile, mSelfManaged, mNotifyOnDeviceNearby, mRevoked, mTimeApprovedMs, mLastTimeConnectedMs); } @@ -293,6 +312,7 @@ public final class AssociationInfo implements Parcelable { dest.writeBoolean(mSelfManaged); dest.writeBoolean(mNotifyOnDeviceNearby); + dest.writeBoolean(mRevoked); dest.writeLong(mTimeApprovedMs); dest.writeLong(mLastTimeConnectedMs); } @@ -309,6 +329,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = in.readBoolean(); mNotifyOnDeviceNearby = in.readBoolean(); + mRevoked = in.readBoolean(); mTimeApprovedMs = in.readLong(); mLastTimeConnectedMs = in.readLong(); } @@ -352,11 +373,13 @@ public final class AssociationInfo implements Parcelable { @NonNull private final AssociationInfo mOriginalInfo; private boolean mNotifyOnDeviceNearby; + private boolean mRevoked; private long mLastTimeConnectedMs; private Builder(@NonNull AssociationInfo info) { mOriginalInfo = info; mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby; + mRevoked = info.mRevoked; mLastTimeConnectedMs = info.mLastTimeConnectedMs; } @@ -388,6 +411,17 @@ public final class AssociationInfo implements Parcelable { } /** + * Should only be used by the CompanionDeviceManagerService. + * @hide + */ + @Override + @NonNull + public Builder setRevoked(boolean revoked) { + mRevoked = revoked; + return this; + } + + /** * @hide */ @NonNull @@ -401,6 +435,7 @@ public final class AssociationInfo implements Parcelable { mOriginalInfo.mDeviceProfile, mOriginalInfo.mSelfManaged, mNotifyOnDeviceNearby, + mRevoked, mOriginalInfo.mTimeApprovedMs, mLastTimeConnectedMs ); @@ -433,5 +468,12 @@ public final class AssociationInfo implements Parcelable { */ @NonNull Builder setLastTimeConnected(long lastTimeConnectedMs); + + /** + * Should only be used by the CompanionDeviceManagerService. + * @hide + */ + @NonNull + Builder setRevoked(boolean revoked); } } diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 20a4fdf658c6..10d6f2d6d04b 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -542,14 +542,17 @@ public class ApkLiteParseUtils { int minVer = DEFAULT_MIN_SDK_VERSION; String minCode = null; + boolean minAssigned = false; int targetVer = DEFAULT_TARGET_SDK_VERSION; String targetCode = null; if (!TextUtils.isEmpty(minSdkVersionString)) { try { minVer = Integer.parseInt(minSdkVersionString); + minAssigned = true; } catch (NumberFormatException ignored) { minCode = minSdkVersionString; + minAssigned = !TextUtils.isEmpty(minCode); } } @@ -558,7 +561,7 @@ public class ApkLiteParseUtils { targetVer = Integer.parseInt(targetSdkVersionString); } catch (NumberFormatException ignored) { targetCode = targetSdkVersionString; - if (minCode == null) { + if (!minAssigned) { minCode = targetCode; } } diff --git a/core/java/android/content/res/CompatibilityInfo.java b/core/java/android/content/res/CompatibilityInfo.java index 439c6396f1d0..608e34bd07b4 100644 --- a/core/java/android/content/res/CompatibilityInfo.java +++ b/core/java/android/content/res/CompatibilityInfo.java @@ -420,7 +420,10 @@ public class CompatibilityInfo implements Parcelable { * Translate a Rect in screen coordinates into the app window's coordinates. */ @UnsupportedAppUsage - public void translateRectInScreenToAppWindow(Rect rect) { + public void translateRectInScreenToAppWindow(@Nullable Rect rect) { + if (rect == null) { + return; + } rect.scale(applicationInvertedScale); } diff --git a/core/java/android/debug/AdbManagerInternal.java b/core/java/android/debug/AdbManagerInternal.java index d730129507d7..e448706fabfe 100644 --- a/core/java/android/debug/AdbManagerInternal.java +++ b/core/java/android/debug/AdbManagerInternal.java @@ -55,6 +55,12 @@ public abstract class AdbManagerInternal { public abstract File getAdbTempKeysFile(); /** + * Notify the AdbManager that the key files have changed and any in-memory state should be + * reloaded. + */ + public abstract void notifyKeyFilesUpdated(); + + /** * Starts adbd for a transport. */ public abstract void startAdbdForTransport(byte transportType); diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index b505395a091a..8bc11cbc61de 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -1448,5 +1448,15 @@ public final class DisplayManager { * @hide */ String KEY_HIGH_REFRESH_RATE_BLACKLIST = "high_refresh_rate_blacklist"; + + /** + * Key for the brightness throttling data as a String formatted: + * <displayId>,<no of throttling levels>,[<severity as string>,<brightness cap>] + * Where the latter part is repeated for each throttling level, and the entirety is repeated + * for each display, separated by a semicolon. + * For example: + * 123,1,critical,0.8;456,2,moderate,0.9,critical,0.7 + */ + String KEY_BRIGHTNESS_THROTTLING_DATA = "brightness_throttling_data"; } } diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java index 3a042a5dee4d..e8e4fc988937 100644 --- a/core/java/android/hardware/radio/ProgramList.java +++ b/core/java/android/hardware/radio/ProgramList.java @@ -26,7 +26,6 @@ import android.os.Parcelable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -173,38 +172,63 @@ public final class ProgramList implements AutoCloseable { } } - void apply(@NonNull Chunk chunk) { + void apply(Chunk chunk) { + List<ProgramSelector.Identifier> removedList = new ArrayList<>(); + List<ProgramSelector.Identifier> changedList = new ArrayList<>(); + List<ProgramList.ListCallback> listCallbacksCopied; + List<OnCompleteListener> onCompleteListenersCopied = new ArrayList<>(); synchronized (mLock) { if (mIsClosed) return; mIsComplete = false; + listCallbacksCopied = new ArrayList<>(mListCallbacks); if (chunk.isPurge()) { - new HashSet<>(mPrograms.keySet()).stream().forEach(id -> removeLocked(id)); + for (ProgramSelector.Identifier id : mPrograms.keySet()) { + removeLocked(id, removedList); + } } - chunk.getRemoved().stream().forEach(id -> removeLocked(id)); - chunk.getModified().stream().forEach(info -> putLocked(info)); + chunk.getRemoved().stream().forEach(id -> removeLocked(id, removedList)); + chunk.getModified().stream().forEach(info -> putLocked(info, changedList)); if (chunk.isComplete()) { mIsComplete = true; - mOnCompleteListeners.forEach(cb -> cb.onComplete()); + onCompleteListenersCopied = new ArrayList<>(mOnCompleteListeners); + } + } + + for (int i = 0; i < removedList.size(); i++) { + for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) { + listCallbacksCopied.get(cbIndex).onItemRemoved(removedList.get(i)); + } + } + for (int i = 0; i < changedList.size(); i++) { + for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) { + listCallbacksCopied.get(cbIndex).onItemChanged(changedList.get(i)); + } + } + if (chunk.isComplete()) { + for (int cbIndex = 0; cbIndex < onCompleteListenersCopied.size(); cbIndex++) { + onCompleteListenersCopied.get(cbIndex).onComplete(); } } } - private void putLocked(@NonNull RadioManager.ProgramInfo value) { + private void putLocked(RadioManager.ProgramInfo value, + List<ProgramSelector.Identifier> changedIdentifierList) { ProgramSelector.Identifier key = value.getSelector().getPrimaryId(); mPrograms.put(Objects.requireNonNull(key), value); ProgramSelector.Identifier sel = value.getSelector().getPrimaryId(); - mListCallbacks.forEach(cb -> cb.onItemChanged(sel)); + changedIdentifierList.add(sel); } - private void removeLocked(@NonNull ProgramSelector.Identifier key) { + private void removeLocked(ProgramSelector.Identifier key, + List<ProgramSelector.Identifier> removedIdentifierList) { RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key)); if (removed == null) return; ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId(); - mListCallbacks.forEach(cb -> cb.onItemRemoved(sel)); + removedIdentifierList.add(sel); } /** diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 334a659d7f23..8e67705c5cf0 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -135,6 +135,7 @@ import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; +import android.window.CompatOnBackInvokedCallback; import android.window.ImeOnBackInvokedDispatcher; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; @@ -350,7 +351,7 @@ public class InputMethodService extends AbstractInputMethodService { private RingBuffer<MotionEvent> mPendingEvents; private ImeOnBackInvokedDispatcher mImeDispatcher; private Boolean mBackCallbackRegistered = false; - private final OnBackInvokedCallback mCompatBackCallback = this::compatHandleBack; + private final CompatOnBackInvokedCallback mCompatBackCallback = this::compatHandleBack; /** * Returns whether {@link InputMethodService} is responsible for rendering the back button and diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING index 22ddbccbaaae..c70f1f07e739 100644 --- a/core/java/android/os/TEST_MAPPING +++ b/core/java/android/os/TEST_MAPPING @@ -59,6 +59,18 @@ }, { "file_patterns": [ + "Parcel\\.java", + "[^/]*Bundle[^/]*\\.java" + ], + "name": "FrameworksMockingCoreTests", + "options": [ + { "include-filter": "android.os.BundleRecyclingTest"}, + { "exclude-annotation": "androidx.test.filters.FlakyTest" }, + { "exclude-annotation": "org.junit.Ignore" } + ] + }, + { + "file_patterns": [ "BatteryUsageStats[^/]*\\.java", "PowerComponents\\.java", "[^/]*BatteryConsumer[^/]*\\.java" diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 9679a6ab3acb..1df7dbc0cd0b 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -24,7 +24,6 @@ import static android.graphics.Matrix.MSKEW_X; import static android.graphics.Matrix.MSKEW_Y; import static android.view.SurfaceControl.METADATA_WINDOW_TYPE; import static android.view.View.SYSTEM_UI_FLAG_VISIBLE; -import static android.view.ViewRootImpl.LOCAL_LAYOUT; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import android.animation.AnimationHandler; @@ -41,7 +40,6 @@ import android.app.Service; import android.app.WallpaperColors; import android.app.WallpaperInfo; import android.app.WallpaperManager; -import android.app.WindowConfiguration; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; @@ -260,8 +258,6 @@ public abstract class WallpaperService extends Service { private final Point mLastSurfaceSize = new Point(); private final Matrix mTmpMatrix = new Matrix(); private final float[] mTmpValues = new float[9]; - private final WindowLayout mWindowLayout = new WindowLayout(); - private final Rect mTempRect = new Rect(); final WindowManager.LayoutParams mLayout = new WindowManager.LayoutParams(); @@ -1100,8 +1096,7 @@ public abstract class WallpaperService extends Service { | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; final Configuration config = mMergedConfiguration.getMergedConfiguration(); - final WindowConfiguration winConfig = config.windowConfiguration; - final Rect maxBounds = winConfig.getMaxBounds(); + final Rect maxBounds = config.windowConfiguration.getMaxBounds(); if (myWidth == ViewGroup.LayoutParams.MATCH_PARENT && myHeight == ViewGroup.LayoutParams.MATCH_PARENT) { mLayout.width = myWidth; @@ -1139,7 +1134,7 @@ public abstract class WallpaperService extends Service { if (mSession.addToDisplay(mWindow, mLayout, View.VISIBLE, mDisplay.getDisplayId(), mRequestedVisibilities, inputChannel, - mInsetsState, mTempControls) < 0) { + mInsetsState, mTempControls, new Rect()) < 0) { Log.w(TAG, "Failed to add window while updating wallpaper surface."); return; } @@ -1158,29 +1153,9 @@ public abstract class WallpaperService extends Service { } else { mLayout.surfaceInsets.set(0, 0, 0, 0); } - - int relayoutResult = 0; - if (LOCAL_LAYOUT) { - if (!mSurfaceControl.isValid()) { - relayoutResult = mSession.updateVisibility(mWindow, mLayout, - View.VISIBLE, mMergedConfiguration, mSurfaceControl, - mInsetsState, mTempControls); - } - - final Rect displayCutoutSafe = mTempRect; - mInsetsState.getDisplayCutoutSafe(displayCutoutSafe); - mWindowLayout.computeFrames(mLayout, mInsetsState, displayCutoutSafe, - winConfig.getBounds(), winConfig.getWindowingMode(), mWidth, - mHeight, mRequestedVisibilities, null /* attachedWindowFrame */, - 1f /* compatScale */, mWinFrames); - - mSession.updateLayout(mWindow, mLayout, 0 /* flags */, mWinFrames, mWidth, - mHeight); - } else { - relayoutResult = mSession.relayout(mWindow, mLayout, mWidth, mHeight, - View.VISIBLE, 0, mWinFrames, mMergedConfiguration, - mSurfaceControl, mInsetsState, mTempControls, mSyncSeqIdBundle); - } + final int relayoutResult = mSession.relayout(mWindow, mLayout, mWidth, mHeight, + View.VISIBLE, 0, mWinFrames, mMergedConfiguration, mSurfaceControl, + mInsetsState, mTempControls, mSyncSeqIdBundle); final int transformHint = SurfaceControl.rotationToBufferTransform( (mDisplayInstallOrientation + mDisplay.getRotation()) % 4); @@ -1229,7 +1204,7 @@ public abstract class WallpaperService extends Service { null /* ignoringVisibilityState */, config.isScreenRound(), false /* alwaysConsumeSystemBars */, mLayout.softInputMode, mLayout.flags, SYSTEM_UI_FLAG_VISIBLE, mLayout.type, - winConfig.getWindowingMode(), null /* typeSideMap */); + config.windowConfiguration.getWindowingMode(), null /* typeSideMap */); if (!fixedSize) { final Rect padding = mIWallpaperEngine.mDisplayPadding; diff --git a/core/java/android/view/IRemoteAnimationRunner.aidl b/core/java/android/view/IRemoteAnimationRunner.aidl index 1f64fb8ca2ec..1981c9d66c8b 100644 --- a/core/java/android/view/IRemoteAnimationRunner.aidl +++ b/core/java/android/view/IRemoteAnimationRunner.aidl @@ -46,5 +46,5 @@ oneway interface IRemoteAnimationRunner { * won't have any effect anymore. */ @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) - void onAnimationCancelled(); + void onAnimationCancelled(boolean isKeyguardOccluded); } diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 649accd1126d..ef57b1ddd4b3 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -50,13 +50,15 @@ interface IWindowSession { int addToDisplay(IWindow window, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, in InsetsVisibilities requestedVisibilities, out InputChannel outInputChannel, out InsetsState insetsState, - out InsetsSourceControl[] activeControls); + out InsetsSourceControl[] activeControls, out Rect attachedFrame); int addToDisplayAsUser(IWindow window, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, in int userId, in InsetsVisibilities requestedVisibilities, out InputChannel outInputChannel, - out InsetsState insetsState, out InsetsSourceControl[] activeControls); + out InsetsState insetsState, out InsetsSourceControl[] activeControls, + out Rect attachedFrame); int addToDisplayWithoutInputChannel(IWindow window, in WindowManager.LayoutParams attrs, - in int viewVisibility, in int layerStackId, out InsetsState insetsState); + in int viewVisibility, in int layerStackId, out InsetsState insetsState, + out Rect attachedFrame); @UnsupportedAppUsage void remove(IWindow window); @@ -107,41 +109,6 @@ interface IWindowSession { out InsetsState insetsState, out InsetsSourceControl[] activeControls, out Bundle bundle); - /** - * Changes the view visibility and the attributes of a window. This should only be called when - * the visibility of the root view is changed. This returns a valid surface if the root view is - * visible. This also returns the latest information for the caller to compute its window frame. - * - * @param window The window being updated. - * @param attrs If non-null, new attributes to apply to the window. - * @param viewVisibility Window root view's visibility. - * @param outMergedConfiguration New config container that holds global, override and merged - * config for window, if it is now becoming visible and the merged configuration has changed - * since it was last displayed. - * @param outSurfaceControl Object in which is placed the new display surface. - * @param outInsetsState The current insets state in the system. - * @param outActiveControls The insets source controls for the caller to override the insets - * state in the system. - * - * @return int Result flags: {@link WindowManagerGlobal#RELAYOUT_FIRST_TIME}. - */ - int updateVisibility(IWindow window, in WindowManager.LayoutParams attrs, int viewVisibility, - out MergedConfiguration outMergedConfiguration, out SurfaceControl outSurfaceControl, - out InsetsState outInsetsState, out InsetsSourceControl[] outActiveControls); - - /** - * Reports the layout results and the attributes of a window to the server. - * - * @param window The window being reported. - * @param attrs If non-null, new attributes to apply to the window. - * @param flags Request flags: {@link WindowManagerGlobal#RELAYOUT_INSETS_PENDING}. - * @param clientFrames the window frames computed by the client. - * @param requestedWidth The width the window wants to be. - * @param requestedHeight The height the window wants to be. - */ - oneway void updateLayout(IWindow window, in WindowManager.LayoutParams attrs, int flags, - in ClientWindowFrames clientFrames, int requestedWidth, int requestedHeight); - /* * Notify the window manager that an application is relaunching and * windows should be prepared for replacement. @@ -384,4 +351,9 @@ interface IWindowSession { * Clears a touchable region set by {@link #setInsets}. */ void clearTouchableRegion(IWindow window); + + /** + * Returns whether this window needs to cancel draw and retry later. + */ + boolean cancelDraw(IWindow window); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 294d7d6e2bc6..9a3957c1bcae 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -60,13 +60,11 @@ import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowLayout.UNSPECIFIED_LENGTH; import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; -import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN; import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW; -import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_APPEARANCE_CONTROLLED; @@ -77,13 +75,12 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INSET_PARENT_ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; +import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS; import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER; @@ -196,6 +193,7 @@ import android.view.contentcapture.MainContentCaptureSession; import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; import android.window.ClientWindowFrames; +import android.window.CompatOnBackInvokedCallback; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import android.window.SurfaceSyncer; @@ -339,13 +337,12 @@ public final class ViewRootImpl implements ViewParent, /** * The top level {@link OnBackInvokedDispatcher}. */ - private final WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher = - new WindowOnBackInvokedDispatcher(); + private final WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher; /** * Compatibility {@link OnBackInvokedCallback} that dispatches KEYCODE_BACK events * to view root for apps using legacy back behavior. */ - private OnBackInvokedCallback mCompatOnBackInvokedCallback; + private CompatOnBackInvokedCallback mCompatOnBackInvokedCallback; /** * Callback for notifying about global configuration changes. @@ -566,8 +563,6 @@ public final class ViewRootImpl implements ViewParent, private final WindowLayout mWindowLayout; - private ViewRootImpl mParentViewRoot; - // This is used to reduce the race between window focus changes being dispatched from // the window manager and input events coming through the input system. @GuardedBy("this") @@ -601,6 +596,14 @@ public final class ViewRootImpl implements ViewParent, */ private boolean mSyncBuffer = false; + /** + * Flag to determine whether the client needs to check with WMS if it can draw. WMS will notify + * the client that it can't draw if we're still in the middle of a sync set that includes this + * window. Once the sync is complete, the window can resume drawing. This is to ensure we don't + * deadlock the client by trying to request draws when there may not be any buffers available. + */ + private boolean mCheckIfCanDraw = false; + int mSyncSeqId = 0; int mLastSyncSeqId = 0; @@ -962,6 +965,8 @@ public final class ViewRootImpl implements ViewParent, mFastScrollSoundEffectsEnabled = audioManager.areNavigationRepeatSoundEffectsEnabled(); mScrollCaptureRequestTimeout = SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS; + mOnBackInvokedDispatcher = new WindowOnBackInvokedDispatcher( + context.getApplicationInfo().isOnBackInvokedCallbackEnabled()); } public static void addFirstDrawHandler(Runnable callback) { @@ -1185,7 +1190,6 @@ public final class ViewRootImpl implements ViewParent, if (panelParentView != null) { mAttachInfo.mPanelParentWindowToken = panelParentView.getApplicationWindowToken(); - mParentViewRoot = panelParentView.getViewRootImpl(); } mAdded = true; int res; /* = WindowManagerImpl.ADD_OKAY; */ @@ -1216,14 +1220,21 @@ public final class ViewRootImpl implements ViewParent, collectViewAttributes(); adjustLayoutParamsForCompatibility(mWindowAttributes); controlInsetsForCompatibility(mWindowAttributes); + + Rect attachedFrame = new Rect(); res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), userId, mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets, - mTempControls); + mTempControls, attachedFrame); + if (!attachedFrame.isValid()) { + attachedFrame = null; + } if (mTranslator != null) { mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); + mTranslator.translateRectInScreenToAppWindow(attachedFrame); } + mTmpFrames.attachedFrame = attachedFrame; } catch (RemoteException e) { mAdded = false; mView = null; @@ -1250,8 +1261,8 @@ public final class ViewRootImpl implements ViewParent, mWindowLayout.computeFrames(mWindowAttributes, state, displayCutoutSafe, winConfig.getBounds(), winConfig.getWindowingMode(), UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH, - mInsetsController.getRequestedVisibilities(), - getAttachedWindowFrame(), 1f /* compactScale */, mTmpFrames); + mInsetsController.getRequestedVisibilities(), 1f /* compactScale */, + mTmpFrames); setFrame(mTmpFrames.frame); registerBackCallbackOnWindow(); if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) { @@ -1373,14 +1384,6 @@ public final class ViewRootImpl implements ViewParent, } } - private Rect getAttachedWindowFrame() { - final int type = mWindowAttributes.type; - final boolean layoutAttached = (mParentViewRoot != null - && type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW - && type != TYPE_APPLICATION_ATTACHED_DIALOG); - return layoutAttached ? mParentViewRoot.mWinFrame : null; - } - /** * Register any kind of listeners if setView was success. */ @@ -1738,16 +1741,20 @@ public final class ViewRootImpl implements ViewParent, final Rect frame = frames.frame; final Rect displayFrame = frames.displayFrame; + final Rect attachedFrame = frames.attachedFrame; if (mTranslator != null) { mTranslator.translateRectInScreenToAppWindow(frame); mTranslator.translateRectInScreenToAppWindow(displayFrame); + mTranslator.translateRectInScreenToAppWindow(attachedFrame); } final boolean frameChanged = !mWinFrame.equals(frame); final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration); + final boolean attachedFrameChanged = LOCAL_LAYOUT + && !Objects.equals(mTmpFrames.attachedFrame, attachedFrame); final boolean displayChanged = mDisplay.getDisplayId() != displayId; final boolean resizeModeChanged = mResizeMode != resizeMode; - if (msg == MSG_RESIZED && !frameChanged && !configChanged && !displayChanged - && !resizeModeChanged && !forceNextWindowRelayout) { + if (msg == MSG_RESIZED && !frameChanged && !configChanged && !attachedFrameChanged + && !displayChanged && !resizeModeChanged && !forceNextWindowRelayout) { return; } @@ -1765,6 +1772,9 @@ public final class ViewRootImpl implements ViewParent, setFrame(frame); mTmpFrames.displayFrame.set(displayFrame); + if (mTmpFrames.attachedFrame != null && attachedFrame != null) { + mTmpFrames.attachedFrame.set(attachedFrame); + } if (mDragResizing && mUseMTRenderer) { boolean fullscreen = frame.equals(mPendingBackDropFrame); @@ -2698,6 +2708,9 @@ public final class ViewRootImpl implements ViewParent, mIsInTraversal = true; mWillDrawSoon = true; + boolean cancelDraw = false; + boolean isSyncRequest = false; + boolean windowSizeMayChange = false; WindowManager.LayoutParams lp = mWindowAttributes; @@ -2803,10 +2816,6 @@ public final class ViewRootImpl implements ViewParent, // Execute enqueued actions on every traversal in case a detached view enqueued an action getRunQueue().executeActions(mAttachInfo.mHandler); - if (mApplyInsetsRequested) { - dispatchApplyInsets(host); - } - if (mFirst) { // make sure touch mode code executes by setting cached value // to opposite of the added touch mode. @@ -2870,6 +2879,18 @@ public final class ViewRootImpl implements ViewParent, } } + if (mApplyInsetsRequested) { + dispatchApplyInsets(host); + if (mLayoutRequested) { + // Short-circuit catching a new layout request here, so + // we don't need to go through two layout passes when things + // change due to fitting system windows, which can happen a lot. + windowSizeMayChange |= measureHierarchy(host, lp, + mView.getContext().getResources(), + desiredWindowWidth, desiredWindowHeight); + } + } + if (layoutRequested) { // Clear this now, so that if anything requests a layout in the // rest of this function we will catch it and re-run a full @@ -2967,6 +2988,8 @@ public final class ViewRootImpl implements ViewParent, mViewFrameInfo.flags |= FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED; } relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); + cancelDraw = (relayoutResult & RELAYOUT_RES_CANCEL_AND_REDRAW) + == RELAYOUT_RES_CANCEL_AND_REDRAW; final boolean dragResizing = mPendingDragResizing; if (mSyncSeqId > mLastSyncSeqId) { mLastSyncSeqId = mSyncSeqId; @@ -2975,6 +2998,7 @@ public final class ViewRootImpl implements ViewParent, } reportNextDraw(); mSyncBuffer = true; + isSyncRequest = true; } final boolean surfaceControlChanged = @@ -3263,6 +3287,19 @@ public final class ViewRootImpl implements ViewParent, } } } else { + // If a relayout isn't going to happen, we still need to check if this window can draw + // when mCheckIfCanDraw is set. This is because it means we had a sync in the past, but + // have not been told by WMS that the sync is complete and that we can continue to draw + if (mCheckIfCanDraw) { + try { + cancelDraw = mWindowSession.cancelDraw(mWindow); + if (DEBUG_BLAST) { + Log.d(mTag, "cancelDraw returned " + cancelDraw); + } + } catch (RemoteException e) { + } + } + // Not the first pass and no window/insets/visibility change but the window // may have moved and we need check that and if so to update the left and right // in the attach info. We translate only the window frame since on window move @@ -3481,7 +3518,9 @@ public final class ViewRootImpl implements ViewParent, reportNextDraw(); } - boolean cancelAndRedraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw(); + mCheckIfCanDraw = isSyncRequest || cancelDraw; + + boolean cancelAndRedraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || cancelDraw; if (!cancelAndRedraw) { createSyncIfNeeded(); } @@ -8006,69 +8045,20 @@ public final class ViewRootImpl implements ViewParent, final int requestedWidth = (int) (mView.getMeasuredWidth() * appScale + 0.5f); final int requestedHeight = (int) (mView.getMeasuredHeight() * appScale + 0.5f); - int relayoutResult = 0; - WindowConfiguration winConfig = getConfiguration().windowConfiguration; - if (LOCAL_LAYOUT) { - if (mFirst || viewVisibility != mViewVisibility) { - relayoutResult = mWindowSession.updateVisibility(mWindow, params, viewVisibility, - mPendingMergedConfiguration, mSurfaceControl, mTempInsets, mTempControls); - if (mTranslator != null) { - mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); - mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); - } - mInsetsController.onStateChanged(mTempInsets); - mInsetsController.onControlsChanged(mTempControls); - - mPendingAlwaysConsumeSystemBars = - (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0; - } - final InsetsState state = mInsetsController.getState(); - final Rect displayCutoutSafe = mTempRect; - state.getDisplayCutoutSafe(displayCutoutSafe); - if (mWindowAttributes.type == TYPE_APPLICATION_STARTING) { - // TODO(b/210378379): Remove the special logic. - // Letting starting window use the window bounds from the pending config is for the - // fixed rotation, because the config is not overridden before the starting window - // is created. - winConfig = mPendingMergedConfiguration.getMergedConfiguration() - .windowConfiguration; - } - mWindowLayout.computeFrames(mWindowAttributes, state, displayCutoutSafe, - winConfig.getBounds(), winConfig.getWindowingMode(), requestedWidth, - requestedHeight, mInsetsController.getRequestedVisibilities(), - getAttachedWindowFrame(), 1f /* compatScale */, mTmpFrames); - - mWindowSession.updateLayout(mWindow, params, - insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mTmpFrames, - requestedWidth, requestedHeight); - - } else { - relayoutResult = mWindowSession.relayout(mWindow, params, - requestedWidth, requestedHeight, viewVisibility, - insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, - mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets, - mTempControls, mRelayoutBundle); - final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid"); - if (maybeSyncSeqId > 0) { - mSyncSeqId = maybeSyncSeqId; - } - - if (mTranslator != null) { - mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame); - mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame); - mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); - mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); - } - mInsetsController.onStateChanged(mTempInsets); - mInsetsController.onControlsChanged(mTempControls); - - mPendingAlwaysConsumeSystemBars = - (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0; + int relayoutResult = mWindowSession.relayout(mWindow, params, + requestedWidth, requestedHeight, viewVisibility, + insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, + mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets, + mTempControls, mRelayoutBundle); + final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid"); + if (maybeSyncSeqId > 0) { + mSyncSeqId = maybeSyncSeqId; } final int transformHint = SurfaceControl.rotationToBufferTransform( (mDisplayInstallOrientation + mDisplay.getRotation()) % 4); + final WindowConfiguration winConfig = getConfiguration().windowConfiguration; WindowLayout.computeSurfaceSize(mWindowAttributes, winConfig.getMaxBounds(), requestedWidth, requestedHeight, mTmpFrames.frame, mPendingDragResizing, mSurfaceSize); @@ -8117,10 +8107,23 @@ public final class ViewRootImpl implements ViewParent, destroySurface(); } + mPendingAlwaysConsumeSystemBars = + (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0; + if (restore) { params.restore(); } + + if (mTranslator != null) { + mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame); + mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame); + mTranslator.translateRectInScreenToAppWindow(mTmpFrames.attachedFrame); + mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); + mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); + } setFrame(mTmpFrames.frame); + mInsetsController.onStateChanged(mTempInsets); + mInsetsController.onControlsChanged(mTempControls); return relayoutResult; } @@ -10854,13 +10857,6 @@ public final class ViewRootImpl implements ViewParent, OnBackInvokedDispatcher.PRIORITY_DEFAULT, mCompatOnBackInvokedCallback); } - private void unregisterCompatOnBackInvokedCallback() { - if (mCompatOnBackInvokedCallback != null) { - mOnBackInvokedDispatcher.unregisterOnBackInvokedCallback(mCompatOnBackInvokedCallback); - mCompatOnBackInvokedCallback = null; - } - } - @Override public void setTouchableRegion(Region r) { if (r != null) { diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java index c320b262ebd7..9b6b2b906e8e 100644 --- a/core/java/android/view/WindowLayout.java +++ b/core/java/android/view/WindowLayout.java @@ -66,14 +66,15 @@ public class WindowLayout { public void computeFrames(WindowManager.LayoutParams attrs, InsetsState state, Rect displayCutoutSafe, Rect windowBounds, @WindowingMode int windowingMode, int requestedWidth, int requestedHeight, InsetsVisibilities requestedVisibilities, - Rect attachedWindowFrame, float compatScale, ClientWindowFrames outFrames) { + float compatScale, ClientWindowFrames frames) { final int type = attrs.type; final int fl = attrs.flags; final int pfl = attrs.privateFlags; final boolean layoutInScreen = (fl & FLAG_LAYOUT_IN_SCREEN) == FLAG_LAYOUT_IN_SCREEN; - final Rect outDisplayFrame = outFrames.displayFrame; - final Rect outParentFrame = outFrames.parentFrame; - final Rect outFrame = outFrames.frame; + final Rect attachedWindowFrame = frames.attachedFrame; + final Rect outDisplayFrame = frames.displayFrame; + final Rect outParentFrame = frames.parentFrame; + final Rect outFrame = frames.frame; // Compute bounds restricted by insets final Insets insets = state.calculateInsets(windowBounds, attrs.getFitInsetsTypes(), @@ -104,7 +105,7 @@ public class WindowLayout { final DisplayCutout cutout = state.getDisplayCutout(); final Rect displayCutoutSafeExceptMaybeBars = mTempDisplayCutoutSafeExceptMaybeBarsRect; displayCutoutSafeExceptMaybeBars.set(displayCutoutSafe); - outFrames.isParentFrameClippedByDisplayCutout = false; + frames.isParentFrameClippedByDisplayCutout = false; if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS && !cutout.isEmpty()) { // Ensure that windows with a non-ALWAYS display cutout mode are laid out in // the cutout safe zone. @@ -167,7 +168,7 @@ public class WindowLayout { if (!attachedInParent && !floatingInScreenWindow) { mTempRect.set(outParentFrame); outParentFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars); - outFrames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame); + frames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame); } outDisplayFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars); } @@ -287,12 +288,9 @@ public class WindowLayout { } } - if (DEBUG) Log.d(TAG, "computeWindowFrames " + attrs.getTitle() - + " outFrames=" + outFrames + if (DEBUG) Log.d(TAG, "computeFrames " + attrs.getTitle() + + " frames=" + frames + " windowBounds=" + windowBounds.toShortString() - + " attachedWindowFrame=" + (attachedWindowFrame != null - ? attachedWindowFrame.toShortString() - : "null") + " requestedWidth=" + requestedWidth + " requestedHeight=" + requestedHeight + " compatScale=" + compatScale diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index b818a41293ca..dd990e75e3cc 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -2591,6 +2591,7 @@ public interface WindowManager extends ViewManager { PRIVATE_FLAG_SYSTEM_ERROR, PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR, + PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT, PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY, PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH, PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME, @@ -2652,6 +2653,10 @@ public interface WindowManager extends ViewManager { equals = PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR, name = "FORCE_STATUS_BAR_VISIBLE"), @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT, + equals = PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT, + name = "LAYOUT_SIZE_EXTENDED_BY_CUTOUT"), + @ViewDebug.FlagToString( mask = PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY, equals = PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY, name = "FORCE_DECOR_VIEW_VISIBILITY"), diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index 85a5762f7f3d..25445abefca2 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -83,6 +83,11 @@ public final class WindowManagerGlobal { public static final int RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS = 1 << 3; /** + * The window manager has told the window it cannot draw this frame and should retry again. + */ + public static final int RELAYOUT_RES_CANCEL_AND_REDRAW = 1 << 4; + + /** * Flag for relayout: the client will be later giving * internal insets; as a result, the window will not impact other window * layouts until the insets are given. diff --git a/core/java/android/view/WindowlessWindowLayout.java b/core/java/android/view/WindowlessWindowLayout.java index 7cc50c579d0d..5bec5b6b6722 100644 --- a/core/java/android/view/WindowlessWindowLayout.java +++ b/core/java/android/view/WindowlessWindowLayout.java @@ -30,10 +30,10 @@ public class WindowlessWindowLayout extends WindowLayout { public void computeFrames(WindowManager.LayoutParams attrs, InsetsState state, Rect displayCutoutSafe, Rect windowBounds, @WindowingMode int windowingMode, int requestedWidth, int requestedHeight, InsetsVisibilities requestedVisibilities, - Rect attachedWindowFrame, float compatScale, ClientWindowFrames outFrames) { - outFrames.frame.set(0, 0, attrs.width, attrs.height); - outFrames.displayFrame.set(outFrames.frame); - outFrames.parentFrame.set(outFrames.frame); + float compatScale, ClientWindowFrames frames) { + frames.frame.set(0, 0, attrs.width, attrs.height); + frames.displayFrame.set(frames.frame); + frames.parentFrame.set(frames.frame); } } diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index a212348a2315..94da2741f71a 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -149,7 +149,7 @@ public class WindowlessWindowManager implements IWindowSession { public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities, InputChannel outInputChannel, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { + InsetsSourceControl[] outActiveControls, Rect outAttachedFrame) { final SurfaceControl.Builder b = new SurfaceControl.Builder(mSurfaceSession) .setFormat(attrs.format) .setBLASTLayer() @@ -181,6 +181,7 @@ public class WindowlessWindowManager implements IWindowSession { synchronized (this) { mStateForWindow.put(window.asBinder(), state); } + outAttachedFrame.set(0, 0, -1, -1); final int res = WindowManagerGlobal.ADD_OKAY | WindowManagerGlobal.ADD_FLAG_APP_VISIBLE | WindowManagerGlobal.ADD_FLAG_USE_BLAST; @@ -196,15 +197,15 @@ public class WindowlessWindowManager implements IWindowSession { public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities, InputChannel outInputChannel, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { + InsetsSourceControl[] outActiveControls, Rect outAttachedFrame) { return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibilities, - outInputChannel, outInsetsState, outActiveControls); + outInputChannel, outInsetsState, outActiveControls, outAttachedFrame); } @Override public int addToDisplayWithoutInputChannel(android.view.IWindow window, android.view.WindowManager.LayoutParams attrs, int viewVisibility, int layerStackId, - android.view.InsetsState insetsState) { + android.view.InsetsState insetsState, Rect outAttachedFrame) { return 0; } @@ -337,21 +338,6 @@ public class WindowlessWindowManager implements IWindowSession { } @Override - public int updateVisibility(IWindow window, WindowManager.LayoutParams inAttrs, - int viewVisibility, MergedConfiguration outMergedConfiguration, - SurfaceControl outSurfaceControl, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { - // TODO(b/161810301): Finish the implementation. - return 0; - } - - @Override - public void updateLayout(IWindow window, WindowManager.LayoutParams inAttrs, int flags, - ClientWindowFrames clientWindowFrames, int requestedWidth, int requestedHeight) { - // TODO(b/161810301): Finish the implementation. - } - - @Override public void prepareToReplaceWindows(android.os.IBinder appToken, boolean childrenOnly) { } @@ -552,4 +538,9 @@ public class WindowlessWindowManager implements IWindowSession { } } } + + @Override + public boolean cancelDraw(IWindow window) { + return false; + } } diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java index 51f3fe2551ad..929e81ed9044 100644 --- a/core/java/android/window/ClientWindowFrames.java +++ b/core/java/android/window/ClientWindowFrames.java @@ -17,6 +17,7 @@ package android.window; import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; @@ -40,6 +41,12 @@ public class ClientWindowFrames implements Parcelable { */ public final @NonNull Rect parentFrame = new Rect(); + /** + * The frame this window attaches to. If this is not null, this is the frame of the parent + * window. + */ + public @Nullable Rect attachedFrame; + public boolean isParentFrameClippedByDisplayCutout; public ClientWindowFrames() { @@ -49,6 +56,9 @@ public class ClientWindowFrames implements Parcelable { frame.set(other.frame); displayFrame.set(other.displayFrame); parentFrame.set(other.parentFrame); + if (other.attachedFrame != null) { + attachedFrame = new Rect(other.attachedFrame); + } isParentFrameClippedByDisplayCutout = other.isParentFrameClippedByDisplayCutout; } @@ -61,6 +71,7 @@ public class ClientWindowFrames implements Parcelable { frame.readFromParcel(in); displayFrame.readFromParcel(in); parentFrame.readFromParcel(in); + attachedFrame = in.readTypedObject(Rect.CREATOR); isParentFrameClippedByDisplayCutout = in.readBoolean(); } @@ -69,6 +80,7 @@ public class ClientWindowFrames implements Parcelable { frame.writeToParcel(dest, flags); displayFrame.writeToParcel(dest, flags); parentFrame.writeToParcel(dest, flags); + dest.writeTypedObject(attachedFrame, flags); dest.writeBoolean(isParentFrameClippedByDisplayCutout); } @@ -78,6 +90,7 @@ public class ClientWindowFrames implements Parcelable { return "ClientWindowFrames{frame=" + frame.toShortString(sb) + " display=" + displayFrame.toShortString(sb) + " parentFrame=" + parentFrame.toShortString(sb) + + (attachedFrame != null ? " attachedFrame=" + attachedFrame.toShortString() : "") + " parentClippedByDisplayCutout=" + isParentFrameClippedByDisplayCutout + "}"; } diff --git a/core/java/android/window/CompatOnBackInvokedCallback.java b/core/java/android/window/CompatOnBackInvokedCallback.java new file mode 100644 index 000000000000..81d7291d9f88 --- /dev/null +++ b/core/java/android/window/CompatOnBackInvokedCallback.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 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.window; + +/** + * Marker interface for {@link OnBackInvokedCallback} used for backward compatibility between the + * new system back and the old back event dispatching. Callbacks implementing this interface are + * allowed to be registered even if <code>enableOnbackInvoked</code> is set to false in the + * application manifest. + * @hide + */ +public interface CompatOnBackInvokedCallback extends OnBackInvokedCallback{ + + @Override + void onBackInvoked(); +} diff --git a/core/java/android/window/ProxyOnBackInvokedDispatcher.java b/core/java/android/window/ProxyOnBackInvokedDispatcher.java index 10d43e89a09d..8ad109317f4b 100644 --- a/core/java/android/window/ProxyOnBackInvokedDispatcher.java +++ b/core/java/android/window/ProxyOnBackInvokedDispatcher.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.util.Log; import android.util.Pair; +import android.window.WindowOnBackInvokedDispatcher.Checker; import java.util.ArrayList; import java.util.List; @@ -50,6 +51,11 @@ public class ProxyOnBackInvokedDispatcher implements OnBackInvokedDispatcher { private final Object mLock = new Object(); private OnBackInvokedDispatcher mActualDispatcher = null; private ImeOnBackInvokedDispatcher mImeDispatcher; + private final Checker mChecker; + + public ProxyOnBackInvokedDispatcher(boolean applicationCallBackEnabled) { + mChecker = new Checker(applicationCallBackEnabled); + } @Override public void registerOnBackInvokedCallback( @@ -58,11 +64,9 @@ public class ProxyOnBackInvokedDispatcher implements OnBackInvokedDispatcher { Log.v(TAG, String.format("Proxy register %s. mActualDispatcher=%s", callback, mActualDispatcher)); } - if (priority < 0) { - throw new IllegalArgumentException("Application registered OnBackInvokedCallback " - + "cannot have negative priority. Priority: " + priority); + if (mChecker.checkApplicationCallbackRegistration(priority, callback)) { + registerOnBackInvokedCallbackUnchecked(callback, priority); } - registerOnBackInvokedCallbackUnchecked(callback, priority); } @Override diff --git a/core/java/android/window/SizeConfigurationBuckets.java b/core/java/android/window/SizeConfigurationBuckets.java index f474f0a76cc6..998bec0892ae 100644 --- a/core/java/android/window/SizeConfigurationBuckets.java +++ b/core/java/android/window/SizeConfigurationBuckets.java @@ -104,24 +104,15 @@ public final class SizeConfigurationBuckets implements Parcelable { /** * Get the changes between two configurations but don't count changes in sizes if they don't * cross boundaries that are important to the app. - * - * This is a static helper to deal with null `buckets`. When no buckets have been specified, - * this actually filters out all 3 size-configs. This is legacy behavior. */ public static int filterDiff(int diff, @NonNull Configuration oldConfig, @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) { - final boolean nonSizeLayoutFieldsUnchanged = - areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout); if (buckets == null) { - // Only unflip CONFIG_SCREEN_LAYOUT if non-size-related attributes of screen layout do - // not change. - if (nonSizeLayoutFieldsUnchanged) { - return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE - | CONFIG_SCREEN_LAYOUT); - } else { - return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE); - } + return diff; } + + final boolean nonSizeLayoutFieldsUnchanged = + areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout); if ((diff & CONFIG_SCREEN_SIZE) != 0) { final boolean crosses = buckets.crossesHorizontalSizeThreshold(oldConfig.screenWidthDp, newConfig.screenWidthDp) diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 1d0bc5a6f0ba..d147524d3b3d 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -30,6 +30,7 @@ import android.view.IWindowSession; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; +import java.util.Objects; import java.util.TreeMap; /** @@ -62,6 +63,11 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { /** Holds all callbacks by priorities. */ private final TreeMap<Integer, ArrayList<OnBackInvokedCallback>> mOnBackInvokedCallbacks = new TreeMap<>(); + private final Checker mChecker; + + public WindowOnBackInvokedDispatcher(boolean applicationCallBackEnabled) { + mChecker = new Checker(applicationCallBackEnabled); + } /** * Sends the pending top callback (if one exists) to WM when the view root @@ -86,14 +92,16 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void registerOnBackInvokedCallback( @Priority int priority, @NonNull OnBackInvokedCallback callback) { - if (priority < 0) { - throw new IllegalArgumentException("Application registered OnBackInvokedCallback " - + "cannot have negative priority. Priority: " + priority); + if (mChecker.checkApplicationCallbackRegistration(priority, callback)) { + registerOnBackInvokedCallbackUnchecked(callback, priority); } - registerOnBackInvokedCallbackUnchecked(callback, priority); } - private void registerOnBackInvokedCallbackUnchecked( + /** + * Register a callback bypassing platform checks. This is used to register compatibility + * callbacks. + */ + public void registerOnBackInvokedCallbackUnchecked( @NonNull OnBackInvokedCallback callback, @Priority int priority) { if (mImeDispatcher != null) { mImeDispatcher.registerOnBackInvokedCallback(priority, callback); @@ -203,6 +211,14 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { return null; } + /** + * Returns the checker used to check whether a callback can be registered + */ + @NonNull + public Checker getChecker() { + return mChecker; + } + static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { private final WeakReference<OnBackInvokedCallback> mCallback; @@ -289,4 +305,41 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { mImeDispatcher = imeDispatcher; } + + + /** + * Class used to check whether a callback can be registered or not. This is meant to be + * shared with {@link ProxyOnBackInvokedDispatcher} which needs to do the same checks. + */ + public static class Checker { + + private final boolean mApplicationCallBackEnabled; + + public Checker(boolean applicationCallBackEnabled) { + mApplicationCallBackEnabled = applicationCallBackEnabled; + } + + /** + * Checks whether the given callback can be registered with the given priority. + * @return true if the callback can be added. + * @throws IllegalArgumentException if the priority is negative. + */ + public boolean checkApplicationCallbackRegistration(int priority, + OnBackInvokedCallback callback) { + if (!mApplicationCallBackEnabled + && !(callback instanceof CompatOnBackInvokedCallback)) { + Log.w("OnBackInvokedCallback", + "OnBackInvokedCallback is not enabled for the application." + + "\nSet 'android:enableOnBackInvokedCallback=\"true\"' in the" + + " application manifest."); + return false; + } + if (priority < 0) { + throw new IllegalArgumentException("Application registered OnBackInvokedCallback " + + "cannot have negative priority. Priority: " + priority); + } + Objects.requireNonNull(callback); + return true; + } + } } diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 52fd7fec932e..22340c6b0c55 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -75,6 +75,8 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_LOADING_TO_SHOW_INFO_WITH_ACTIONS; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_SHOW_FUNCTION_SCREEN_WITH_ACTIONS; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TAKE_SCREENSHOT; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_COLLAPSE; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_EXPAND; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__UNFOLD_ANIM; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_DIALOG_OPEN; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH; @@ -206,6 +208,8 @@ public class InteractionJankMonitor { public static final int CUJ_SETTINGS_TOGGLE = 57; public static final int CUJ_SHADE_DIALOG_OPEN = 58; public static final int CUJ_USER_DIALOG_OPEN = 59; + public static final int CUJ_TASKBAR_EXPAND = 60; + public static final int CUJ_TASKBAR_COLLAPSE = 61; private static final int NO_STATSD_LOGGING = -1; @@ -274,6 +278,8 @@ public class InteractionJankMonitor { UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_TOGGLE, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_DIALOG_OPEN, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_DIALOG_OPEN, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_EXPAND, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_COLLAPSE, }; private static volatile InteractionJankMonitor sInstance; @@ -354,6 +360,8 @@ public class InteractionJankMonitor { CUJ_SETTINGS_TOGGLE, CUJ_SHADE_DIALOG_OPEN, CUJ_USER_DIALOG_OPEN, + CUJ_TASKBAR_EXPAND, + CUJ_TASKBAR_COLLAPSE }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -792,6 +800,10 @@ public class InteractionJankMonitor { return "SHADE_DIALOG_OPEN"; case CUJ_USER_DIALOG_OPEN: return "USER_DIALOG_OPEN"; + case CUJ_TASKBAR_EXPAND: + return "TASKBAR_EXPAND"; + case CUJ_TASKBAR_COLLAPSE: + return "TASKBAR_COLLAPSE"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index e258fc19a75f..fb38bba8ee16 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -342,8 +342,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { boolean mDecorFitsSystemWindows = true; - private final ProxyOnBackInvokedDispatcher mProxyOnBackInvokedDispatcher = - new ProxyOnBackInvokedDispatcher(); + private final ProxyOnBackInvokedDispatcher mProxyOnBackInvokedDispatcher; static class WindowManagerHolder { static final IWindowManager sWindowManager = IWindowManager.Stub.asInterface( @@ -358,6 +357,8 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { mLayoutInflater = LayoutInflater.from(context); mRenderShadowsInCompositor = Settings.Global.getInt(context.getContentResolver(), DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR, 1) != 0; + mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher( + context.getApplicationInfo().isOnBackInvokedCallbackEnabled()); } /** diff --git a/core/res/res/layout/notification_template_material_base.xml b/core/res/res/layout/notification_template_material_base.xml index 0756d68063f5..fd787f6ea470 100644 --- a/core/res/res/layout/notification_template_material_base.xml +++ b/core/res/res/layout/notification_template_material_base.xml @@ -138,7 +138,7 @@ </LinearLayout> - <ImageView + <com.android.internal.widget.CachingIconView android:id="@+id/right_icon" android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" @@ -150,6 +150,8 @@ android:clipToOutline="true" android:importantForAccessibility="no" android:scaleType="centerCrop" + android:maxDrawableWidth="@dimen/notification_right_icon_size" + android:maxDrawableHeight="@dimen/notification_right_icon_size" /> <FrameLayout diff --git a/core/res/res/layout/notification_template_right_icon.xml b/core/res/res/layout/notification_template_right_icon.xml index f163ed5f955a..8b3b795f7473 100644 --- a/core/res/res/layout/notification_template_right_icon.xml +++ b/core/res/res/layout/notification_template_right_icon.xml @@ -13,7 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<ImageView +<com.android.internal.widget.CachingIconView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/right_icon" android:layout_width="@dimen/notification_right_icon_size" @@ -25,4 +25,6 @@ android:clipToOutline="true" android:importantForAccessibility="no" android:scaleType="centerCrop" + android:maxDrawableWidth="@dimen/notification_right_icon_size" + android:maxDrawableHeight="@dimen/notification_right_icon_size" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 2171987c081e..a8c7bf249ada 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2944,7 +2944,7 @@ <!-- System bluetooth stack package name --> <string name="config_systemBluetoothStack" translatable="false"> - com.android.bluetooth.services + com.android.bluetooth </string> <!-- Flag indicating that the media framework should not allow changes or mute on any @@ -5180,11 +5180,11 @@ <!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. --> <bool name="config_letterboxIsEducationEnabled">false</bool> - <!-- Default min aspect ratio for unresizable apps which is used when an app doesn't specify - android:minAspectRatio in accordance with CDD 7.1.1.2 requirement: - https://source.android.com/compatibility/12/android-12-cdd#7112_screen_aspect_ratio. - An exception will be thrown if the given aspect ratio < 4:3. --> - <item name="config_letterboxDefaultMinAspectRatioForUnresizableApps" format="float" type="dimen">1.5</item> + <!-- Default min aspect ratio for unresizable apps which are eligible for size compat mode. + Values <= 1.0 will be ignored. Activity min/max aspect ratio restrictions will still be + espected so this override can control the maximum screen area that can be occupied by + the app in the letterbox mode. --> + <item name="config_letterboxDefaultMinAspectRatioForUnresizableApps" format="float" type="dimen">0.0</item> <!-- Whether using split screen aspect ratio as a default aspect ratio for unresizable apps. --> <bool name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled">false</bool> diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index 04857ec756d2..3e4b1cc87ef8 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -161,7 +161,10 @@ <!-- ChooserActivityTest permissions--> <uses-permission android:name="android.permission.SET_CLIP_SOURCE" /> - <application android:theme="@style/Theme" android:supportsRtl="true"> + <application + android:theme="@style/Theme" + android:supportsRtl="true" + android:enableOnBackInvokedCallback="true"> <uses-library android:name="android.test.runner" /> <uses-library android:name="org.apache.http.legacy" android:required="false" /> <meta-data diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index c57aa740124c..f448cb3091e7 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -64,7 +64,7 @@ public class WindowOnBackInvokedDispatcherTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mDispatcher = new WindowOnBackInvokedDispatcher(); + mDispatcher = new WindowOnBackInvokedDispatcher(true /* applicationCallbackEnabled */); mDispatcher.attachToWindow(mWindowSession, mWindow); } diff --git a/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java b/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java new file mode 100644 index 000000000000..7c7649813824 --- /dev/null +++ b/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Test for verifying {@link android.os.Bundle} recycles the underlying parcel where needed. + * + * <p>Build/Install/Run: + * atest FrameworksMockingCoreTests:android.os.BundleRecyclingTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class BundleRecyclingTest { + private Parcel mParcelSpy; + private Bundle mBundle; + + @Before + public void setUp() throws Exception { + setUpBundle(/* hasLazy */ true); + } + + @Test + public void bundleClear_whenUnparcelledWithoutLazy_recyclesParcelOnce() { + setUpBundle(/* hasLazy */ false); + // Will unparcel and immediately recycle parcel + assertNotNull(mBundle.getString("key")); + verify(mParcelSpy, times(1)).recycle(); + assertFalse(mBundle.isDefinitelyEmpty()); + + // Should not recycle again + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + assertTrue(mBundle.isDefinitelyEmpty()); + } + + @Test + public void bundleClear_whenParcelled_recyclesParcel() { + assertTrue(mBundle.isParcelled()); + verify(mParcelSpy, times(0)).recycle(); + + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + assertTrue(mBundle.isDefinitelyEmpty()); + + // Should not recycle again + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + } + + @Test + public void bundleClear_whenUnparcelledWithLazyValueUnwrapped_recyclesParcel() { + // Will unparcel with a lazy value, and immediately unwrap the lazy value, + // with no lazy values left at the end of getParcelable + assertNotNull(mBundle.getParcelable("key", CustomParcelable.class)); + verify(mParcelSpy, times(0)).recycle(); + + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + assertTrue(mBundle.isDefinitelyEmpty()); + + // Should not recycle again + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + } + + @Test + public void bundleClear_whenUnparcelledWithLazy_recyclesParcel() { + // Will unparcel but keep the CustomParcelable lazy + assertFalse(mBundle.isEmpty()); + verify(mParcelSpy, times(0)).recycle(); + + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + assertTrue(mBundle.isDefinitelyEmpty()); + + // Should not recycle again + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + } + + @Test + public void bundleClear_whenClearedWithSharedParcel_doesNotRecycleParcel() { + Bundle copy = new Bundle(); + copy.putAll(mBundle); + + mBundle.clear(); + assertTrue(mBundle.isDefinitelyEmpty()); + + copy.clear(); + assertTrue(copy.isDefinitelyEmpty()); + + verify(mParcelSpy, never()).recycle(); + } + + @Test + public void bundleClear_whenClearedWithCopiedParcel_doesNotRecycleParcel() { + // Will unparcel but keep the CustomParcelable lazy + assertFalse(mBundle.isEmpty()); + + Bundle copy = mBundle.deepCopy(); + copy.putAll(mBundle); + + mBundle.clear(); + assertTrue(mBundle.isDefinitelyEmpty()); + + copy.clear(); + assertTrue(copy.isDefinitelyEmpty()); + + verify(mParcelSpy, never()).recycle(); + } + + private void setUpBundle(boolean hasLazy) { + AtomicReference<Parcel> parcel = new AtomicReference<>(); + StaticMockitoSession session = mockitoSession() + .strictness(Strictness.STRICT_STUBS) + .spyStatic(Parcel.class) + .startMocking(); + doAnswer((Answer<Parcel>) invocationOnSpy -> { + Parcel spy = (Parcel) invocationOnSpy.callRealMethod(); + spyOn(spy); + parcel.set(spy); + return spy; + }).when(() -> Parcel.obtain()); + + Bundle bundle = new Bundle(); + bundle.setClassLoader(getClass().getClassLoader()); + Parcel p = createBundle(hasLazy); + bundle.readFromParcel(p); + p.recycle(); + + session.finishMocking(); + + mParcelSpy = parcel.get(); + mBundle = bundle; + } + + /** + * Create a test bundle, parcel it and return the parcel. + */ + private Parcel createBundle(boolean hasLazy) { + final Bundle source = new Bundle(); + if (hasLazy) { + source.putParcelable("key", new CustomParcelable(13, "Tiramisu")); + } else { + source.putString("key", "tiramisu"); + } + return getParcelledBundle(source); + } + + /** + * Take a bundle, write it to a parcel and return the parcel. + */ + private Parcel getParcelledBundle(Bundle bundle) { + final Parcel p = Parcel.obtain(); + // Don't use p.writeParcelabe(), which would write the creator, which we don't need. + bundle.writeToParcel(p, 0); + p.setDataPosition(0); + return p; + } + + private static class CustomParcelable implements Parcelable { + public final int integer; + public final String string; + + CustomParcelable(int integer, String string) { + this.integer = integer; + this.string = string; + } + + protected CustomParcelable(Parcel in) { + integer = in.readInt(); + string = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(integer); + out.writeString(string); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof CustomParcelable)) { + return false; + } + CustomParcelable that = (CustomParcelable) other; + return integer == that.integer && string.equals(that.string); + } + + @Override + public int hashCode() { + return Objects.hash(integer, string); + } + + public static final Creator<CustomParcelable> CREATOR = new Creator<CustomParcelable>() { + @Override + public CustomParcelable createFromParcel(Parcel in) { + return new CustomParcelable(in); + } + @Override + public CustomParcelable[] newArray(int size) { + return new CustomParcelable[size]; + } + }; + } +} diff --git a/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java index fa4aa803c75e..ed857e8fc960 100644 --- a/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java +++ b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java @@ -88,26 +88,15 @@ public class SizeConfigurationBucketsTest { } /** - * Tests that null size configuration buckets unflips the correct configuration flags. + * Tests that {@code null} size configuration buckets do not unflip the configuration flags. */ @Test public void testNullSizeConfigurationBuckets() { - // Check that all 3 size configurations are filtered out of the diff if the buckets are null - // and non-size attributes of screen layout are unchanged. Add a non-size related config - // change (i.e. CONFIG_LOCALE) to test that the diff is not set to zero. final int diff = CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE | CONFIG_SCREEN_LAYOUT | CONFIG_LOCALE; final int filteredDiffNonSizeLayoutUnchanged = SizeConfigurationBuckets.filterDiff(diff, Configuration.EMPTY, Configuration.EMPTY, null); - assertEquals(CONFIG_LOCALE, filteredDiffNonSizeLayoutUnchanged); - - // Check that only screen size and smallest screen size are filtered out of the diff if the - // buckets are null and non-size attributes of screen layout are changed. - final Configuration newConfig = new Configuration(); - newConfig.screenLayout |= SCREENLAYOUT_ROUND_YES; - final int filteredDiffNonSizeLayoutChanged = SizeConfigurationBuckets.filterDiff(diff, - Configuration.EMPTY, newConfig, null); - assertEquals(CONFIG_SCREEN_LAYOUT | CONFIG_LOCALE, filteredDiffNonSizeLayoutChanged); + assertEquals(diff, filteredDiffNonSizeLayoutUnchanged); } /** diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index f8da95d04411..6706e4e87e0d 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -475,12 +475,6 @@ "group": "WM_DEBUG_ADD_REMOVE", "at": "com\/android\/server\/wm\/ResetTargetTaskHelper.java" }, - "-1635750891": { - "message": "Received remote change for Display[%d], applied: [%dx%d, rot = %d]", - "level": "VERBOSE", - "group": "WM_DEBUG_CONFIGURATION", - "at": "com\/android\/server\/wm\/RemoteDisplayChangeController.java" - }, "-1633115609": { "message": "Key dispatch not paused for screen off", "level": "VERBOSE", @@ -1711,6 +1705,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, + "-417730399": { + "message": "Preparing to sync a window that was already in the sync, so try dropping buffer. win=%s", + "level": "DEBUG", + "group": "WM_DEBUG_SYNC_ENGINE", + "at": "com\/android\/server\/wm\/WindowState.java" + }, "-415865166": { "message": "findFocusedWindow: Found new focus @ %s", "level": "VERBOSE", @@ -2137,6 +2137,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimation.java" }, + "-4263657": { + "message": "Got a buffer for request id=%d but latest request is id=%d. Since the buffer is out-of-date, drop it. win=%s", + "level": "DEBUG", + "group": "WM_DEBUG_SYNC_ENGINE", + "at": "com\/android\/server\/wm\/WindowState.java" + }, "3593205": { "message": "commitVisibility: %s: visible=%b mVisibleRequested=%b", "level": "VERBOSE", @@ -2599,6 +2605,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/TaskFragment.java" }, + "385237117": { + "message": "moveFocusableActivityToTop: already on top and focused, activity=%s", + "level": "DEBUG", + "group": "WM_DEBUG_FOCUS", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "385595355": { "message": "Starting animation on %s: type=%d, anim=%s", "level": "VERBOSE", @@ -3403,6 +3415,12 @@ "group": "WM_DEBUG_BOOT", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1239439010": { + "message": "moveFocusableActivityToTop: set focused, activity=%s", + "level": "DEBUG", + "group": "WM_DEBUG_FOCUS", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "1252594551": { "message": "Window types in WindowContext and LayoutParams.type should match! Type from LayoutParams is %d, but type from WindowContext is %d", "level": "WARN", @@ -3877,12 +3895,6 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/WindowStateAnimator.java" }, - "1764619787": { - "message": "Remote change for Display[%d]: timeout reached", - "level": "VERBOSE", - "group": "WM_DEBUG_CONFIGURATION", - "at": "com\/android\/server\/wm\/RemoteDisplayChangeController.java" - }, "1774661765": { "message": "Devices still not ready after waiting %d milliseconds before attempting to detect safe mode.", "level": "WARN", @@ -3991,12 +4003,6 @@ "group": "WM_DEBUG_STARTING_WINDOW", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "1856211951": { - "message": "moveFocusableActivityToTop: already on top, activity=%s", - "level": "DEBUG", - "group": "WM_DEBUG_FOCUS", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "1856783490": { "message": "resumeTopActivity: Restarting %s", "level": "DEBUG", diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 1629b6ace35d..239621eeed1e 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -40,6 +40,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.net.Uri; import android.os.Build; +import android.os.Trace; import android.system.ErrnoException; import android.system.Os; import android.util.DisplayMetrics; @@ -223,13 +224,21 @@ public final class ImageDecoder implements AutoCloseable { public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { return nCreate(mData, mOffset, mLength, preferAnimation, this); } + + @Override + public String toString() { + return "ByteArraySource{len=" + mLength + "}"; + } } private static class ByteBufferSource extends Source { ByteBufferSource(@NonNull ByteBuffer buffer) { mBuffer = buffer; + mLength = mBuffer.limit() - mBuffer.position(); } + private final ByteBuffer mBuffer; + private final int mLength; @Override public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { @@ -241,6 +250,11 @@ public final class ImageDecoder implements AutoCloseable { ByteBuffer buffer = mBuffer.slice(); return nCreate(buffer, buffer.position(), buffer.limit(), preferAnimation, this); } + + @Override + public String toString() { + return "ByteBufferSource{len=" + mLength + "}"; + } } private static class ContentResolverSource extends Source { @@ -285,6 +299,16 @@ public final class ImageDecoder implements AutoCloseable { return createFromAssetFileDescriptor(assetFd, preferAnimation, this); } + + @Override + public String toString() { + String uri = mUri.toString(); + if (uri.length() > 90) { + // We want to keep the Uri usable - usually the authority and the end is important. + uri = uri.substring(0, 80) + ".." + uri.substring(uri.length() - 10); + } + return "ContentResolverSource{uri=" + uri + "}"; + } } @NonNull @@ -399,6 +423,11 @@ public final class ImageDecoder implements AutoCloseable { return createFromStream(is, false, preferAnimation, this); } } + + @Override + public String toString() { + return "InputStream{s=" + mInputStream + "}"; + } } /** @@ -444,6 +473,11 @@ public final class ImageDecoder implements AutoCloseable { return createFromAsset(ais, preferAnimation, this); } } + + @Override + public String toString() { + return "AssetInputStream{s=" + mAssetInputStream + "}"; + } } private static class ResourceSource extends Source { @@ -485,6 +519,17 @@ public final class ImageDecoder implements AutoCloseable { return createFromAsset((AssetInputStream) is, preferAnimation, this); } + + @Override + public String toString() { + // Try to return a human-readable name for debugging purposes. + try { + return "Resource{name=" + mResources.getResourceName(mResId) + "}"; + } catch (Resources.NotFoundException e) { + // It's ok if we don't find it, fall back to ID. + } + return "Resource{id=" + mResId + "}"; + } } /** @@ -521,6 +566,11 @@ public final class ImageDecoder implements AutoCloseable { InputStream is = mAssets.open(mFileName); return createFromAsset((AssetInputStream) is, preferAnimation, this); } + + @Override + public String toString() { + return "AssetSource{file=" + mFileName + "}"; + } } private static class FileSource extends Source { @@ -534,6 +584,11 @@ public final class ImageDecoder implements AutoCloseable { public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { return createFromFile(mFile, preferAnimation, this); } + + @Override + public String toString() { + return "FileSource{file=" + mFile + "}"; + } } private static class CallableSource extends Source { @@ -557,6 +612,11 @@ public final class ImageDecoder implements AutoCloseable { } return createFromAssetFileDescriptor(assetFd, preferAnimation, this); } + + @Override + public String toString() { + return "CallableSource{obj=" + mCallable.toString() + "}"; + } } /** @@ -1763,61 +1823,65 @@ public final class ImageDecoder implements AutoCloseable { @NonNull private static Drawable decodeDrawableImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeDrawable"); try (ImageDecoder decoder = src.createImageDecoder(true /*preferAnimation*/)) { decoder.mSource = src; decoder.callHeaderDecoded(listener, src); - if (decoder.mUnpremultipliedRequired) { - // Though this could be supported (ignored) for opaque images, - // it seems better to always report this error. - throw new IllegalStateException("Cannot decode a Drawable " + - "with unpremultiplied pixels!"); - } + try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) { + if (decoder.mUnpremultipliedRequired) { + // Though this could be supported (ignored) for opaque images, + // it seems better to always report this error. + throw new IllegalStateException( + "Cannot decode a Drawable with unpremultiplied pixels!"); + } - if (decoder.mMutable) { - throw new IllegalStateException("Cannot decode a mutable " + - "Drawable!"); - } + if (decoder.mMutable) { + throw new IllegalStateException("Cannot decode a mutable Drawable!"); + } - // this call potentially manipulates the decoder so it must be performed prior to - // decoding the bitmap and after decode set the density on the resulting bitmap - final int srcDensity = decoder.computeDensity(src); - if (decoder.mAnimated) { - // AnimatedImageDrawable calls postProcessAndRelease only if - // mPostProcessor exists. - ImageDecoder postProcessPtr = decoder.mPostProcessor == null ? - null : decoder; - decoder.checkState(true); - Drawable d = new AnimatedImageDrawable(decoder.mNativePtr, - postProcessPtr, decoder.mDesiredWidth, - decoder.mDesiredHeight, decoder.getColorSpacePtr(), - decoder.checkForExtended(), srcDensity, - src.computeDstDensity(), decoder.mCropRect, - decoder.mInputStream, decoder.mAssetFd); - // d has taken ownership of these objects. - decoder.mInputStream = null; - decoder.mAssetFd = null; - return d; - } + // this call potentially manipulates the decoder so it must be performed prior to + // decoding the bitmap and after decode set the density on the resulting bitmap + final int srcDensity = decoder.computeDensity(src); + if (decoder.mAnimated) { + // AnimatedImageDrawable calls postProcessAndRelease only if + // mPostProcessor exists. + ImageDecoder postProcessPtr = decoder.mPostProcessor == null ? null : decoder; + decoder.checkState(true); + Drawable d = new AnimatedImageDrawable(decoder.mNativePtr, + postProcessPtr, decoder.mDesiredWidth, + decoder.mDesiredHeight, decoder.getColorSpacePtr(), + decoder.checkForExtended(), srcDensity, + src.computeDstDensity(), decoder.mCropRect, + decoder.mInputStream, decoder.mAssetFd); + // d has taken ownership of these objects. + decoder.mInputStream = null; + decoder.mAssetFd = null; + return d; + } - Bitmap bm = decoder.decodeBitmapInternal(); - bm.setDensity(srcDensity); + Bitmap bm = decoder.decodeBitmapInternal(); + bm.setDensity(srcDensity); - Resources res = src.getResources(); - byte[] np = bm.getNinePatchChunk(); - if (np != null && NinePatch.isNinePatchChunk(np)) { - Rect opticalInsets = new Rect(); - bm.getOpticalInsets(opticalInsets); - Rect padding = decoder.mOutPaddingRect; - if (padding == null) { - padding = new Rect(); + Resources res = src.getResources(); + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + Rect opticalInsets = new Rect(); + bm.getOpticalInsets(opticalInsets); + Rect padding = decoder.mOutPaddingRect; + if (padding == null) { + padding = new Rect(); + } + nGetPadding(decoder.mNativePtr, padding); + return new NinePatchDrawable(res, bm, np, padding, + opticalInsets, null); } - nGetPadding(decoder.mNativePtr, padding); - return new NinePatchDrawable(res, bm, np, padding, - opticalInsets, null); - } - return new BitmapDrawable(res, bm); + return new BitmapDrawable(res, bm); + } + } finally { + // Close the ImageDecoder#decode trace. + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } } @@ -1867,26 +1931,51 @@ public final class ImageDecoder implements AutoCloseable { @NonNull private static Bitmap decodeBitmapImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeBitmap"); try (ImageDecoder decoder = src.createImageDecoder(false /*preferAnimation*/)) { decoder.mSource = src; decoder.callHeaderDecoded(listener, src); + try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) { + // this call potentially manipulates the decoder so it must be performed prior to + // decoding the bitmap + final int srcDensity = decoder.computeDensity(src); + Bitmap bm = decoder.decodeBitmapInternal(); + bm.setDensity(srcDensity); - // this call potentially manipulates the decoder so it must be performed prior to - // decoding the bitmap - final int srcDensity = decoder.computeDensity(src); - Bitmap bm = decoder.decodeBitmapInternal(); - bm.setDensity(srcDensity); - - Rect padding = decoder.mOutPaddingRect; - if (padding != null) { - byte[] np = bm.getNinePatchChunk(); - if (np != null && NinePatch.isNinePatchChunk(np)) { - nGetPadding(decoder.mNativePtr, padding); + Rect padding = decoder.mOutPaddingRect; + if (padding != null) { + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + nGetPadding(decoder.mNativePtr, padding); + } } + return bm; } + } finally { + // Close the ImageDecoder#decode trace. + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } + } - return bm; + /** + * This describes the decoder in traces to ease debugging. It has to be called after + * header has been decoded and width/height have been populated. It should be used + * inside a try-with-resources call to automatically complete the trace. + */ + private static AutoCloseable traceDecoderSource(ImageDecoder decoder) { + final boolean resourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES); + if (resourceTracingEnabled) { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder)); } + + return new AutoCloseable() { + @Override + public void close() throws Exception { + if (resourceTracingEnabled) { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } + } + }; } // This method may modify the decoder so it must be called prior to performing the decode @@ -1994,6 +2083,66 @@ public final class ImageDecoder implements AutoCloseable { } } + /** + * Returns a short string describing what passed ImageDecoder is loading - + * it reports image dimensions, desired dimensions (if any) and source resource. + * + * The string appears in perf traces to simplify search for slow or memory intensive + * image loads. + * + * Example: ID#w=300;h=250;dw=150;dh=150;src=Resource{name=@resource} + * + * @hide + */ + private static String describeDecoderForTrace(@NonNull ImageDecoder decoder) { + StringBuilder builder = new StringBuilder(); + // Source dimensions + builder.append("ID#w="); + builder.append(decoder.mWidth); + builder.append(";h="); + builder.append(decoder.mHeight); + // Desired dimensions (if present) + if (decoder.mDesiredWidth != decoder.mWidth + || decoder.mDesiredHeight != decoder.mHeight) { + builder.append(";dw="); + builder.append(decoder.mDesiredWidth); + builder.append(";dh="); + builder.append(decoder.mDesiredHeight); + } + // Source description + builder.append(";src="); + builder.append(decoder.mSource); + return builder.toString(); + } + + /** + * Records a trace with information about the source being decoded - dimensions, + * desired dimensions and source information. + * + * It significantly eases debugging of slow resource loads on main thread and + * possible large memory consumers. + * + * @hide + */ + private static final class ImageDecoderSourceTrace implements AutoCloseable { + + private final boolean mResourceTracingEnabled; + + ImageDecoderSourceTrace(ImageDecoder decoder) { + mResourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES); + if (mResourceTracingEnabled) { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder)); + } + } + + @Override + public void close() { + if (mResourceTracingEnabled) { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } + } + } + private static native ImageDecoder nCreate(long asset, boolean preferAnimation, Source src) throws IOException; private static native ImageDecoder nCreate(ByteBuffer buffer, int position, int limit, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index b90b4c1b39a1..2f79caeec7ba 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -24,9 +24,9 @@ import static androidx.window.extensions.embedding.SplitContainer.getFinishSecon import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; -import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair; -import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; +import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; import android.app.Activity; @@ -389,6 +389,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * in a state that the caller shouldn't handle. */ @VisibleForTesting + @GuardedBy("mLock") boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) { if (isInPictureInPicture(activity) || activity.isFinishing()) { // We don't embed activity when it is in PIP, or finishing. Return true since we don't @@ -589,8 +590,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** Finds the activity below the given activity. */ + @VisibleForTesting @Nullable - private Activity findActivityBelow(@NonNull Activity activity) { + Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; final TaskFragmentContainer container = getContainerWithActivity(activity); if (container != null) { @@ -614,6 +616,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Checks if there is a rule to split the two activities. If there is one, puts them into split * and returns {@code true}. Otherwise, returns {@code false}. */ + @GuardedBy("mLock") private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); @@ -624,25 +627,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() - && canReuseContainer(splitRule, splitContainer.getSplitRule()) - && !boundsSmallerThanMinDimensions(primaryContainer.getLastRequestedBounds(), - getMinDimensions(primaryActivity))) { + && canReuseContainer(splitRule, splitContainer.getSplitRule())) { // Can launch in the existing secondary container if the rules share the same // presentation. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); - if (secondaryContainer == getContainerWithActivity(secondaryActivity) - && !boundsSmallerThanMinDimensions(secondaryContainer.getLastRequestedBounds(), - getMinDimensions(secondaryActivity))) { + if (secondaryContainer == getContainerWithActivity(secondaryActivity)) { // The activity is already in the target TaskFragment. return true; } secondaryContainer.addPendingAppearedActivity(secondaryActivity); final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.reparentActivityToTaskFragment( - secondaryContainer.getTaskFragmentToken(), - secondaryActivity.getActivityToken()); - mPresenter.applyTransaction(wct); - return true; + if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, + secondaryActivity, null /* secondaryIntent */) + != RESULT_EXPAND_FAILED_NO_TF_INFO) { + wct.reparentActivityToTaskFragment( + secondaryContainer.getTaskFragmentToken(), + secondaryActivity.getActivityToken()); + mPresenter.applyTransaction(wct); + return true; + } } // Create new split pair. mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule); @@ -650,6 +653,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } private void onActivityConfigurationChanged(@NonNull Activity activity) { + if (activity.isFinishing()) { + // Do nothing if the activity is currently finishing. + return; + } + if (isInPictureInPicture(activity)) { // We don't embed activity when it is in PIP. return; @@ -795,6 +803,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a container for the new activity intent to launch into as splitting with the primary * activity. */ + @GuardedBy("mLock") @Nullable private TaskFragmentContainer getSecondaryContainerForSplitIfAny( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @@ -808,16 +817,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() && (canReuseContainer(splitRule, splitContainer.getSplitRule()) // TODO(b/231845476) we should always respect clearTop. - || !respectClearTop)) { - final Rect secondaryBounds = splitContainer.getSecondaryContainer() - .getLastRequestedBounds(); - if (secondaryBounds.isEmpty() - || !boundsSmallerThanMinDimensions(secondaryBounds, - getMinDimensions(intent))) { - // Can launch in the existing secondary container if the rules share the same - // presentation. - return splitContainer.getSecondaryContainer(); - } + || !respectClearTop) + && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, + null /* secondaryActivity */, intent) != RESULT_EXPAND_FAILED_NO_TF_INFO) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + return splitContainer.getSecondaryContainer(); } // Create a new TaskFragment to split with the primary activity for the new activity. return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, @@ -871,6 +876,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * if needed. * @param taskId parent Task of the new TaskFragment. */ + @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { if (activityInTask == null) { @@ -884,7 +890,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen pendingAppearedIntent, taskContainer, this); if (!taskContainer.isTaskBoundsInitialized()) { // Get the initial bounds before the TaskFragment has appeared. - final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask); + final Rect taskBounds = getNonEmbeddedActivityBounds(activityInTask); if (!taskContainer.setTaskBounds(taskBounds)) { Log.w(TAG, "Can't find bounds from activity=" + activityInTask); } @@ -1127,6 +1133,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } boolean launchPlaceholderIfNecessary(@NonNull Activity activity, boolean isOnCreated) { + if (activity.isFinishing()) { + return false; + } + final TaskFragmentContainer container = getContainerWithActivity(activity); // Don't launch placeholder if the container is occluded. if (container != null && container != getTopActiveContainer(container.getTaskId())) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 1b79ad999435..a89847a30d20 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -65,6 +65,41 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * No need to expand the splitContainer because screen is big enough to + * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied. + */ + static final int RESULT_NOT_EXPANDED = 0; + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * The splitContainer should be expanded. It is usually because minimum dimensions is not + * satisfied. + * @see #shouldShowSideBySide(Rect, SplitRule, Pair) + */ + static final int RESULT_EXPANDED = 1; + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * The splitContainer should be expanded, but the client side hasn't received + * {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer + * instead. + */ + static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2; + + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)} + */ + @IntDef(value = { + RESULT_NOT_EXPANDED, + RESULT_EXPANDED, + RESULT_EXPAND_FAILED_NO_TF_INFO, + }) + private @interface ResultCode {} + private final SplitController mController; SplitPresenter(@NonNull Executor executor, SplitController controller) { @@ -396,6 +431,44 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.updateWindowingMode(wct, fragmentToken, windowingMode); } + /** + * Expands the split container if the current split bounds are smaller than the Activity or + * Intent that is added to the container. + * + * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} + * and if {@link android.window.TaskFragmentInfo} has reported to the client side. + */ + @ResultCode + int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, + @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity, + @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) { + if (secondaryActivity == null && secondaryIntent == null) { + throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be" + + " non-null."); + } + final Rect taskBounds = getParentContainerBounds(primaryActivity); + final Pair<Size, Size> minDimensionsPair; + if (secondaryActivity != null) { + minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); + } else { + minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity, + secondaryIntent); + } + // Expand the splitContainer if minimum dimensions are not satisfied. + if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) { + // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment + // bounds. Return failure to create a new SplitContainer which fills task bounds. + if (splitContainer.getPrimaryContainer().getInfo() == null + || splitContainer.getSecondaryContainer().getInfo() == null) { + return RESULT_EXPAND_FAILED_NO_TF_INFO; + } + expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken()); + expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken()); + return RESULT_EXPANDED; + } + return RESULT_NOT_EXPANDED; + } + static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); } @@ -565,11 +638,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { if (container != null) { return getParentContainerBounds(container); } - return getTaskBoundsFromActivity(activity); + // Obtain bounds from Activity instead because the Activity hasn't been embedded yet. + return getNonEmbeddedActivityBounds(activity); } + /** + * Obtains the bounds from a non-embedded Activity. + * <p> + * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most + * cases unless we want to obtain task bounds before + * {@link TaskContainer#isTaskBoundsInitialized()}. + */ @NonNull - static Rect getTaskBoundsFromActivity(@NonNull Activity activity) { + static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) { final WindowConfiguration windowConfiguration = activity.getResources().getConfiguration().windowConfiguration; if (!activity.isInMultiWindowMode()) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java index 1ac33173668b..c4f37091a491 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java @@ -83,9 +83,9 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { if (TaskFragmentAnimationController.DEBUG) { - Log.v(TAG, "onAnimationCancelled"); + Log.v(TAG, "onAnimationCancelled: isKeyguardOccluded=" + isKeyguardOccluded); } mHandler.post(this::cancelAnimation); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index cfb32050e32f..18086f552ea3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -25,7 +25,10 @@ import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; import android.annotation.Nullable; import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityManager.AppTask; import android.app.Application; +import android.app.WindowConfiguration; import android.content.Context; import android.graphics.Rect; import android.os.Bundle; @@ -180,7 +183,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { if (displayId != DEFAULT_DISPLAY) { Log.w(TAG, "This sample doesn't support display features on secondary displays"); return features; - } else if (activity.isInMultiWindowMode()) { + } else if (isTaskInMultiWindowMode(activity)) { // It is recommended not to report any display features in multi-window mode, since it // won't be possible to synchronize the display feature positions with window movement. return features; @@ -204,6 +207,32 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } /** + * Checks whether the task associated with the activity is in multi-window. If task info is not + * available it defaults to {@code true}. + */ + private boolean isTaskInMultiWindowMode(@NonNull Activity activity) { + final ActivityManager am = activity.getSystemService(ActivityManager.class); + if (am == null) { + return true; + } + + final List<AppTask> appTasks = am.getAppTasks(); + final int taskId = activity.getTaskId(); + AppTask task = null; + for (AppTask t : appTasks) { + if (t.getTaskInfo().taskId == taskId) { + task = t; + break; + } + } + if (task == null) { + // The task might be removed on the server already. + return true; + } + return WindowConfiguration.inMultiWindowMode(task.getTaskInfo().getWindowingMode()); + } + + /** * Returns {@link true} if a {@link Rect} has zero width and zero height, * {@code false} otherwise. */ diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java index 835c40365cda..effc1a3ef3ea 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock; import android.annotation.NonNull; import android.app.Activity; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; @@ -57,13 +58,21 @@ public class EmbeddingTestUtils { /** Creates a rule to always split the given activity and the given intent. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { + return createSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); + } + + /** Creates a rule to always split the given activity and the given intent. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent, boolean clearTop) { final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); return new SplitPairRule.Builder( activityPair -> false, targetPair::equals, w -> true) .setSplitRatio(SPLIT_RATIO) - .setShouldClearTop(true) + .setShouldClearTop(clearTop) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) .build(); } @@ -75,6 +84,14 @@ public class EmbeddingTestUtils { true /* clearTop */); } + /** Creates a rule to always split the given activities. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { + return createSplitRule(primaryActivity, secondaryActivity, + DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, + clearTop); + } + /** Creates a rule to always split the given activities with the given finish behaviors. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, @@ -105,4 +122,12 @@ public class EmbeddingTestUtils { false /* isTaskFragmentClearedForPip */, new Point()); } + + static ActivityInfo createActivityInfoWithMinDimensions() { + ActivityInfo aInfo = new ActivityInfo(); + final Rect primaryBounds = getSplitBounds(true /* isPrimary */); + aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, + primaryBounds.width() + 1, primaryBounds.height() + 1); + return aInfo; + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index ef7728cec387..042547fd30f2 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; @@ -34,6 +35,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; @@ -436,6 +438,50 @@ public class SplitControllerTest { } @Test + public void testResolveStartActivityIntent_shouldExpandSplitContainer() { + final Intent intent = new Intent().setComponent( + new ComponentName(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class)); + setupSplitRule(mActivity, intent, false /* clearTop */); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity)); + } + + @Test + public void testResolveStartActivityIntent_noInfo_shouldCreateSplitContainer() { + final Intent intent = new Intent().setComponent( + new ComponentName(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class)); + setupSplitRule(mActivity, intent, false /* clearTop */); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); + + final TaskFragmentContainer secondaryContainer = mSplitController + .getContainerWithActivity(secondaryActivity); + secondaryContainer.mInfo = null; + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertNotEquals(container, secondaryContainer); + } + + @Test public void testPlaceActivityInTopContainer() { mSplitController.placeActivityInTopContainer(mActivity); @@ -787,11 +833,7 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(mActivity, activityBelow); - ActivityInfo aInfo = new ActivityInfo(); - final Rect primaryBounds = getSplitBounds(true /* isPrimary */); - aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, - primaryBounds.width() + 1, primaryBounds.height() + 1); - doReturn(aInfo).when(mActivity).getActivityInfo(); + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); @@ -810,17 +852,12 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(activityBelow, mActivity); - ActivityInfo aInfo = new ActivityInfo(); - final Rect secondaryBounds = getSplitBounds(false /* isPrimary */); - aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, - secondaryBounds.width() + 1, secondaryBounds.height() + 1); - doReturn(aInfo).when(mActivity).getActivityInfo(); + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); container.addPendingAppearedActivity(mActivity); - // Allow to split as primary. boolean result = mSplitController.resolveActivityToContainer(mActivity, false /* isOnReparent */); @@ -828,6 +865,29 @@ public class SplitControllerTest { assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */); } + // Suppress GuardedBy warning on unit tests + @SuppressWarnings("GuardedBy") + @Test + public void testResolveActivityToContainer_minDimensions_shouldExpandSplitContainer() { + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, secondaryActivity, false /* clearTop */); + + setupSplitRule(primaryActivity, mActivity, false /* clearTop */); + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); + doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity)); + + clearInvocations(mSplitPresenter); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */); + assertEquals(mSplitController.getContainerWithActivity(secondaryActivity), + mSplitController.getContainerWithActivity(mActivity)); + verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any()); + } + @Test public void testResolveActivityToContainer_inUnknownTaskFragment() { doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity); @@ -944,23 +1004,41 @@ public class SplitControllerTest { /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { - final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent); + setupSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent, boolean clearTop) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent, clearTop); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { - final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity); + setupSplitRule(primaryActivity, secondaryActivity, true /* clearTop */); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity, clearTop); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } /** Adds a pair of TaskFragments as split for the given activities. */ private void addSplitTaskFragments(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { + addSplitTaskFragments(primaryActivity, secondaryActivity, true /* clearTop */); + } + + /** Adds a pair of TaskFragments as split for the given activities. */ + private void addSplitTaskFragments(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { registerSplitPair(createMockTaskFragmentContainer(primaryActivity), createMockTaskFragmentContainer(secondaryActivity), - createSplitRule(primaryActivity, secondaryActivity)); + createSplitRule(primaryActivity, secondaryActivity, clearTop)); } /** Registers the two given TaskFragments as split pair. */ @@ -1011,16 +1089,18 @@ public class SplitControllerTest { if (primaryContainer.mInfo != null) { final Rect primaryBounds = matchParentBounds ? new Rect() : getSplitBounds(true /* isPrimary */); + final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED + : WINDOWING_MODE_MULTI_WINDOW; assertTrue(primaryContainer.areLastRequestedBoundsEqual(primaryBounds)); - assertTrue(primaryContainer.isLastRequestedWindowingModeEqual( - WINDOWING_MODE_MULTI_WINDOW)); + assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); } if (secondaryContainer.mInfo != null) { final Rect secondaryBounds = matchParentBounds ? new Rect() : getSplitBounds(false /* isPrimary */); + final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED + : WINDOWING_MODE_MULTI_WINDOW; assertTrue(secondaryContainer.areLastRequestedBoundsEqual(secondaryBounds)); - assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual( - WINDOWING_MODE_MULTI_WINDOW)); + assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); } } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index acc398a27baf..d79319666c01 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -20,11 +20,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_END; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_FILL; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPANDED; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_NOT_EXPANDED; import static androidx.window.extensions.embedding.SplitPresenter.getBoundsForPosition; import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; @@ -34,6 +39,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -49,6 +55,7 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.util.Pair; import android.util.Size; @@ -195,6 +202,52 @@ public class SplitPresenterTest { splitRule, mActivity, minDimensionsPair)); } + @Test + public void testExpandSplitContainerIfNeeded() { + SplitContainer splitContainer = mock(SplitContainer.class); + Activity secondaryActivity = createMockActivity(); + SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); + TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); + TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID); + doReturn(splitRule).when(splitContainer).getSplitRule(); + doReturn(primaryTf).when(splitContainer).getPrimaryContainer(); + doReturn(secondaryTf).when(splitContainer).getSecondaryContainer(); + + assertThrows(IllegalArgumentException.class, () -> + mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, + null /* secondaryActivity */, null /* secondaryIntent */)); + + assertEquals(RESULT_NOT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, + splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); + verify(mPresenter, never()).expandTaskFragment(any(), any()); + + doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo(); + assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded( + mTransaction, splitContainer, mActivity, secondaryActivity, + null /* secondaryIntent */)); + + primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity)); + secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity)); + + assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, + splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(primaryTf.getTaskFragmentToken())); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(secondaryTf.getTaskFragmentToken())); + + clearInvocations(mPresenter); + + assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, + splitContainer, mActivity, null /* secondaryActivity */, + new Intent(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class))); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(primaryTf.getTaskFragmentToken())); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(secondaryTf.getTaskFragmentToken())); + } + private Activity createMockActivity() { final Activity activity = mock(Activity.class); final Configuration activityConfig = new Configuration(); @@ -203,6 +256,7 @@ public class SplitPresenterTest { doReturn(mActivityResources).when(activity).getResources(); doReturn(activityConfig).when(mActivityResources).getConfiguration(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); + doReturn(mock(IBinder.class)).when(activity).getActivityToken(); return activity; } } diff --git a/libs/WindowManager/Shell/res/values-television/config.xml b/libs/WindowManager/Shell/res/values-television/config.xml index 86ca65526336..cc0333efd82b 100644 --- a/libs/WindowManager/Shell/res/values-television/config.xml +++ b/libs/WindowManager/Shell/res/values-television/config.xml @@ -43,4 +43,13 @@ <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself if a custom action is present before closing it. --> <integer name="config_pipForceCloseDelay">5000</integer> + + <!-- Animation duration when exit starting window: fade out icon --> + <integer name="starting_window_app_reveal_icon_fade_out_duration">0</integer> + + <!-- Animation duration when exit starting window: reveal app --> + <integer name="starting_window_app_reveal_anim_delay">0</integer> + + <!-- Animation duration when exit starting window: reveal app --> + <integer name="starting_window_app_reveal_anim_duration">0</integer> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index b2f09895d7d8..68a08513e7f5 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -157,7 +157,7 @@ <string name="accessibility_bubble_dismissed">Bubble dismissed.</string> <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] --> - <string name="restart_button_description">Tap to restart this app and go full screen.</string> + <string name="restart_button_description">Tap to restart this app for a better view.</string> <!-- Description of the camera compat button for applying stretched issues treatment in the hint for compatibility control. [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java index e71a59d26740..8c0affb0a432 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java @@ -31,13 +31,15 @@ public interface BackAnimation { /** * Called when a {@link MotionEvent} is generated by a back gesture. * - * @param event the original {@link MotionEvent} - * @param action the original {@link KeyEvent#getAction()} when the event was dispatched to + * @param touchX the X touch position of the {@link MotionEvent}. + * @param touchY the Y touch position of the {@link MotionEvent}. + * @param keyAction the original {@link KeyEvent#getAction()} when the event was dispatched to * the process. This is forwarded separately because the input pipeline may mutate * the {#event} action state later. * @param swipeEdge the edge from which the swipe begins. */ - void onBackMotion(MotionEvent event, int action, @BackEvent.SwipeEdge int swipeEdge); + void onBackMotion(float touchX, float touchY, int keyAction, + @BackEvent.SwipeEdge int swipeEdge); /** * Sets whether the back gesture is past the trigger threshold or not. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 0cb56d72004d..0cf2b28921e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -184,8 +184,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Override public void onBackMotion( - MotionEvent event, int action, @BackEvent.SwipeEdge int swipeEdge) { - mShellExecutor.execute(() -> onMotionEvent(event, action, swipeEdge)); + float touchX, float touchY, int keyAction, @BackEvent.SwipeEdge int swipeEdge) { + mShellExecutor.execute(() -> onMotionEvent(touchX, touchY, keyAction, swipeEdge)); } @Override @@ -256,33 +256,34 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont * Called when a new motion event needs to be transferred to this * {@link BackAnimationController} */ - public void onMotionEvent(MotionEvent event, int action, @BackEvent.SwipeEdge int swipeEdge) { + public void onMotionEvent(float touchX, float touchY, int keyAction, + @BackEvent.SwipeEdge int swipeEdge) { if (mTransitionInProgress) { return; } - if (action == MotionEvent.ACTION_MOVE) { + if (keyAction == MotionEvent.ACTION_MOVE) { if (!mBackGestureStarted) { // Let the animation initialized here to make sure the onPointerDownOutsideFocus // could be happened when ACTION_DOWN, it may change the current focus that we // would access it when startBackNavigation. - initAnimation(event); + initAnimation(touchX, touchY); } - onMove(event, swipeEdge); - } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + onMove(touchX, touchY, swipeEdge); + } else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, - "Finishing gesture with event action: %d", action); + "Finishing gesture with event action: %d", keyAction); onGestureFinished(); } } - private void initAnimation(MotionEvent event) { + private void initAnimation(float touchX, float touchY) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted); if (mBackGestureStarted || mBackNavigationInfo != null) { Log.e(TAG, "Animation is being initialized but is already started."); finishAnimation(); } - mInitTouchLocation.set(event.getX(), event.getY()); + mInitTouchLocation.set(touchX, touchY); mBackGestureStarted = true; try { @@ -351,18 +352,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mTransaction.setVisibility(screenshotSurface, true); } - private void onMove(MotionEvent event, @BackEvent.SwipeEdge int swipeEdge) { + private void onMove(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) { if (!mBackGestureStarted || mBackNavigationInfo == null) { return; } - int deltaX = Math.round(event.getX() - mInitTouchLocation.x); + int deltaX = Math.round(touchX - mInitTouchLocation.x); float progressThreshold = PROGRESS_THRESHOLD >= 0 ? PROGRESS_THRESHOLD : mProgressThreshold; float progress = Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1); int backType = mBackNavigationInfo.getType(); RemoteAnimationTarget animationTarget = mBackNavigationInfo.getDepartingAnimationTarget(); BackEvent backEvent = new BackEvent( - event.getX(), event.getY(), progress, swipeEdge, animationTarget); + touchX, touchY, progress, swipeEdge, animationTarget); IOnBackInvokedCallback targetCallback = null; if (shouldDispatchToLauncher(backType)) { targetCallback = mBackToLauncherCallback; 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 1e369899e354..a8c1071eb69e 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 @@ -68,15 +68,15 @@ import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.Transitions; -import com.android.wm.shell.unfold.UnfoldAnimationController; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.UnfoldAnimationController; import com.android.wm.shell.unfold.UnfoldBackgroundController; import com.android.wm.shell.unfold.UnfoldTransitionHandler; import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator; import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator; import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator; -import com.android.wm.shell.unfold.qualifier.UnfoldTransition; import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition; +import com.android.wm.shell.unfold.qualifier.UnfoldTransition; import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -218,6 +218,7 @@ public abstract class WMShellModule { PipKeepClearAlgorithm pipKeepClearAlgorithm, PipBoundsState pipBoundsState, PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, + PipTransitionState pipTransitionState, PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, @@ -227,7 +228,7 @@ public abstract class WMShellModule { return Optional.ofNullable(PipController.create(context, displayController, pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, - pipMediaController, phonePipMenuController, pipTaskOrganizer, + pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index 3b3091a9caf3..bbc47e47afc5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -86,12 +86,12 @@ public interface Pip { } /** - * Registers the pinned stack animation listener. + * Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed. * - * @param callback The callback of pinned stack animation. + * @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()} + * when it's changed. */ - default void setPinnedStackAnimationListener(Consumer<Boolean> callback) { - } + default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {} /** * Set the pinned stack with {@link PipAnimationController.AnimationType} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 4eba1697b595..cf2734c375f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -591,7 +591,7 @@ public class PipAnimationController { final Rect insets = computeInsets(fraction); getSurfaceTransactionHelper().scaleAndCrop(tx, leash, sourceHintRect, initialSourceValue, bounds, insets, - isInPipDirection); + isInPipDirection, fraction); if (shouldApplyCornerRadius()) { final Rect sourceBounds = new Rect(initialContainerRect); sourceBounds.inset(insets); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index a017a2674359..c0bc108baada 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -104,7 +104,7 @@ public class PipSurfaceTransactionHelper { public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, SurfaceControl leash, Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets, - boolean isInPipDirection) { + boolean isInPipDirection, float fraction) { mTmpDestinationRect.set(sourceBounds); // Similar to {@link #scale}, we want to position the surface relative to the screen // coordinates so offset the bounds to 0,0 @@ -116,9 +116,13 @@ public class PipSurfaceTransactionHelper { if (isInPipDirection && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) { // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only. - scale = sourceBounds.width() <= sourceBounds.height() + final float endScale = sourceBounds.width() <= sourceBounds.height() ? (float) destinationBounds.width() / sourceRectHint.width() : (float) destinationBounds.height() / sourceRectHint.height(); + final float startScale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + scale = (1 - fraction) * startScale + fraction * endScale; } else { scale = sourceBounds.width() <= sourceBounds.height() ? (float) destinationBounds.width() / sourceBounds.width() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index bd386b5681d8..22b0ccbc8488 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -942,7 +942,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // Re-set the PIP bounds to none. mPipBoundsState.setBounds(new Rect()); mPipUiEventLoggerLogger.setTaskInfo(null); - mPipMenuController.detach(); + mMainExecutor.executeDelayed(() -> mPipMenuController.detach(), 0); if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) { mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); @@ -1472,6 +1472,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, "%s: Abort animation, invalid leash", TAG); return null; } + if (isInPipDirection(direction) + && !isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) { + // The given source rect hint is too small for enter PiP animation, reset it to null. + sourceHintRect = null; + } final int rotationDelta = mWaitForFixedRotation ? deltaRotation(mCurrentRotation, mNextRotation) : Surface.ROTATION_0; @@ -1546,6 +1551,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** + * This is a situation in which the source rect hint on at least one axis is smaller + * than the destination bounds, which represents a problem because we would have to scale + * up that axis to fit the bounds. So instead, just fallback to the non-source hint + * animation in this case. + * + * @return {@code false} if the given source is too small to use for the entering animation. + */ + private boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) { + return sourceRectHint != null + && sourceRectHint.width() > destinationBounds.width() + && sourceRectHint.height() > destinationBounds.height(); + } + + /** * Sync with {@link SplitScreenController} on destination bounds if PiP is going to * split screen. * diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 28427a808d90..05a890fc65ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -42,6 +42,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SP import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; import static com.android.wm.shell.transition.Transitions.isOpeningType; +import android.animation.Animator; import android.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; @@ -248,6 +249,13 @@ public class PipTransition extends PipTransitionController { return false; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + end(); + } + /** Helper to identify whether this handler is currently the one playing an animation */ private boolean isAnimatingLocally() { return mFinishTransaction != null; @@ -283,6 +291,13 @@ public class PipTransition extends PipTransitionController { } @Override + public void end() { + Animator animator = mPipAnimationController.getCurrentAnimator(); + if (animator == null) return; + animator.end(); + } + + @Override public boolean handleRotateDisplay(int startRotation, int endRotation, WindowContainerTransaction wct) { if (mRequestedEnterTransition != null && mOneShotAnimationType == ANIM_TYPE_ALPHA) { @@ -700,7 +715,7 @@ public class PipTransition extends PipTransitionController { mSurfaceTransactionHelper .crop(finishTransaction, leash, destinationBounds) .round(finishTransaction, leash, true /* applyCornerRadius */); - mPipMenuController.attach(leash); + mTransitions.getMainExecutor().executeDelayed(() -> mPipMenuController.attach(leash), 0); if (taskInfo.pictureInPictureParams != null && taskInfo.pictureInPictureParams.isAutoEnterEnabled() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index d3f69f6762f9..90a2695bdf90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -28,9 +28,7 @@ import android.app.TaskInfo; import android.content.ComponentName; import android.content.pm.ActivityInfo; import android.graphics.Rect; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -56,7 +54,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected final ShellTaskOrganizer mShellTaskOrganizer; protected final PipMenuController mPipMenuController; protected final Transitions mTransitions; - private final Handler mMainHandler; private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); protected PipTaskOrganizer mPipOrganizer; @@ -144,7 +141,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipAnimationController = pipAnimationController; mTransitions = transitions; - mMainHandler = new Handler(Looper.getMainLooper()); if (Transitions.ENABLE_SHELL_TRANSITIONS) { transitions.addHandler(this); } @@ -237,6 +233,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH @NonNull final Transitions.TransitionFinishCallback finishCallback) { } + /** End the currently-playing PiP animation. */ + public void end() { + } + /** * Callback interface for PiP transitions (both from and to PiP mode) */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java index 85e56b7dd99f..1a4be3b41911 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java @@ -17,12 +17,15 @@ package com.android.wm.shell.pip; import android.annotation.IntDef; +import android.annotation.NonNull; import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.pm.ActivityInfo; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; /** * Used to keep track of PiP leash state as it appears and animates by {@link PipTaskOrganizer} and @@ -37,6 +40,9 @@ public class PipTransitionState { public static final int ENTERED_PIP = 4; public static final int EXITING_PIP = 5; + private final List<OnPipTransitionStateChangedListener> mOnPipTransitionStateChangedListeners = + new ArrayList<>(); + /** * If set to {@code true}, no entering PiP transition would be kicked off and most likely * it's due to the fact that Launcher is handling the transition directly when swiping @@ -65,7 +71,13 @@ public class PipTransitionState { } public void setTransitionState(@TransitionState int state) { - mState = state; + if (mState != state) { + for (int i = 0; i < mOnPipTransitionStateChangedListeners.size(); i++) { + mOnPipTransitionStateChangedListeners.get(i).onPipTransitionStateChanged( + mState, state); + } + mState = state; + } } public @TransitionState int getTransitionState() { @@ -73,8 +85,7 @@ public class PipTransitionState { } public boolean isInPip() { - return mState >= TASK_APPEARED - && mState != EXITING_PIP; + return isInPip(mState); } public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) { @@ -94,4 +105,23 @@ public class PipTransitionState { return mState < ENTERING_PIP || mState == EXITING_PIP; } + + public void addOnPipTransitionStateChangedListener( + @NonNull OnPipTransitionStateChangedListener listener) { + mOnPipTransitionStateChangedListeners.add(listener); + } + + public void removeOnPipTransitionStateChangedListener( + @NonNull OnPipTransitionStateChangedListener listener) { + mOnPipTransitionStateChangedListeners.remove(listener); + } + + public static boolean isInPip(@TransitionState int state) { + return state >= TASK_APPEARED && state != EXITING_PIP; + } + + public interface OnPipTransitionStateChangedListener { + void onPipTransitionStateChanged(@TransitionState int oldState, + @TransitionState int newState); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index c3e6d82df781..3000998f210d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -84,6 +84,7 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.transition.Transitions; @@ -128,11 +129,14 @@ public class PipController implements PipTransitionController.PipTransitionCallb protected PhonePipMenuController mMenuController; protected PipTaskOrganizer mPipTaskOrganizer; + private PipTransitionState mPipTransitionState; protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener = new PipControllerPinnedTaskListener(); private boolean mIsKeyguardShowingOrAnimating; + private Consumer<Boolean> mOnIsInPipStateChangedListener; + private interface PipAnimationListener { /** * Notifies the listener that the Pip animation is started. @@ -291,6 +295,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb PipKeepClearAlgorithm pipKeepClearAlgorithm, PipBoundsState pipBoundsState, PipMotionHelper pipMotionHelper, PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, + PipTransitionState pipTransitionState, PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, @@ -305,7 +310,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb return new PipController(context, displayController, pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, pipMediaController, - phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController, + phonePipMenuController, pipTaskOrganizer, pipTransitionState, + pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor) .mImpl; @@ -321,6 +327,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb PipMediaController pipMediaController, PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, + PipTransitionState pipTransitionState, PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, @@ -344,6 +351,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState = pipBoundsState; mPipMotionHelper = pipMotionHelper; mPipTaskOrganizer = pipTaskOrganizer; + mPipTransitionState = pipTransitionState; mMainExecutor = mainExecutor; mMediaController = pipMediaController; mMenuController = phonePipMenuController; @@ -370,6 +378,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb onDisplayChanged(mDisplayController.getDisplayLayout(displayId), false /* saveRestoreSnapFraction */); }); + mPipTransitionState.addOnPipTransitionStateChangedListener((oldState, newState) -> { + if (mOnIsInPipStateChangedListener != null) { + final boolean wasInPip = PipTransitionState.isInPip(oldState); + final boolean nowInPip = PipTransitionState.isInPip(newState); + if (nowInPip != wasInPip) { + mOnIsInPipStateChangedListener.accept(nowInPip); + } + } + }); mPipBoundsState.setOnMinimalSizeChangeCallback( () -> { // The minimal size drives the normal bounds, so they need to be recalculated. @@ -664,6 +681,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb } } + private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + mOnIsInPipStateChangedListener = callback; + if (mOnIsInPipStateChangedListener != null) { + callback.accept(mPipTransitionState.isInPip()); + } + } + private void setShelfHeightLocked(boolean visible, int height) { final int shelfHeight = visible ? height : 0; mPipBoundsState.setShelfVisibility(visible, shelfHeight); @@ -941,6 +965,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override + public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + mMainExecutor.execute(() -> { + PipController.this.setOnIsInPipStateChangedListener(callback); + }); + } + + @Override public void setPinnedStackAnimationType(int animationType) { mMainExecutor.execute(() -> { PipController.this.setPinnedStackAnimationType(animationType); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index f7057d454df9..e55729a883e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -225,9 +225,25 @@ class SplitScreenTransitions { void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { - if (mergeTarget == mAnimatingTransition && mActiveRemoteHandler != null) { + if (mergeTarget != mAnimatingTransition) return; + if (mActiveRemoteHandler != null) { mActiveRemoteHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + } else { + for (int i = mAnimations.size() - 1; i >= 0; --i) { + final Animator anim = mAnimations.get(i); + mTransitions.getAnimExecutor().execute(anim::end); + } + } + } + + boolean end() { + // If its remote, there's nothing we can do right now. + if (mActiveRemoteHandler != null) return false; + for (int i = mAnimations.size() - 1; i >= 0; --i) { + final Animator anim = mAnimations.get(i); + mTransitions.getAnimExecutor().execute(anim::end); } + return true; } void onTransitionMerged(@NonNull IBinder transition) { 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 6cfb700fc16a..59b0afe22acb 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 @@ -457,10 +457,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { onRemoteAnimationFinishedOrCancelled(evictWct); try { - adapter.getRunner().onAnimationCancelled(); + adapter.getRunner().onAnimationCancelled(isKeyguardOccluded); } catch (RemoteException e) { Slog.e(TAG, "Error starting remote animation", e); } @@ -1521,6 +1521,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.mergeAnimation(transition, info, t, mergeTarget, finishCallback); } + /** Jump the current transition animation to the end. */ + public boolean end() { + return mSplitTransitions.end(); + } + @Override public void onTransitionMerged(@NonNull IBinder transition) { mSplitTransitions.onTransitionMerged(transition); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index 95bc579a4a51..19d3acbf28d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -20,10 +20,8 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.graphics.Color.WHITE; import static android.graphics.Color.alpha; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.view.ViewRootImpl.LOCAL_LAYOUT; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; -import static android.view.WindowLayout.UNSPECIFIED_LENGTH; import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; import static android.view.WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; @@ -53,7 +51,6 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManager.TaskDescription; import android.app.ActivityThread; -import android.app.WindowConfiguration; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; @@ -80,7 +77,6 @@ import android.view.SurfaceSession; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; -import android.view.WindowLayout; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.window.ClientWindowFrames; @@ -212,8 +208,6 @@ public class TaskSnapshotWindow { final IWindowSession session = WindowManagerGlobal.getWindowSession(); final SurfaceControl surfaceControl = new SurfaceControl(); final ClientWindowFrames tmpFrames = new ClientWindowFrames(); - final WindowLayout windowLayout = new WindowLayout(); - final Rect displayCutoutSafe = new Rect(); final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0]; final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); @@ -238,7 +232,8 @@ public class TaskSnapshotWindow { try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay"); final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId, - info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls); + info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls, + new Rect()); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (res < 0) { Slog.w(TAG, "Failed to add snapshot starting window res=" + res); @@ -250,25 +245,9 @@ public class TaskSnapshotWindow { window.setOuter(snapshotSurface); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); - if (LOCAL_LAYOUT) { - if (!surfaceControl.isValid()) { - session.updateVisibility(window, layoutParams, View.VISIBLE, - tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls); - } - tmpInsetsState.getDisplayCutoutSafe(displayCutoutSafe); - final WindowConfiguration winConfig = - tmpMergedConfiguration.getMergedConfiguration().windowConfiguration; - windowLayout.computeFrames(layoutParams, tmpInsetsState, displayCutoutSafe, - winConfig.getBounds(), winConfig.getWindowingMode(), UNSPECIFIED_LENGTH, - UNSPECIFIED_LENGTH, info.requestedVisibilities, - null /* attachedWindowFrame */, 1f /* compatScale */, tmpFrames); - session.updateLayout(window, layoutParams, 0 /* flags */, tmpFrames, - UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH); - } else { - session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, - tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, - tmpControls, new Bundle()); - } + session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, + tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, + tmpControls, new Bundle()); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); 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 1ffe26df729f..7234d559e153 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 @@ -53,10 +53,18 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { private static class MixedTransition { static final int TYPE_ENTER_PIP_FROM_SPLIT = 1; + /** The default animation for this mixed transition. */ + static final int ANIM_TYPE_DEFAULT = 0; + + /** For ENTER_PIP_FROM_SPLIT, indicates that this is a to-home animation. */ + static final int ANIM_TYPE_GOING_HOME = 1; + final int mType; + int mAnimType = 0; final IBinder mTransition; Transitions.TransitionFinishCallback mFinishCallback = null; + Transitions.TransitionHandler mLeftoversHandler = null; /** * Mixed transitions are made up of multiple "parts". This keeps track of how many @@ -128,7 +136,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { MixedTransition mixed = null; for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { if (mActiveTransitions.get(i).mTransition != transition) continue; - mixed = mActiveTransitions.remove(i); + mixed = mActiveTransitions.get(i); break; } if (mixed == null) return false; @@ -137,6 +145,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction, finishCallback); } else { + mActiveTransitions.remove(mixed); throw new IllegalStateException("Starting mixed animation without a known mixed type? " + mixed.mType); } @@ -178,6 +187,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { --mixed.mInFlightSubAnimations; if (mixed.mInFlightSubAnimations > 0) return; + mActiveTransitions.remove(mixed); if (isGoingHome) { mSplitHandler.onTransitionAnimationComplete(); } @@ -216,8 +226,8 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { finishCB); // Dispatch the rest of the transition normally. This will most-likely be taken by // recents or default handler. - mPlayer.dispatchTransition(mixed.mTransition, everythingElse, otherStartT, - finishTransaction, finishCB, this); + mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, everythingElse, + otherStartT, finishTransaction, finishCB, this); } else { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just " + "forward animation to Pip-Handler."); @@ -235,6 +245,32 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { + for (int i = 0; i < mActiveTransitions.size(); ++i) { + if (mActiveTransitions.get(i) != mergeTarget) continue; + MixedTransition mixed = mActiveTransitions.get(i); + if (mixed.mInFlightSubAnimations <= 0) { + // Already done, so no need to end it. + return; + } + if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { + if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) { + boolean ended = mSplitHandler.end(); + // If split couldn't end (because it is remote), then don't end everything else + // since we have to play out the animation anyways. + if (!ended) return; + mPipHandler.end(); + if (mixed.mLeftoversHandler != null) { + mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, + finishCallback); + } + } else { + mPipHandler.end(); + } + } else { + throw new IllegalStateException("Playing a mixed transition with unknown type? " + + mixed.mType); + } + } } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index c3eaa8ee1da0..dcd6277966dd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -523,6 +523,18 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return true; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + ArrayList<Animator> anims = mAnimations.get(mergeTarget); + if (anims == null) return; + for (int i = anims.size() - 1; i >= 0; --i) { + final Animator anim = anims.get(i); + mAnimExecutor.execute(anim::end); + } + } + private void edgeExtendWindow(TransitionInfo.Change change, Animation a, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction) { @@ -854,13 +866,19 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { }); }; va.addListener(new AnimatorListenerAdapter() { + private boolean mFinished = false; + @Override public void onAnimationEnd(Animator animation) { + if (mFinished) return; + mFinished = true; finisher.run(); } @Override public void onAnimationCancel(Animator animation) { + if (mFinished) return; + mFinished = true; finisher.run(); } }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java index 61e11e877b90..61e92f355dc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java @@ -107,7 +107,7 @@ public class LegacyTransitions { } @Override - public void onAnimationCancelled() throws RemoteException { + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { mCancelled = true; mApps = mWallpapers = mNonApps = null; checkApply(); diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt index d4298b8f29a0..49eca63a23ec 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt @@ -25,11 +25,13 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until +import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME import com.android.wm.shell.flicker.testapp.Components +import org.junit.Assert class SplitScreenHelper( instrumentation: Instrumentation, @@ -187,5 +189,23 @@ class SplitScreenHelper( SystemClock.sleep(GESTURE_STEP_MS) } } + + fun createShortcutOnHotseatIfNotExist( + taplInstrumentation: LauncherInstrumentation, + appName: String + ) { + taplInstrumentation.workspace + .deleteAppIcon(taplInstrumentation.workspace.getHotseatAppIcon(0)) + val allApps = taplInstrumentation.workspace.switchToAllApps() + allApps.freeze() + try { + val appIconSrc = allApps.getAppIcon(appName) + Assert.assertNotNull("Unable to find app icon", appIconSrc) + val appIconDest = appIconSrc.dragToHotseat(0) + Assert.assertNotNull("Unable to drag app icon on hotseat", appIconDest) + } finally { + allApps.unfreeze() + } + } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt new file mode 100644 index 000000000000..05c6e24ee89d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.splitscreen + +import android.platform.test.annotations.Presubmit +import android.view.WindowManagerPolicyConstants +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.layerIsVisibleAtEnd +import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test enter split screen by dragging app icon from taskbar. + * This test is only for large screen devices. + * + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromTaskbar` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class EnterSplitScreenByDragFromTaskbar( + testSpec: FlickerTestParameter +) : SplitScreenBase(testSpec) { + + @Before + fun before() { + Assume.assumeTrue(taplInstrumentation.isTablet) + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + eachRun { + taplInstrumentation.goHome() + SplitScreenHelper.createShortcutOnHotseatIfNotExist( + taplInstrumentation, secondaryApp.appName + ) + primaryApp.launchViaIntent(wmHelper) + } + } + transitions { + taplInstrumentation.launchedAppState.taskbar + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen( + secondaryApp.component.packageName, + primaryApp.component.packageName + ) + } + } + + @Presubmit + @Test + fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @Presubmit + @Test + fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp.component) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + testSpec.endRotation, primaryApp.component, false /* splitLeftTop */ + ) + + @Presubmit + @Test + fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible( + testSpec.endRotation, secondaryApp.component, true /* splitLeftTop */ + ) + + @Presubmit + @Test + fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = + testSpec.appWindowBecomesVisible(secondaryApp.component) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedNavigationModes = + listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index fcfcbfa091db..e7c5cb2183db 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -298,7 +298,7 @@ public class BackAnimationControllerTest { private void doMotionEvent(int actionDown, int coordinate) { mController.onMotionEvent( - MotionEvent.obtain(0, mEventTime, actionDown, coordinate, coordinate, 0), + coordinate, coordinate, actionDown, BackEvent.EDGE_LEFT); mEventTime += 10; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index abd55dd7d606..babc9707ef9c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -53,6 +53,7 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip.PipTransitionState; import org.junit.Before; import org.junit.Test; @@ -80,6 +81,7 @@ public class PipControllerTest extends ShellTestCase { @Mock private PipSnapAlgorithm mMockPipSnapAlgorithm; @Mock private PipMediaController mMockPipMediaController; @Mock private PipTaskOrganizer mMockPipTaskOrganizer; + @Mock private PipTransitionState mMockPipTransitionState; @Mock private PipTransitionController mMockPipTransitionController; @Mock private PipTouchHandler mMockPipTouchHandler; @Mock private PipMotionHelper mMockPipMotionHelper; @@ -104,8 +106,8 @@ public class PipControllerTest extends ShellTestCase { mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController, - mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, - mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState, + mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, mMockTaskStackListener, mPipParamsChangedForwarder, mMockOneHandedController, mMockExecutor); when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm); @@ -138,8 +140,8 @@ public class PipControllerTest extends ShellTestCase { mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController, - mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler, - mMockPipTransitionController, mMockWindowManagerShellWrapper, + mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState, + mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, mMockTaskStackListener, mPipParamsChangedForwarder, mMockOneHandedController, mMockExecutor)); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java index 630d0d2c827c..14d8ce4682c6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java @@ -249,7 +249,8 @@ public class StartingSurfaceDrawerTests { any() /* window */, any() /* attrs */, anyInt() /* viewVisibility */, anyInt() /* displayId */, any() /* requestedVisibility */, any() /* outInputChannel */, - any() /* outInsetsState */, any() /* outActiveControls */); + any() /* outInsetsState */, any() /* outActiveControls */, + any() /* outAttachedFrame */); TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo, mBinder, snapshot, mTestExecutor, () -> { diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl index b136d5bc4db3..2bdd5c8bc977 100644 --- a/media/java/android/media/projection/IMediaProjection.aidl +++ b/media/java/android/media/projection/IMediaProjection.aidl @@ -17,7 +17,7 @@ package android.media.projection; import android.media.projection.IMediaProjectionCallback; -import android.window.WindowContainerToken; +import android.os.IBinder; /** {@hide} */ interface IMediaProjection { @@ -31,14 +31,14 @@ interface IMediaProjection { void unregisterCallback(IMediaProjectionCallback callback); /** - * Returns the {@link android.window.WindowContainerToken} identifying the task to record, or - * {@code null} if there is none. + * Returns the {@link android.os.IBinder} identifying the task to record, or {@code null} if + * there is none. */ - WindowContainerToken getTaskRecordingWindowContainerToken(); + IBinder getLaunchCookie(); /** - * Updates the {@link android.window.WindowContainerToken} identifying the task to record, or - * {@code null} if there is none. + * Updates the {@link android.os.IBinder} identifying the task to record, or {@code null} if + * there is none. */ - void setTaskRecordingWindowContainerToken(in WindowContainerToken token); + void setLaunchCookie(in IBinder launchCookie); } diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index ba7bf3f5f5d3..ae44fc575f7c 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -25,13 +25,13 @@ import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.util.ArrayMap; import android.util.Log; import android.view.ContentRecordingSession; import android.view.Surface; -import android.window.WindowContainerToken; import java.util.Map; @@ -172,18 +172,16 @@ public final class MediaProjection { @NonNull VirtualDisplayConfig.Builder virtualDisplayConfig, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { try { - final WindowContainerToken taskWindowContainerToken = - mImpl.getTaskRecordingWindowContainerToken(); + final IBinder launchCookie = mImpl.getLaunchCookie(); Context windowContext = null; ContentRecordingSession session; - if (taskWindowContainerToken == null) { + if (launchCookie == null) { windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(), TYPE_APPLICATION, null /* options */); session = ContentRecordingSession.createDisplaySession( windowContext.getWindowContextToken()); } else { - session = ContentRecordingSession.createTaskSession( - taskWindowContainerToken.asBinder()); + session = ContentRecordingSession.createTaskSession(launchCookie); } virtualDisplayConfig.setWindowManagerMirroring(true); final DisplayManager dm = mContext.getSystemService(DisplayManager.class); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index 89e10c4b5e11..fc70ba40fb47 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -20,15 +20,19 @@ import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; +import android.os.Build; import android.os.ParcelUuid; import android.util.Log; +import androidx.annotation.ChecksSdkIntAtLeast; + import com.android.internal.annotations.VisibleForTesting; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * CsipDeviceManager manages the set of remote CSIP Bluetooth devices. @@ -126,32 +130,84 @@ public class CsipDeviceManager { } } + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) + private static boolean isAtLeastT() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; + } + // Group devices by groupId @VisibleForTesting void onGroupIdChanged(int groupId) { - int firstMatchedIndex = -1; - CachedBluetoothDevice mainDevice = null; + if (!isValidGroupId(groupId)) { + log("onGroupIdChanged: groupId is invalid"); + return; + } + log("onGroupIdChanged: mCachedDevices list =" + mCachedDevices.toString()); + final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); + final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager(); + final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile(); + final BluetoothDevice mainBluetoothDevice = (leAudioProfile != null && isAtLeastT()) ? + leAudioProfile.getConnectedGroupLeadDevice(groupId) : null; + CachedBluetoothDevice newMainDevice = + mainBluetoothDevice != null ? deviceManager.findDevice(mainBluetoothDevice) : null; + if (newMainDevice != null) { + final CachedBluetoothDevice finalNewMainDevice = newMainDevice; + final List<CachedBluetoothDevice> memberDevices = mCachedDevices.stream() + .filter(cachedDevice -> !cachedDevice.equals(finalNewMainDevice) + && cachedDevice.getGroupId() == groupId) + .collect(Collectors.toList()); + if (memberDevices == null || memberDevices.isEmpty()) { + log("onGroupIdChanged: There is no member device in list."); + return; + } + log("onGroupIdChanged: removed from UI device =" + memberDevices + + ", with groupId=" + groupId + " mainDevice= " + newMainDevice); + for (CachedBluetoothDevice memberDeviceItem : memberDevices) { + Set<CachedBluetoothDevice> memberSet = memberDeviceItem.getMemberDevice(); + if (!memberSet.isEmpty()) { + log("onGroupIdChanged: Transfer the member list into new main device."); + for (CachedBluetoothDevice memberListItem : memberSet) { + if (!memberListItem.equals(newMainDevice)) { + newMainDevice.addMemberDevice(memberListItem); + } + } + memberSet.clear(); + } - for (int i = mCachedDevices.size() - 1; i >= 0; i--) { - final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); - if (cachedDevice.getGroupId() != groupId) { - continue; + newMainDevice.addMemberDevice(memberDeviceItem); + mCachedDevices.remove(memberDeviceItem); + mBtManager.getEventManager().dispatchDeviceRemoved(memberDeviceItem); } - if (firstMatchedIndex == -1) { - // Found the first one - firstMatchedIndex = i; - mainDevice = cachedDevice; - continue; + if (!mCachedDevices.contains(newMainDevice)) { + mCachedDevices.add(newMainDevice); + mBtManager.getEventManager().dispatchDeviceAdded(newMainDevice); } + } else { + log("onGroupIdChanged: There is no main device from the LE profile."); + int firstMatchedIndex = -1; - log("onGroupIdChanged: removed from UI device =" + cachedDevice - + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex); + for (int i = mCachedDevices.size() - 1; i >= 0; i--) { + final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); + if (cachedDevice.getGroupId() != groupId) { + continue; + } + + if (firstMatchedIndex == -1) { + // Found the first one + firstMatchedIndex = i; + newMainDevice = cachedDevice; + continue; + } + + log("onGroupIdChanged: removed from UI device =" + cachedDevice + + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex); - mainDevice.addMemberDevice(cachedDevice); - mCachedDevices.remove(i); - mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); - break; + newMainDevice.addMemberDevice(cachedDevice); + mCachedDevices.remove(i); + mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); + break; + } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index 19df1e9c0730..0f57d8785de9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -21,6 +21,7 @@ import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; +import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCodecConfig; @@ -183,6 +184,37 @@ public class LeAudioProfile implements LocalBluetoothProfile { return mBluetoothAdapter.getActiveDevices(BluetoothProfile.LE_AUDIO); } + /** + * Get Lead device for the group. + * + * Lead device is the device that can be used as an active device in the system. + * Active devices points to the Audio Device for the Le Audio group. + * This method returns the Lead devices for the connected LE Audio + * group and this device should be used in the setActiveDevice() method by other parts + * of the system, which wants to set to active a particular Le Audio group. + * + * Note: getActiveDevice() returns the Lead device for the currently active LE Audio group. + * Note: When Lead device gets disconnected while Le Audio group is active and has more devices + * in the group, then Lead device will not change. If Lead device gets disconnected, for the + * Le Audio group which is not active, a new Lead device will be chosen + * + * @param groupId The group id. + * @return group lead device. + * + * @hide + */ + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) { + if (DEBUG) { + Log.d(TAG,"getConnectedGroupLeadDevice"); + } + if (mService == null) { + Log.e(TAG,"No service."); + return null; + } + return mService.getConnectedGroupLeadDevice(groupId); + } + @Override public boolean isEnabled(BluetoothDevice device) { if (mService == null || device == null) { diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java index e8cbab8197b2..a040e28169e8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java @@ -32,7 +32,7 @@ public abstract class MediaManager { private static final String TAG = "MediaManager"; protected final Collection<MediaDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>(); - protected final List<MediaDevice> mMediaDevices = new ArrayList<>(); + protected final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>(); protected Context mContext; protected Notification mNotification; diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index f05c1e2e76f2..fa87de2b1353 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -93,6 +93,7 @@ android_library { "SystemUISharedLib", "SystemUI-statsd", "SettingsLib", + "androidx.core_core-ktx", "androidx.viewpager2_viewpager2", "androidx.legacy_legacy-support-v4", "androidx.recyclerview_recyclerview", diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING index 3bd6d51eecc6..f9a9ef65cab6 100644 --- a/packages/SystemUI/TEST_MAPPING +++ b/packages/SystemUI/TEST_MAPPING @@ -52,6 +52,17 @@ ] }, { + "name": "SystemUIGoogleScreenshotTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { // Permission indicators "name": "CtsPermission4TestCases", "options": [ diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 5b47ae525919..fbe33565f11d 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -656,7 +656,7 @@ class ActivityLaunchAnimator( controller.onLaunchAnimationCancelled() } - override fun onAnimationCancelled() { + override fun onAnimationCancelled(isKeyguardOccluded: Boolean) { if (timedOut) { return } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java index 0f10589dfcf9..bd628ccb3c08 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java @@ -29,7 +29,7 @@ import java.lang.annotation.RetentionPolicy; /** * Interface that decides whether a touch on the phone was accidental. i.e. Pocket Dialing. * - * {@see com.android.systemui.classifier.FalsingManagerImpl} + * {@see com.android.systemui.classifier.BrightLineFalsingManager} */ @ProvidesInterface(version = FalsingManager.VERSION) public interface FalsingManager { diff --git a/packages/SystemUI/res-keyguard/values-ca/strings.xml b/packages/SystemUI/res-keyguard/values-ca/strings.xml index eba003fd6bd1..b1f5a3f9206f 100644 --- a/packages/SystemUI/res-keyguard/values-ca/strings.xml +++ b/packages/SystemUI/res-keyguard/values-ca/strings.xml @@ -84,7 +84,7 @@ <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"L\'administrador ha bloquejat el dispositiu"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositiu s\'ha bloquejat manualment"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"No s\'ha reconegut"</string> - <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Per utilitzar Desbloqueig facial, activa l\'accés a la càmera a Configuració"</string> + <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Desbloqueig facial necessita accés a la càmera"</string> <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Introdueix el PIN de la SIM. Et queda # intent; si no l\'encertes, contacta amb l\'operador per desbloquejar el dispositiu.}other{Introdueix el PIN de la SIM. Et queden # intents.}}"</string> <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{La targeta SIM s\'ha desactivat. Introdueix el codi PUK per continuar. Et queda # intent; si no l\'encertes, la SIM no es podrà tornar a fer servir. Contacta amb l\'operador per obtenir informació.}other{La targeta SIM s\'ha desactivat. Introdueix el codi PUK per continuar. Et queden # intents; si no l\'encertes, la SIM no es podrà tornar a fer servir. Contacta amb l\'operador per obtenir informació.}}"</string> <string name="clock_title_default" msgid="6342735240617459864">"Predeterminada"</string> diff --git a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml index 05039d8a363b..9b4df35f1754 100644 --- a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml +++ b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml @@ -55,10 +55,10 @@ <string name="kg_wrong_pin" msgid="4160978845968732624">"Wrong PIN"</string> <string name="kg_too_many_failed_attempts_countdown" msgid="2038195171919795529">"{count,plural, =1{Try again in # second.}other{Try again in # seconds.}}"</string> <string name="kg_sim_pin_instructions" msgid="1942424305184242951">"Enter SIM PIN."</string> - <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Enter SIM PIN for \'<xliff:g id="CARRIER">%1$s</xliff:g>\'."</string> + <string name="kg_sim_pin_instructions_multi" msgid="3639863309953109649">"Enter SIM PIN for \"<xliff:g id="CARRIER">%1$s</xliff:g>\"."</string> <string name="kg_sim_lock_esim_instructions" msgid="5577169988158738030">"<xliff:g id="PREVIOUS_MSG">%1$s</xliff:g> Disable eSIM to use device without mobile service."</string> - <string name="kg_puk_enter_puk_hint" msgid="3005288372875367017">"SIM is now disabled. Enter PUK code to continue. Contact operator for details."</string> - <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" is now disabled. Enter PUK code to continue. Contact operator for details."</string> + <string name="kg_puk_enter_puk_hint" msgid="3005288372875367017">"SIM is now disabled. Enter PUK code to continue. Contact carrier for details."</string> + <string name="kg_puk_enter_puk_hint_multi" msgid="4876780689904862943">"SIM \"<xliff:g id="CARRIER">%1$s</xliff:g>\" is now disabled. Enter PUK code to continue. Contact carrier for details."</string> <string name="kg_puk_enter_pin_hint" msgid="6028432138916150399">"Enter desired PIN code"</string> <string name="kg_enter_confirm_pin_hint" msgid="4261064020391799132">"Confirm desired PIN code"</string> <string name="kg_sim_unlock_progress_dialog_message" msgid="4251352015304070326">"Unlocking SIM card…"</string> @@ -67,9 +67,9 @@ <string name="kg_too_many_failed_pin_attempts_dialog_message" msgid="544687656831558971">"You have incorrectly typed your PIN <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string> <string name="kg_too_many_failed_password_attempts_dialog_message" msgid="190984061975729494">"You have incorrectly typed your password <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string> <string name="kg_too_many_failed_pattern_attempts_dialog_message" msgid="4252405904570284368">"You have incorrectly drawn your unlock pattern <xliff:g id="NUMBER_0">%1$d</xliff:g> times. \n\nTry again in <xliff:g id="NUMBER_1">%2$d</xliff:g> seconds."</string> - <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Incorrect SIM PIN code; you must now contact your operator to unlock your device."</string> - <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Incorrect SIM PIN code; you have # remaining attempt before you must contact your operator to unlock your device.}other{Incorrect SIM PIN code; you have # remaining attempts. }}"</string> - <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM is unusable. Contact your operator."</string> + <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Incorrect SIM PIN code you must now contact your carrier to unlock your device."</string> + <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Incorrect SIM PIN code, you have # remaining attempt before you must contact your carrier to unlock your device.}other{Incorrect SIM PIN code, you have # remaining attempts. }}"</string> + <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM is unusable. Contact your carrier."</string> <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Incorrect SIM PUK code; you have # remaining attempt before SIM becomes permanently unusable.}other{Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.}}"</string> <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN operation failed!"</string> <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK operation failed!"</string> @@ -85,8 +85,8 @@ <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string> <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"To use Face Unlock, turn on camera access in Settings"</string> - <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Enter SIM PIN. You have # remaining attempt before you must contact your operator to unlock your device.}other{Enter SIM PIN. You have # remaining attempts.}}"</string> - <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM is now disabled. Enter PUK code to continue. You have # remaining attempt before SIM becomes permanently unusable. Contact operator for details.}other{SIM is now disabled. Enter PUK code to continue. You have # remaining attempts before SIM becomes permanently unusable. Contact operator for details.}}"</string> + <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Enter SIM PIN. You have # remaining attempt before you must contact your carrier to unlock your device.}other{Enter SIM PIN. You have # remaining attempts.}}"</string> + <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM is now disabled. Enter PUK code to continue. You have # remaining attempt before SIM becomes permanently unusable. Contact carrier for details.}other{SIM is now disabled. Enter PUK code to continue. You have # remaining attempts before SIM becomes permanently unusable. Contact carrier for details.}}"</string> <string name="clock_title_default" msgid="6342735240617459864">"Default"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string> <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string> diff --git a/packages/SystemUI/res-keyguard/values-eu/strings.xml b/packages/SystemUI/res-keyguard/values-eu/strings.xml index 0932e93003f1..bf9491572cf8 100644 --- a/packages/SystemUI/res-keyguard/values-eu/strings.xml +++ b/packages/SystemUI/res-keyguard/values-eu/strings.xml @@ -84,7 +84,7 @@ <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administratzaileak blokeatu egin du gailua"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Eskuz blokeatu da gailua"</string> <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ez da ezagutu"</string> - <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Aurpegi bidez desblokeatzeko eginbidea erabiltzeko, eman kamera atzitzeko baimena ezarpenetan"</string> + <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"Aurpegi bidezko desblokeoak kamera atzitzeko baimena behar du"</string> <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Idatzi SIMaren PINa. # saiakera geratzen zaizu gailua desblokeatzeko operadorearekin harremanetan jarri behar izan aurretik.}other{Idatzi SIMaren PINa. # saiakera gelditzen zaizkizu.}}"</string> <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{Orain, SIMa desgaituta dago. Aurrera egiteko, idatzi PUK kodea. # saiakera geratzen zaizu SIMa betiko ez-erabilgarri geratu aurretik. Xehetasunak lortzeko, jarri operadorearekin harremanetan.}other{Orain, SIMa desgaituta dago. Aurrera egiteko, idatzi PUK kodea. # saiakera geratzen zaizkizu SIMa betiko ez-erabilgarri geratu aurretik. Xehetasunak lortzeko, jarri operadorearekin harremanetan.}}"</string> <string name="clock_title_default" msgid="6342735240617459864">"Lehenetsia"</string> diff --git a/packages/SystemUI/res-keyguard/values-te/strings.xml b/packages/SystemUI/res-keyguard/values-te/strings.xml index cecc645bcd8a..b6ae96d1dea7 100644 --- a/packages/SystemUI/res-keyguard/values-te/strings.xml +++ b/packages/SystemUI/res-keyguard/values-te/strings.xml @@ -22,7 +22,7 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="keyguard_enter_your_pin" msgid="5429932527814874032">"మీ పిన్ని నమోదు చేయండి"</string> <string name="keyguard_enter_your_pattern" msgid="351503370332324745">"మీ నమూనాను నమోదు చేయండి"</string> - <string name="keyguard_enter_your_password" msgid="7225626204122735501">"మీ పాస్వర్డ్ను నమోదు చేయండి"</string> + <string name="keyguard_enter_your_password" msgid="7225626204122735501">"మీ పాస్వర్డ్ను ఎంటర్ చేయండి"</string> <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"చెల్లని కార్డ్."</string> <string name="keyguard_charged" msgid="5478247181205188995">"ఛార్జ్ చేయబడింది"</string> <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • వైర్ లేకుండా ఛార్జ్ అవుతోంది"</string> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 77f1803523a8..acf3e4dcf02a 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -102,12 +102,13 @@ screen. --> <item name="half_opened_bouncer_height_ratio" type="dimen" format="float">0.0</item> - <!-- The actual amount of translation that is applied to the bouncer when it animates from one - side of the screen to the other in one-handed mode. Note that it will always translate from - the side of the screen to the other (it will "jump" closer to the destination while the - opacity is zero), but this controls how much motion will actually be applied to it while - animating. Larger values will cause it to move "faster" while fading out/in. --> - <dimen name="one_handed_bouncer_move_animation_translation">120dp</dimen> + <!-- The actual amount of translation that is applied to the security when it animates from one + side of the screen to the other in one-handed or user switcher mode. Note that it will + always translate from the side of the screen to the other (it will "jump" closer to the + destination while the opacity is zero), but this controls how much motion will actually be + applied to it while animating. Larger values will cause it to move "faster" while + fading out/in. --> + <dimen name="security_shift_animation_translation">120dp</dimen> <dimen name="bouncer_user_switcher_header_text_size">20sp</dimen> diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml index 3f56bafef134..efbdd1af3644 100644 --- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml +++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml @@ -20,5 +20,5 @@ style="@style/clock_subtitle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:format12Hour="EEE, MMM d" - android:format24Hour="EEE, MMM d"/> + android:format12Hour="@string/dream_date_complication_date_format" + android:format24Hour="@string/dream_date_complication_date_format"/> diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml index e066d38e446f..7a57293f58bd 100644 --- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml +++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml @@ -22,8 +22,8 @@ android:fontFamily="@font/clock" android:includeFontPadding="false" android:textColor="@android:color/white" - android:format12Hour="h:mm" - android:format24Hour="kk:mm" + android:format12Hour="@string/dream_time_complication_12_hr_time_format" + android:format24Hour="@string/dream_time_complication_24_hr_time_format" android:shadowColor="@color/keyguard_shadow_color" android:shadowRadius="?attr/shadowRadius" android:textSize="@dimen/dream_overlay_complication_clock_time_text_size"/> diff --git a/packages/SystemUI/res/layout/hybrid_conversation_notification.xml b/packages/SystemUI/res/layout/hybrid_conversation_notification.xml index 43b16618d615..a313833e2a66 100644 --- a/packages/SystemUI/res/layout/hybrid_conversation_notification.xml +++ b/packages/SystemUI/res/layout/hybrid_conversation_notification.xml @@ -57,7 +57,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" - style="?attr/hybridNotificationTextStyle" + android:paddingEnd="4dp" + style="@*android:style/Widget.DeviceDefault.Notification.Text" /> <TextView @@ -65,6 +66,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:singleLine="true" - style="?attr/hybridNotificationTextStyle" + android:paddingEnd="4dp" + style="@*android:style/Widget.DeviceDefault.Notification.Text" /> </com.android.systemui.statusbar.notification.row.HybridConversationNotificationView> diff --git a/packages/SystemUI/res/layout/hybrid_notification.xml b/packages/SystemUI/res/layout/hybrid_notification.xml index e8d77511e53c..9ea7be50adec 100644 --- a/packages/SystemUI/res/layout/hybrid_notification.xml +++ b/packages/SystemUI/res/layout/hybrid_notification.xml @@ -20,19 +20,22 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="bottom|start" - style="?attr/hybridNotificationStyle"> + android:paddingStart="@*android:dimen/notification_content_margin_start" + android:paddingEnd="12dp"> <TextView android:id="@+id/notification_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" - style="?attr/hybridNotificationTitleStyle" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" + android:paddingEnd="4dp" /> <TextView android:id="@+id/notification_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:singleLine="true" - style="?attr/hybridNotificationTextStyle" + android:paddingEnd="4dp" + style="@*android:style/Widget.DeviceDefault.Notification.Text" /> </com.android.systemui.statusbar.notification.row.HybridNotificationView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml index 4d24140abbf4..d88680669fe0 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip.xml @@ -31,6 +31,8 @@ android:padding="@dimen/media_ttt_chip_outer_padding" android:background="@drawable/media_ttt_chip_background" android:layout_marginTop="20dp" + android:layout_marginStart="@dimen/notification_side_paddings" + android:layout_marginEnd="@dimen/notification_side_paddings" android:clipToPadding="false" android:gravity="center_vertical" android:alpha="0.0" @@ -46,8 +48,9 @@ <TextView android:id="@+id/text" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_weight="1" android:textSize="@dimen/media_ttt_text_size" android:textColor="?android:attr/textColorPrimary" android:alpha="0.0" diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml index 5e8b892018eb..2b3d11b0e191 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml @@ -14,20 +14,19 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<!-- TODO(b/203800646): layout_marginTop doesn't seem to work on some large screens. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/media_ttt_receiver_chip" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@drawable/media_ttt_chip_background_receiver" > <com.android.internal.widget.CachingIconView android:id="@+id/app_icon" android:layout_width="@dimen/media_ttt_icon_size_receiver" android:layout_height="@dimen/media_ttt_icon_size_receiver" - android:layout_gravity="center" + android:layout_gravity="center|bottom" + android:alpha="0.0" /> </FrameLayout> diff --git a/packages/SystemUI/res/values-h800dp/dimens.xml b/packages/SystemUI/res/values-h800dp/dimens.xml index e6af6f46ae69..94fe20955ce6 100644 --- a/packages/SystemUI/res/values-h800dp/dimens.xml +++ b/packages/SystemUI/res/values-h800dp/dimens.xml @@ -15,9 +15,6 @@ --> <resources> - <!-- Minimum margin between clock and top of screen or ambient indication --> - <dimen name="keyguard_clock_top_margin">26dp</dimen> - <!-- Large clock maximum font size (dp is intentional, to prevent any further scaling) --> <dimen name="large_clock_text_size">200dp</dimen> diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index 70a72ad23d7e..9a71995383ac 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -108,12 +108,6 @@ <attr name="android:layout" /> </declare-styleable> - <declare-styleable name="HybridNotificationTheme"> - <attr name="hybridNotificationStyle" format="reference" /> - <attr name="hybridNotificationTitleStyle" format="reference" /> - <attr name="hybridNotificationTextStyle" format="reference" /> - </declare-styleable> - <declare-styleable name="PluginInflateContainer"> <attr name="viewType" format="string" /> </declare-styleable> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 4d365412828e..36a2d6469ec8 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1060,6 +1060,7 @@ <!-- Since the generic icon isn't circular, we need to scale it down so it still fits within the circular chip. --> <dimen name="media_ttt_generic_icon_size_receiver">70dp</dimen> + <dimen name="media_ttt_receiver_vert_translation">20dp</dimen> <!-- Window magnification --> <dimen name="magnification_border_drag_size">35dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d88428f972cb..343ec4f67964 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2581,4 +2581,12 @@ <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is unknown --> <string name="bt_le_audio_broadcast_dialog_unknown_name">Unknown</string> + <!-- Date format for the Dream Date Complication [CHAR LIMIT=NONE] --> + <string name="dream_date_complication_date_format">EEE, MMM d</string> + + <!-- Time format for the Dream Time Complication for 12-hour time format [CHAR LIMIT=NONE] --> + <string name="dream_time_complication_12_hr_time_format">h:mm</string> + + <!-- Time format for the Dream Time Complication for 24-hour time format [CHAR LIMIT=NONE] --> + <string name="dream_time_complication_24_hr_time_format">kk:mm</string> </resources> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 8b2481cff2a9..f954bc9bf17c 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -17,30 +17,6 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> - <!-- HybridNotification themes and styles --> - - <style name="HybridNotification"> - <item name="hybridNotificationStyle">@style/hybrid_notification</item> - <item name="hybridNotificationTitleStyle">@style/hybrid_notification_title</item> - <item name="hybridNotificationTextStyle">@style/hybrid_notification_text</item> - </style> - - <style name="hybrid_notification"> - <item name="android:paddingStart">@*android:dimen/notification_content_margin_start</item> - <item name="android:paddingEnd">12dp</item> - </style> - - <style name="hybrid_notification_title"> - <item name="android:paddingEnd">4dp</item> - <item name="android:textAppearance">@*android:style/TextAppearance.DeviceDefault.Notification.Title</item> - </style> - - <style name="hybrid_notification_text" - parent="@*android:style/Widget.DeviceDefault.Notification.Text"> - <item name="android:paddingEnd">4dp</item> - </style> - - <style name="TextAppearance.StatusBar.Clock" parent="@*android:style/TextAppearance.StatusBar.Icon"> <item name="android:textSize">@dimen/status_bar_clock_size</item> <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item> diff --git a/packages/SystemUI/screenshot/Android.bp b/packages/SystemUI/screenshot/Android.bp new file mode 100644 index 000000000000..a79fd9040db3 --- /dev/null +++ b/packages/SystemUI/screenshot/Android.bp @@ -0,0 +1,48 @@ +// Copyright (C) 2022 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + name: "SystemUIScreenshotLib", + manifest: "AndroidManifest.xml", + + srcs: [ + // All files in this library should be in Kotlin besides some exceptions. + "src/**/*.kt", + + // This file was forked from google3, so exceptionally it can be in Java. + "src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java", + ], + + resource_dirs: [ + "res", + ], + + static_libs: [ + "SystemUI-core", + "androidx.test.espresso.core", + "androidx.appcompat_appcompat", + "platform-screenshot-diff-core", + ], + + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SystemUI/screenshot/AndroidManifest.xml b/packages/SystemUI/screenshot/AndroidManifest.xml new file mode 100644 index 000000000000..3b703be34e5d --- /dev/null +++ b/packages/SystemUI/screenshot/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.systemui.testing.screenshot"> + <application> + <activity + android:name="com.android.systemui.testing.screenshot.ScreenshotActivity" + android:exported="true" + android:theme="@style/Theme.SystemUI.Screenshot" /> + </application> + + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> +</manifest> diff --git a/packages/SystemUI/screenshot/res/values/themes.xml b/packages/SystemUI/screenshot/res/values/themes.xml new file mode 100644 index 000000000000..40e50bbb6bbf --- /dev/null +++ b/packages/SystemUI/screenshot/res/values/themes.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 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. +--> +<resources> + <style name="Theme.SystemUI.Screenshot" parent="Theme.SystemUI"> + <item name="android:windowActionBar">false</item> + <item name="android:windowNoTitle">true</item> + + <!-- Make sure that device specific cutouts don't impact the outcome of screenshot tests --> + <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java new file mode 100644 index 000000000000..96ec4c543474 --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2022 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.testing.screenshot; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import android.app.UiAutomation; +import android.content.Context; +import android.provider.Settings; +import android.util.Log; + +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; +import androidx.test.espresso.Espresso; +import androidx.test.espresso.IdlingRegistry; +import androidx.test.espresso.IdlingResource; + +import org.json.JSONObject; +import org.junit.function.ThrowingRunnable; + +import java.util.HashMap; +import java.util.Map; + +/* + * Note: This file was forked from + * google3/third_party/java_src/android_libs/material_components/screenshot_tests/java/android/ + * support/design/scuba/color/DynamicColorsTestUtils.java. + */ + +/** Utility that helps change the dynamic system colors for testing. */ +@RequiresApi(32) +public class DynamicColorsTestUtils { + + private static final String TAG = DynamicColorsTestUtils.class.getSimpleName(); + + private static final String THEME_CUSTOMIZATION_KEY = "theme_customization_overlay_packages"; + private static final String THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY = + "android.theme.customization.system_palette"; + + private static final int ORANGE_SYSTEM_SEED_COLOR = 0xA66800; + private static final int ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR = -8235756; + + private DynamicColorsTestUtils() { + } + + /** + * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on an orange + * seed color, and then wait for the change to propagate to the app by comparing + * android.R.color.system_accent1_600 to the expected orange value. + */ + public static void updateSystemColorsToOrange() { + updateSystemColors(ORANGE_SYSTEM_SEED_COLOR, ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR); + } + + /** + * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided + * {@code seedColor}, and then wait for the change to propagate to the app by comparing + * android.R.color.system_accent1_600 to {@code expectedSystemAccent1600}. + */ + public static void updateSystemColors( + @ColorInt int seedColor, @ColorInt int expectedSystemAccent1600) { + Context context = getInstrumentation().getTargetContext(); + + int actualSystemAccent1600 = + ContextCompat.getColor(context, android.R.color.system_accent1_600); + + if (expectedSystemAccent1600 == actualSystemAccent1600) { + String expectedColorString = Integer.toHexString(expectedSystemAccent1600); + Log.d( + TAG, + "Skipped updating system colors since system_accent1_600 is already equal to " + + "expected: " + + expectedColorString); + return; + } + + updateSystemColors(seedColor); + } + + /** + * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided + * {@code seedColor}, and then wait for the change to propagate to the app by checking + * android.R.color.system_accent1_600 for any change. + */ + public static void updateSystemColors(@ColorInt int seedColor) { + Context context = getInstrumentation().getTargetContext(); + + // Initialize system color idling resource with original system_accent1_600 value. + ColorChangeIdlingResource systemColorIdlingResource = + new ColorChangeIdlingResource(context, android.R.color.system_accent1_600); + + // Update system theme color setting to trigger fabricated resource overlay. + runWithShellPermissionIdentity( + () -> + Settings.Secure.putString( + context.getContentResolver(), + THEME_CUSTOMIZATION_KEY, + buildThemeCustomizationString(seedColor))); + + // Wait for system color update to propagate to app. + IdlingRegistry idlingRegistry = IdlingRegistry.getInstance(); + idlingRegistry.register(systemColorIdlingResource); + Espresso.onIdle(); + idlingRegistry.unregister(systemColorIdlingResource); + + Log.d(TAG, + Settings.Secure.getString(context.getContentResolver(), THEME_CUSTOMIZATION_KEY)); + } + + private static String buildThemeCustomizationString(@ColorInt int seedColor) { + String seedColorHex = Integer.toHexString(seedColor); + Map<String, String> themeCustomizationMap = new HashMap<>(); + themeCustomizationMap.put(THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY, seedColorHex); + return new JSONObject(themeCustomizationMap).toString(); + } + + private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable runnable) { + UiAutomation uiAutomation = getInstrumentation().getUiAutomation(); + uiAutomation.adoptShellPermissionIdentity(); + try { + runnable.run(); + } catch (Throwable e) { + throw new RuntimeException(e); + } finally { + uiAutomation.dropShellPermissionIdentity(); + } + } + + private static class ColorChangeIdlingResource implements IdlingResource { + + private final Context mContext; + private final int mColorResId; + private final int mInitialColorInt; + + private ResourceCallback mResourceCallback; + private boolean mIdleNow; + + ColorChangeIdlingResource(Context context, @ColorRes int colorResId) { + this.mContext = context; + this.mColorResId = colorResId; + this.mInitialColorInt = ContextCompat.getColor(context, colorResId); + } + + @Override + public String getName() { + return ColorChangeIdlingResource.class.getName(); + } + + @Override + public boolean isIdleNow() { + if (mIdleNow) { + return true; + } + + int currentColorInt = ContextCompat.getColor(mContext, mColorResId); + + String initialColorString = Integer.toHexString(mInitialColorInt); + String currentColorString = Integer.toHexString(currentColorInt); + Log.d(TAG, String.format("Initial=%s, Current=%s", initialColorString, + currentColorString)); + + mIdleNow = currentColorInt != mInitialColorInt; + Log.d(TAG, String.format("idleNow=%b", mIdleNow)); + + if (mIdleNow) { + mResourceCallback.onTransitionToIdle(); + } + return mIdleNow; + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { + this.mResourceCallback = resourceCallback; + } + } +} diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt new file mode 100644 index 000000000000..2a55a80eb7f4 --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 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.testing.screenshot + +import androidx.activity.ComponentActivity + +/** The Activity that is launched and whose content is set for screenshot tests. */ +class ScreenshotActivity : ComponentActivity() diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt new file mode 100644 index 000000000000..363ce10fa36c --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2022 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.testing.screenshot + +import android.app.UiModeManager +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.os.UserHandle +import android.view.Display +import android.view.View +import android.view.WindowManagerGlobal +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import platform.test.screenshot.GoldenImagePathManager +import platform.test.screenshot.PathConfig +import platform.test.screenshot.PathElementNoContext +import platform.test.screenshot.ScreenshotTestRule +import platform.test.screenshot.matchers.PixelPerfectMatcher + +/** + * A base rule for screenshot diff tests. + * + * This rules takes care of setting up the activity according to [testSpec] by: + * - emulating the display size and density. + * - setting the dark/light mode. + * - setting the system (Material You) colors to a fixed value. + * + * @see ComposeScreenshotTestRule + * @see ViewScreenshotTestRule + */ +class ScreenshotTestRule(private val testSpec: ScreenshotTestSpec) : TestRule { + private var currentDisplay: DisplaySpec? = null + private var currentGoldenIdentifier: String? = null + + private val pathConfig = + PathConfig( + PathElementNoContext("model", isDir = true) { + currentDisplay?.name ?: error("currentDisplay is null") + }, + ) + private val defaultMatcher = PixelPerfectMatcher() + + private val screenshotRule = + ScreenshotTestRule( + SystemUIGoldenImagePathManager( + pathConfig, + currentGoldenIdentifier = { + currentGoldenIdentifier ?: error("currentGoldenIdentifier is null") + }, + ) + ) + + override fun apply(base: Statement, description: Description): Statement { + // The statement which call beforeTest() before running the test and afterTest() afterwards. + val statement = + object : Statement() { + override fun evaluate() { + try { + beforeTest() + base.evaluate() + } finally { + afterTest() + } + } + } + + return screenshotRule.apply(statement, description) + } + + private fun beforeTest() { + // Update the system colors to a fixed color, so that tests don't depend on the host device + // extracted colors. Note that we don't restore the default device colors at the end of the + // test because changing the colors (and waiting for them to be applied) is costly and makes + // the screenshot tests noticeably slower. + DynamicColorsTestUtils.updateSystemColorsToOrange() + + // Emulate the display size and density. + val display = testSpec.display + val density = display.densityDpi + val wm = WindowManagerGlobal.getWindowManagerService() + val (width, height) = getEmulatedDisplaySize() + wm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, density, UserHandle.myUserId()) + wm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, width, height) + + // Force the dark/light theme. + val uiModeManager = + InstrumentationRegistry.getInstrumentation() + .targetContext + .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiModeManager.setApplicationNightMode( + if (testSpec.isDarkTheme) { + UiModeManager.MODE_NIGHT_YES + } else { + UiModeManager.MODE_NIGHT_NO + } + ) + } + + private fun afterTest() { + // Reset the density and display size. + val wm = WindowManagerGlobal.getWindowManagerService() + wm.clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, UserHandle.myUserId()) + wm.clearForcedDisplaySize(Display.DEFAULT_DISPLAY) + + // Reset the dark/light theme. + val uiModeManager = + InstrumentationRegistry.getInstrumentation() + .targetContext + .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO) + } + + /** + * Compare the content of [view] with the golden image identified by [goldenIdentifier] in the + * context of [testSpec]. + */ + fun screenshotTest(goldenIdentifier: String, view: View) { + val bitmap = drawIntoBitmap(view) + + // Compare bitmap against golden asset. + val isDarkTheme = testSpec.isDarkTheme + val isLandscape = testSpec.isLandscape + val identifierWithSpec = buildString { + append(goldenIdentifier) + if (isDarkTheme) append("_dark") + if (isLandscape) append("_landscape") + } + + // TODO(b/230832101): Provide a way to pass a PathConfig and override the file name on + // device to assertBitmapAgainstGolden instead? + currentDisplay = testSpec.display + currentGoldenIdentifier = goldenIdentifier + screenshotRule.assertBitmapAgainstGolden(bitmap, identifierWithSpec, defaultMatcher) + currentDisplay = null + currentGoldenIdentifier = goldenIdentifier + } + + /** Draw [view] into a [Bitmap]. */ + private fun drawIntoBitmap(view: View): Bitmap { + val bitmap = + Bitmap.createBitmap( + view.measuredWidth, + view.measuredHeight, + Bitmap.Config.ARGB_8888, + ) + val canvas = Canvas(bitmap) + view.draw(canvas) + return bitmap + } + + /** Get the emulated display size for [testSpec]. */ + private fun getEmulatedDisplaySize(): Pair<Int, Int> { + val display = testSpec.display + val isPortraitNaturalPosition = display.width < display.height + return if (testSpec.isLandscape) { + if (isPortraitNaturalPosition) { + display.height to display.width + } else { + display.width to display.height + } + } else { + if (isPortraitNaturalPosition) { + display.width to display.height + } else { + display.height to display.width + } + } + } +} + +private class SystemUIGoldenImagePathManager( + pathConfig: PathConfig, + private val currentGoldenIdentifier: () -> String, +) : + GoldenImagePathManager( + appContext = InstrumentationRegistry.getInstrumentation().context, + deviceLocalPath = + InstrumentationRegistry.getInstrumentation() + .targetContext + .filesDir + .absolutePath + .toString() + "/sysui_screenshots", + pathConfig = pathConfig, + ) { + // This string is appended to all actual/expected screenshots on the device. We append the + // golden identifier so that our pull_golden.py scripts can map a screenshot on device to its + // asset (and automatically update it, if necessary). + override fun toString() = currentGoldenIdentifier() +} diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt new file mode 100644 index 000000000000..7fc624554738 --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 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.testing.screenshot + +/** The specification of a device display to be used in a screenshot test. */ +data class DisplaySpec( + val name: String, + val width: Int, + val height: Int, + val densityDpi: Int, +) + +/** The specification of a screenshot diff test. */ +class ScreenshotTestSpec( + val display: DisplaySpec, + val isDarkTheme: Boolean = false, + val isLandscape: Boolean = false, +) { + companion object { + /** + * Return a list of [ScreenshotTestSpec] for each of the [displays]. + * + * If [isDarkTheme] is null, this will create a spec for both light and dark themes, for + * each of the orientation. + * + * If [isLandscape] is null, this will create a spec for both portrait and landscape, for + * each of the light/dark themes. + */ + fun forDisplays( + vararg displays: DisplaySpec, + isDarkTheme: Boolean? = null, + isLandscape: Boolean? = null, + ): List<ScreenshotTestSpec> { + return displays.flatMap { display -> + buildList { + fun addDisplay(isLandscape: Boolean) { + if (isDarkTheme != true) { + add(ScreenshotTestSpec(display, isDarkTheme = false, isLandscape)) + } + + if (isDarkTheme != false) { + add(ScreenshotTestSpec(display, isDarkTheme = true, isLandscape)) + } + } + + if (isLandscape != true) { + addDisplay(isLandscape = false) + } + + if (isLandscape != false) { + addDisplay(isLandscape = true) + } + } + } + } + } + + override fun toString(): String = buildString { + // This string is appended to PNGs stored in the device, so let's keep it simple. + append(display.name) + if (isDarkTheme) append("_dark") + if (isLandscape) append("_landscape") + } +} 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 new file mode 100644 index 000000000000..35812e39677d --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt @@ -0,0 +1,80 @@ +package com.android.systemui.testing.screenshot + +import android.app.Activity +import android.app.Dialog +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams +import androidx.test.ext.junit.rules.ActivityScenarioRule +import org.junit.Assert.assertEquals +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** A rule for View screenshot diff tests. */ +class ViewScreenshotTestRule(testSpec: ScreenshotTestSpec) : TestRule { + private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java) + private val screenshotRule = ScreenshotTestRule(testSpec) + + private val delegate = RuleChain.outerRule(screenshotRule).around(activityRule) + + override fun apply(base: Statement, description: Description): Statement { + return delegate.apply(base, description) + } + + /** + * Compare the content of the view provided by [viewProvider] with the golden image identified + * by [goldenIdentifier] in the context of [testSpec]. + */ + fun screenshotTest( + goldenIdentifier: String, + layoutParams: LayoutParams = + LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT), + viewProvider: (Activity) -> View, + ) { + activityRule.scenario.onActivity { activity -> + // Make sure that the activity draws full screen and fits the whole display instead of + // the system bars. + activity.window.setDecorFitsSystemWindows(false) + activity.setContentView(viewProvider(activity), layoutParams) + } + + // We call onActivity again because it will make sure that our Activity is done measuring, + // laying out and drawing its content (that we set in the previous onActivity lambda). + activityRule.scenario.onActivity { activity -> + // Check that the content is what we expected. + val content = activity.requireViewById<ViewGroup>(android.R.id.content) + assertEquals(1, content.childCount) + screenshotRule.screenshotTest(goldenIdentifier, content.getChildAt(0)) + } + } + + /** + * Compare the content of the dialog provided by [dialogProvider] with the golden image + * identified by [goldenIdentifier] in the context of [testSpec]. + */ + fun dialogScreenshotTest( + goldenIdentifier: String, + dialogProvider: (Activity) -> Dialog, + ) { + var dialog: Dialog? = null + activityRule.scenario.onActivity { activity -> + // Make sure that the dialog draws full screen and fits the whole display instead of the + // system bars. + dialog = + dialogProvider(activity).apply { + window.setDecorFitsSystemWindows(false) + show() + } + } + + // We call onActivity again because it will make sure that our Dialog is done measuring, + // laying out and drawing its content (that we set in the previous onActivity lambda). + activityRule.scenario.onActivity { + // Check that the content is what we expected. + val dialog = dialog ?: error("dialog is null") + screenshotRule.screenshotTest(goldenIdentifier, dialog.window.decorView) + } + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java index 88fe03465405..203b236fcdd6 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java @@ -80,7 +80,8 @@ public class PipSurfaceTransactionHelper { public PictureInPictureSurfaceTransaction scaleAndCrop( SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets) { + Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets, + float progress) { mTmpSourceRectF.set(sourceBounds); mTmpDestinationRect.set(sourceBounds); mTmpDestinationRect.inset(insets); @@ -93,9 +94,13 @@ public class PipSurfaceTransactionHelper { : (float) destinationBounds.height() / sourceBounds.height(); } else { // scale by sourceRectHint if it's not edge-to-edge - scale = sourceRectHint.width() <= sourceRectHint.height() + final float endScale = sourceRectHint.width() <= sourceRectHint.height() ? (float) destinationBounds.width() / sourceRectHint.width() : (float) destinationBounds.height() / sourceRectHint.height(); + final float startScale = sourceRectHint.width() <= sourceRectHint.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + scale = (1 - progress) * startScale + progress * endScale; } final float left = destinationBounds.left - (insets.left + sourceBounds.left) * scale; final float top = destinationBounds.top - (insets.top + sourceBounds.top) * scale; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java index 76a09b38036c..9265f07ad284 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java @@ -105,7 +105,7 @@ public class RemoteAnimationAdapterCompat { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { remoteAnimationAdapter.onAnimationCancelled(); } }; @@ -114,6 +114,8 @@ public class RemoteAnimationAdapterCompat { private static IRemoteTransition.Stub wrapRemoteTransition( final RemoteAnimationRunnerCompat remoteAnimationAdapter) { return new IRemoteTransition.Stub() { + final ArrayMap<IBinder, Runnable> mFinishRunnables = new ArrayMap<>(); + @Override public void startAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, @@ -218,9 +220,9 @@ public class RemoteAnimationAdapterCompat { for (int i = info.getChanges().size() - 1; i >= 0; --i) { info.getChanges().get(i).getLeash().release(); } - for (int i = leashMap.size() - 1; i >= 0; --i) { - leashMap.valueAt(i).release(); - } + // Don't release here since launcher might still be using them. Instead + // let launcher release them (eg. via RemoteAnimationTargets) + leashMap.clear(); try { finishCallback.onTransitionFinished(null /* wct */, finishTransaction); } catch (RemoteException e) { @@ -229,19 +231,32 @@ public class RemoteAnimationAdapterCompat { } } }; + synchronized (mFinishRunnables) { + mFinishRunnables.put(token, animationFinishedCallback); + } // TODO(bc-unlcok): Pass correct transit type. - remoteAnimationAdapter.onAnimationStart( - TRANSIT_OLD_NONE, - appsCompat, wallpapersCompat, nonAppsCompat, - animationFinishedCallback); + remoteAnimationAdapter.onAnimationStart(TRANSIT_OLD_NONE, + appsCompat, wallpapersCompat, nonAppsCompat, () -> { + synchronized (mFinishRunnables) { + if (mFinishRunnables.remove(token) == null) return; + } + animationFinishedCallback.run(); + }); } @Override public void mergeAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishCallback) { - // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, ignore - // any incoming merges. + // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, adapt + // to legacy cancel. + final Runnable finishRunnable; + synchronized (mFinishRunnables) { + finishRunnable = mFinishRunnables.remove(mergeTarget); + } + if (finishRunnable == null) return; + remoteAnimationAdapter.onAnimationCancelled(); + finishRunnable.run(); } }; } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java index 4ce110bf7c47..249133a9a63b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java @@ -141,8 +141,10 @@ public class RemoteAnimationTargetCompat { final int mode = change.getMode(); t.reparent(leash, info.getRootLeash()); - t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x, - change.getStartAbsBounds().top - info.getRootOffset().y); + final Rect absBounds = + (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds(); + t.setPosition(leash, absBounds.left - info.getRootOffset().x, + absBounds.top - info.getRootOffset().y); // Put all the OPEN/SHOW on top if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java index 83780c8a176d..29e912fdab32 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java @@ -59,6 +59,7 @@ public class KeyguardPasswordViewController private final boolean mShowImeAtScreenOn; private EditText mPasswordEntry; private ImageView mSwitchImeButton; + private boolean mPaused; private final OnEditorActionListener mOnEditorActionListener = (v, actionId, event) -> { // Check if this was the result of hitting the enter key @@ -202,6 +203,7 @@ public class KeyguardPasswordViewController @Override public void onResume(int reason) { super.onResume(reason); + mPaused = false; if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) { showInput(); } @@ -223,6 +225,11 @@ public class KeyguardPasswordViewController @Override public void onPause() { + if (mPaused) { + return; + } + mPaused = true; + if (!mPasswordEntry.isVisibleToUser()) { // Reset all states directly and then hide IME when the screen turned off. super.onPause(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 65c415b5272b..8fb622a8c55e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -94,6 +94,7 @@ import com.android.systemui.util.settings.GlobalSettings; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; public class KeyguardSecurityContainer extends FrameLayout { static final int USER_TYPE_PRIMARY = 1; @@ -128,12 +129,12 @@ public class KeyguardSecurityContainer extends FrameLayout { private static final long IME_DISAPPEAR_DURATION_MS = 125; - // The duration of the animation to switch bouncer sides. - private static final long BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS = 500; + // The duration of the animation to switch security sides. + private static final long SECURITY_SHIFT_ANIMATION_DURATION_MS = 500; - // How much of the switch sides animation should be dedicated to fading the bouncer out. The + // How much of the switch sides animation should be dedicated to fading the security out. The // remainder will fade it back in again. - private static final float BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION = 0.2f; + private static final float SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION = 0.2f; @VisibleForTesting KeyguardSecurityViewFlipper mSecurityViewFlipper; @@ -375,10 +376,23 @@ public class KeyguardSecurityContainer extends FrameLayout { mViewMode.updatePositionByTouchX(x); } - /** Returns whether the inner SecurityViewFlipper is left-aligned when in one-handed mode. */ - public boolean isOneHandedModeLeftAligned() { - return mCurrentMode == MODE_ONE_HANDED - && ((OneHandedViewMode) mViewMode).isLeftAligned(); + public boolean isSidedSecurityMode() { + return mViewMode instanceof SidedSecurityMode; + } + + /** Returns whether the inner SecurityViewFlipper is left-aligned when in sided mode. */ + public boolean isSecurityLeftAligned() { + return mViewMode instanceof SidedSecurityMode + && ((SidedSecurityMode) mViewMode).isLeftAligned(); + } + + /** + * Returns whether the touch happened on the other side of security (like bouncer) when in + * sided mode. + */ + public boolean isTouchOnTheOtherSideOfSecurity(MotionEvent ev) { + return mViewMode instanceof SidedSecurityMode + && ((SidedSecurityMode) mViewMode).isTouchOnTheOtherSideOfSecurity(ev); } public void onPause() { @@ -437,12 +451,15 @@ public class KeyguardSecurityContainer extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); - mDoubleTapDetector.onTouchEvent(event); boolean result = mMotionEventListeners.stream() .anyMatch(listener -> listener.onTouchEvent(event)) || super.onTouchEvent(event); + // double tap detector should be called after listeners handle touches as listeners are + // helping with ignoring falsing. Otherwise falsing will be activated for some double taps + mDoubleTapDetector.onTouchEvent(event); + switch (action) { case MotionEvent.ACTION_MOVE: mVelocityTracker.addMovement(event); @@ -487,11 +504,16 @@ public class KeyguardSecurityContainer extends FrameLayout { private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDoubleTap(MotionEvent e) { - if (!mIsDragging) { - mViewMode.handleDoubleTap(e); - } + return handleDoubleTap(e); + } + } + + @VisibleForTesting boolean handleDoubleTap(MotionEvent e) { + if (!mIsDragging) { + mViewMode.handleDoubleTap(e); return true; } + return false; } void addMotionEventListener(Gefingerpoken listener) { @@ -771,6 +793,195 @@ public class KeyguardSecurityContainer extends FrameLayout { } /** + * Base class for modes which support having on left/right side of the screen, used for large + * screen devices + */ + abstract static class SidedSecurityMode implements ViewMode { + @Nullable private ValueAnimator mRunningSecurityShiftAnimator; + private KeyguardSecurityViewFlipper mViewFlipper; + private ViewGroup mView; + private GlobalSettings mGlobalSettings; + private int mDefaultSideSetting; + + public void init(ViewGroup v, KeyguardSecurityViewFlipper viewFlipper, + GlobalSettings globalSettings, boolean leftAlignedByDefault) { + mView = v; + mViewFlipper = viewFlipper; + mGlobalSettings = globalSettings; + mDefaultSideSetting = + leftAlignedByDefault ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT + : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT; + } + + /** + * Determine if a double tap on this view is on the other side. If so, will animate + * positions and record the preference to always show on this side. + */ + @Override + public void handleDoubleTap(MotionEvent event) { + boolean currentlyLeftAligned = isLeftAligned(); + // Did the tap hit the "other" side of the bouncer? + if (isTouchOnTheOtherSideOfSecurity(event, currentlyLeftAligned)) { + boolean willBeLeftAligned = !currentlyLeftAligned; + updateSideSetting(willBeLeftAligned); + + int keyguardState = willBeLeftAligned + ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_LEFT + : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_RIGHT; + SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, keyguardState); + + updateSecurityViewLocation(willBeLeftAligned, /* animate= */ true); + } + } + + private boolean isTouchOnTheOtherSideOfSecurity(MotionEvent ev, boolean leftAligned) { + float x = ev.getX(); + return (leftAligned && (x > mView.getWidth() / 2f)) + || (!leftAligned && (x < mView.getWidth() / 2f)); + } + + public boolean isTouchOnTheOtherSideOfSecurity(MotionEvent ev) { + return isTouchOnTheOtherSideOfSecurity(ev, isLeftAligned()); + } + + protected abstract void updateSecurityViewLocation(boolean leftAlign, boolean animate); + + protected void translateSecurityViewLocation(boolean leftAlign, boolean animate) { + translateSecurityViewLocation(leftAlign, animate, i -> {}); + } + + /** + * Moves the inner security view to the correct location with animation. This is triggered + * when the user double taps on the side of the screen that is not currently occupied by + * the security view. + */ + protected void translateSecurityViewLocation(boolean leftAlign, boolean animate, + Consumer<Float> securityAlphaListener) { + if (mRunningSecurityShiftAnimator != null) { + mRunningSecurityShiftAnimator.cancel(); + mRunningSecurityShiftAnimator = null; + } + + int targetTranslation = leftAlign + ? 0 : mView.getMeasuredWidth() - mViewFlipper.getWidth(); + + if (animate) { + // This animation is a bit fun to implement. The bouncer needs to move, and fade + // in/out at the same time. The issue is, the bouncer should only move a short + // amount (120dp or so), but obviously needs to go from one side of the screen to + // the other. This needs a pretty custom animation. + // + // This works as follows. It uses a ValueAnimation to simply drive the animation + // progress. This animator is responsible for both the translation of the bouncer, + // and the current fade. It will fade the bouncer out while also moving it along the + // 120dp path. Once the bouncer is fully faded out though, it will "snap" the + // bouncer closer to its destination, then fade it back in again. The effect is that + // the bouncer will move from 0 -> X while fading out, then + // (destination - X) -> destination while fading back in again. + // TODO(b/208250221): Make this animation properly abortable. + Interpolator positionInterpolator = AnimationUtils.loadInterpolator( + mView.getContext(), android.R.interpolator.fast_out_extra_slow_in); + Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN; + Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; + + mRunningSecurityShiftAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); + mRunningSecurityShiftAnimator.setDuration(SECURITY_SHIFT_ANIMATION_DURATION_MS); + mRunningSecurityShiftAnimator.setInterpolator(Interpolators.LINEAR); + + int initialTranslation = (int) mViewFlipper.getTranslationX(); + int totalTranslation = (int) mView.getResources().getDimension( + R.dimen.security_shift_animation_translation); + + final boolean shouldRestoreLayerType = mViewFlipper.hasOverlappingRendering() + && mViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE; + if (shouldRestoreLayerType) { + mViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null); + } + + float initialAlpha = mViewFlipper.getAlpha(); + + mRunningSecurityShiftAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mRunningSecurityShiftAnimator = null; + } + }); + mRunningSecurityShiftAnimator.addUpdateListener(animation -> { + float switchPoint = SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION; + boolean isFadingOut = animation.getAnimatedFraction() < switchPoint; + + int currentTranslation = (int) (positionInterpolator.getInterpolation( + animation.getAnimatedFraction()) * totalTranslation); + int translationRemaining = totalTranslation - currentTranslation; + + // Flip the sign if we're going from right to left. + if (leftAlign) { + currentTranslation = -currentTranslation; + translationRemaining = -translationRemaining; + } + + float opacity; + if (isFadingOut) { + // The bouncer fades out over the first X%. + float fadeOutFraction = MathUtils.constrainedMap( + /* rangeMin= */1.0f, + /* rangeMax= */0.0f, + /* valueMin= */0.0f, + /* valueMax= */switchPoint, + animation.getAnimatedFraction()); + opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction); + + // When fading out, the alpha needs to start from the initial opacity of the + // view flipper, otherwise we get a weird bit of jank as it ramps back to + // 100%. + mViewFlipper.setAlpha(opacity * initialAlpha); + + // Animate away from the source. + mViewFlipper.setTranslationX(initialTranslation + currentTranslation); + } else { + // And in again over the remaining (100-X)%. + float fadeInFraction = MathUtils.constrainedMap( + /* rangeMin= */0.0f, + /* rangeMax= */1.0f, + /* valueMin= */switchPoint, + /* valueMax= */1.0f, + animation.getAnimatedFraction()); + + opacity = fadeInInterpolator.getInterpolation(fadeInFraction); + mViewFlipper.setAlpha(opacity); + + // Fading back in, animate towards the destination. + mViewFlipper.setTranslationX(targetTranslation - translationRemaining); + } + securityAlphaListener.accept(opacity); + + if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) { + mViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null); + } + }); + + mRunningSecurityShiftAnimator.start(); + } else { + mViewFlipper.setTranslationX(targetTranslation); + } + } + + + boolean isLeftAligned() { + return mGlobalSettings.getInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE, + mDefaultSideSetting) + == Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT; + } + + protected void updateSideSetting(boolean leftAligned) { + mGlobalSettings.putInt( + Settings.Global.ONE_HANDED_KEYGUARD_SIDE, + leftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT + : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT); + } + } + + /** * Default bouncer is centered within the space */ static class DefaultViewMode implements ViewMode { @@ -802,7 +1013,7 @@ public class KeyguardSecurityContainer extends FrameLayout { * User switcher mode will display both the current user icon as well as * a user switcher, in both portrait and landscape modes. */ - static class UserSwitcherViewMode implements ViewMode { + static class UserSwitcherViewMode extends SidedSecurityMode { private ViewGroup mView; private ViewGroup mUserSwitcherViewGroup; private KeyguardSecurityViewFlipper mViewFlipper; @@ -814,11 +1025,15 @@ public class KeyguardSecurityContainer extends FrameLayout { private UserSwitcherController.UserSwitchCallback mUserSwitchCallback = this::setupUserSwitcher; + private float mAnimationLastAlpha = 1f; + private boolean mAnimationWaitsToShift = true; + @Override public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, @NonNull UserSwitcherController userSwitcherController) { + init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */false); mView = v; mViewFlipper = viewFlipper; mFalsingManager = falsingManager; @@ -832,9 +1047,7 @@ public class KeyguardSecurityContainer extends FrameLayout { true); mUserSwitcherViewGroup = mView.findViewById(R.id.keyguard_bouncer_user_switcher); } - updateSecurityViewLocation(); - mUserSwitcher = mView.findViewById(R.id.user_switcher_header); setupUserSwitcher(); mUserSwitcherController.addUserSwitchCallback(mUserSwitchCallback); @@ -1030,18 +1243,65 @@ public class KeyguardSecurityContainer extends FrameLayout { @Override public void updateSecurityViewLocation() { - int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans); + updateSecurityViewLocation(isLeftAligned(), /* animate= */false); + } + + public void updateSecurityViewLocation(boolean leftAlign, boolean animate) { + setYTranslation(); + setGravity(); + setXTranslation(leftAlign, animate); + } + private void setXTranslation(boolean leftAlign, boolean animate) { + if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { + mUserSwitcherViewGroup.setTranslationX(0); + mViewFlipper.setTranslationX(0); + } else { + int switcherTargetTranslation = leftAlign + ? mView.getMeasuredWidth() - mViewFlipper.getWidth() : 0; + if (animate) { + mAnimationWaitsToShift = true; + mAnimationLastAlpha = 1f; + translateSecurityViewLocation(leftAlign, animate, securityAlpha -> { + // During the animation security view fades out - alpha goes from 1 to + // (almost) 0 - and then fades in - alpha grows back to 1. + // If new alpha is bigger than previous one it means we're at inflection + // point and alpha is zero or almost zero. That's when we want to do + // translation of user switcher, so that it's not visible to the user. + boolean fullyFadeOut = securityAlpha == 0.0f + || securityAlpha > mAnimationLastAlpha; + if (fullyFadeOut && mAnimationWaitsToShift) { + mUserSwitcherViewGroup.setTranslationX(switcherTargetTranslation); + mAnimationWaitsToShift = false; + } + mUserSwitcherViewGroup.setAlpha(securityAlpha); + mAnimationLastAlpha = securityAlpha; + }); + } else { + translateSecurityViewLocation(leftAlign, animate); + mUserSwitcherViewGroup.setTranslationX(switcherTargetTranslation); + } + } + + } + + private void setGravity() { if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL); updateViewGravity(mUserSwitcherViewGroup, Gravity.CENTER_HORIZONTAL); + updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL); + } else { + // horizontal gravity is the same because we translate these views anyway + updateViewGravity(mViewFlipper, Gravity.LEFT | Gravity.BOTTOM); + updateViewGravity(mUserSwitcherViewGroup, Gravity.LEFT | Gravity.CENTER_VERTICAL); + } + } + private void setYTranslation() { + int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans); + if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { mUserSwitcherViewGroup.setTranslationY(yTrans); mViewFlipper.setTranslationY(-yTrans); } else { - updateViewGravity(mViewFlipper, Gravity.RIGHT | Gravity.BOTTOM); - updateViewGravity(mUserSwitcherViewGroup, Gravity.LEFT | Gravity.CENTER_VERTICAL); - // Attempt to reposition a bit higher to make up for this frame being a bit lower // on the device mUserSwitcherViewGroup.setTranslationY(-yTrans); @@ -1060,20 +1320,18 @@ public class KeyguardSecurityContainer extends FrameLayout { * Logic to enabled one-handed bouncer mode. Supports animating the bouncer * between alternate sides of the display. */ - static class OneHandedViewMode implements ViewMode { - @Nullable private ValueAnimator mRunningOneHandedAnimator; + static class OneHandedViewMode extends SidedSecurityMode { private ViewGroup mView; private KeyguardSecurityViewFlipper mViewFlipper; - private GlobalSettings mGlobalSettings; @Override public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, @NonNull UserSwitcherController userSwitcherController) { + init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */true); mView = v; mViewFlipper = viewFlipper; - mGlobalSettings = globalSettings; updateSecurityViewGravity(); updateSecurityViewLocation(isLeftAligned(), /* animate= */false); @@ -1107,159 +1365,13 @@ public class KeyguardSecurityContainer extends FrameLayout { updateSecurityViewLocation(isTouchOnLeft, /* animate= */false); } - boolean isLeftAligned() { - return mGlobalSettings.getInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE, - Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT) - == Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT; - } - - private void updateSideSetting(boolean leftAligned) { - mGlobalSettings.putInt( - Settings.Global.ONE_HANDED_KEYGUARD_SIDE, - leftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT - : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT); - } - - /** - * Determine if a double tap on this view is on the other side. If so, will animate - * positions and record the preference to always show on this side. - */ - @Override - public void handleDoubleTap(MotionEvent event) { - float x = event.getX(); - boolean currentlyLeftAligned = isLeftAligned(); - // Did the tap hit the "other" side of the bouncer? - if ((currentlyLeftAligned && (x > mView.getWidth() / 2f)) - || (!currentlyLeftAligned && (x < mView.getWidth() / 2f))) { - - boolean willBeLeftAligned = !currentlyLeftAligned; - updateSideSetting(willBeLeftAligned); - - int keyguardState = willBeLeftAligned - ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_LEFT - : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_RIGHT; - SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, keyguardState); - - updateSecurityViewLocation(willBeLeftAligned, true /* animate */); - } - } - @Override public void updateSecurityViewLocation() { updateSecurityViewLocation(isLeftAligned(), /* animate= */false); } - /** - * Moves the inner security view to the correct location (in one handed mode) with - * animation. This is triggered when the user taps on the side of the screen that is not - * currently occupied by the security view. - */ - private void updateSecurityViewLocation(boolean leftAlign, boolean animate) { - if (mRunningOneHandedAnimator != null) { - mRunningOneHandedAnimator.cancel(); - mRunningOneHandedAnimator = null; - } - - int targetTranslation = leftAlign - ? 0 : (int) (mView.getMeasuredWidth() - mViewFlipper.getWidth()); - - if (animate) { - // This animation is a bit fun to implement. The bouncer needs to move, and fade - // in/out at the same time. The issue is, the bouncer should only move a short - // amount (120dp or so), but obviously needs to go from one side of the screen to - // the other. This needs a pretty custom animation. - // - // This works as follows. It uses a ValueAnimation to simply drive the animation - // progress. This animator is responsible for both the translation of the bouncer, - // and the current fade. It will fade the bouncer out while also moving it along the - // 120dp path. Once the bouncer is fully faded out though, it will "snap" the - // bouncer closer to its destination, then fade it back in again. The effect is that - // the bouncer will move from 0 -> X while fading out, then - // (destination - X) -> destination while fading back in again. - // TODO(b/208250221): Make this animation properly abortable. - Interpolator positionInterpolator = AnimationUtils.loadInterpolator( - mView.getContext(), android.R.interpolator.fast_out_extra_slow_in); - Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN; - Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; - - mRunningOneHandedAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); - mRunningOneHandedAnimator.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS); - mRunningOneHandedAnimator.setInterpolator(Interpolators.LINEAR); - - int initialTranslation = (int) mViewFlipper.getTranslationX(); - int totalTranslation = (int) mView.getResources().getDimension( - R.dimen.one_handed_bouncer_move_animation_translation); - - final boolean shouldRestoreLayerType = mViewFlipper.hasOverlappingRendering() - && mViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE; - if (shouldRestoreLayerType) { - mViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null); - } - - float initialAlpha = mViewFlipper.getAlpha(); - - mRunningOneHandedAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mRunningOneHandedAnimator = null; - } - }); - mRunningOneHandedAnimator.addUpdateListener(animation -> { - float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION; - boolean isFadingOut = animation.getAnimatedFraction() < switchPoint; - - int currentTranslation = (int) (positionInterpolator.getInterpolation( - animation.getAnimatedFraction()) * totalTranslation); - int translationRemaining = totalTranslation - currentTranslation; - - // Flip the sign if we're going from right to left. - if (leftAlign) { - currentTranslation = -currentTranslation; - translationRemaining = -translationRemaining; - } - - if (isFadingOut) { - // The bouncer fades out over the first X%. - float fadeOutFraction = MathUtils.constrainedMap( - /* rangeMin= */1.0f, - /* rangeMax= */0.0f, - /* valueMin= */0.0f, - /* valueMax= */switchPoint, - animation.getAnimatedFraction()); - float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction); - - // When fading out, the alpha needs to start from the initial opacity of the - // view flipper, otherwise we get a weird bit of jank as it ramps back to - // 100%. - mViewFlipper.setAlpha(opacity * initialAlpha); - - // Animate away from the source. - mViewFlipper.setTranslationX(initialTranslation + currentTranslation); - } else { - // And in again over the remaining (100-X)%. - float fadeInFraction = MathUtils.constrainedMap( - /* rangeMin= */0.0f, - /* rangeMax= */1.0f, - /* valueMin= */switchPoint, - /* valueMax= */1.0f, - animation.getAnimatedFraction()); - - float opacity = fadeInInterpolator.getInterpolation(fadeInFraction); - mViewFlipper.setAlpha(opacity); - - // Fading back in, animate towards the destination. - mViewFlipper.setTranslationX(targetTranslation - translationRemaining); - } - - if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) { - mViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null); - } - }); - - mRunningOneHandedAnimator.start(); - } else { - mViewFlipper.setTranslationX(targetTranslation); - } + protected void updateSecurityViewLocation(boolean leftAlign, boolean animate) { + translateSecurityViewLocation(leftAlign, animate); } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 19a2d9ed5b7b..61e262440607 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -116,12 +116,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard // If we're in one handed mode, the user can tap on the opposite side of the screen // to move the bouncer across. In that case, inhibit the falsing (otherwise the taps // to move the bouncer to each screen side can end up closing it instead). - if (mView.getMode() == KeyguardSecurityContainer.MODE_ONE_HANDED) { - boolean isLeftAligned = mView.isOneHandedModeLeftAligned(); - if ((isLeftAligned && ev.getX() > mView.getWidth() / 2f) - || (!isLeftAligned && ev.getX() <= mView.getWidth() / 2f)) { - mFalsingCollector.avoidGesture(); - } + if (mView.isTouchOnTheOtherSideOfSecurity(ev)) { + mFalsingCollector.avoidGesture(); } if (mTouchDown != null) { @@ -169,8 +165,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) { int bouncerSide = SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__DEFAULT; - if (mView.getMode() == KeyguardSecurityContainer.MODE_ONE_HANDED) { - bouncerSide = mView.isOneHandedModeLeftAligned() + if (mView.isSidedSecurityMode()) { + bouncerSide = mView.isSecurityLeftAligned() ? SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__LEFT : SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__RIGHT; } @@ -367,8 +363,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard public void onResume(int reason) { if (mCurrentSecurityMode != SecurityMode.None) { int state = SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN; - if (mView.getMode() == KeyguardSecurityContainer.MODE_ONE_HANDED) { - state = mView.isOneHandedModeLeftAligned() + if (mView.isSidedSecurityMode()) { + state = mView.isSecurityLeftAligned() ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN_LEFT : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN_RIGHT; } diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt index 61b1b66338e8..c5955860aebf 100644 --- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -27,12 +27,15 @@ import android.graphics.Matrix import android.graphics.Paint import android.graphics.Path import android.graphics.RectF +import android.hardware.biometrics.BiometricSourceType import android.view.View import androidx.core.graphics.ColorUtils import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.settingslib.Utils import com.android.systemui.animation.Interpolators import com.android.systemui.plugins.statusbar.StatusBarStateController +import java.util.concurrent.Executor /** * When the face is enrolled, we use this view to show the face scanning animation and the camera @@ -42,7 +45,8 @@ class FaceScanningOverlay( context: Context, pos: Int, val statusBarStateController: StatusBarStateController, - val keyguardUpdateMonitor: KeyguardUpdateMonitor + val keyguardUpdateMonitor: KeyguardUpdateMonitor, + val mainExecutor: Executor ) : ScreenDecorations.DisplayCutoutView(context, pos) { private var showScanningAnim = false private val rimPaint = Paint() @@ -54,11 +58,26 @@ class FaceScanningOverlay( com.android.systemui.R.attr.wallpaperTextColorAccent) private var cameraProtectionAnimator: ValueAnimator? = null var hideOverlayRunnable: Runnable? = null + var faceAuthSucceeded = false init { visibility = View.INVISIBLE // only show this view when face scanning is happening } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mainExecutor.execute { + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + mainExecutor.execute { + keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) + } + } + override fun setColor(color: Int) { cameraProtectionColor = color invalidate() @@ -108,7 +127,6 @@ class FaceScanningOverlay( if (showScanningAnimNow == showScanningAnim) { return } - showScanningAnim = showScanningAnimNow updateProtectionBoundingPath() // Delay the relayout until the end of the animation when hiding, @@ -120,13 +138,20 @@ class FaceScanningOverlay( cameraProtectionAnimator?.cancel() cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress, - if (showScanningAnimNow) 1.0f else HIDDEN_CAMERA_PROTECTION_SCALE).apply { - startDelay = if (showScanningAnim) 0 else PULSE_DISAPPEAR_DURATION - duration = if (showScanningAnim) PULSE_APPEAR_DURATION else - CAMERA_PROTECTION_DISAPPEAR_DURATION - interpolator = if (showScanningAnim) Interpolators.STANDARD else - Interpolators.EMPHASIZED - + if (showScanningAnimNow) SHOW_CAMERA_PROTECTION_SCALE + else HIDDEN_CAMERA_PROTECTION_SCALE).apply { + startDelay = + if (showScanningAnim) 0 + else if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION + else PULSE_ERROR_DISAPPEAR_DURATION + duration = + if (showScanningAnim) CAMERA_PROTECTION_APPEAR_DURATION + else if (faceAuthSucceeded) CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION + else CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION + interpolator = + if (showScanningAnim) Interpolators.STANDARD_ACCELERATE + else if (faceAuthSucceeded) Interpolators.STANDARD + else Interpolators.STANDARD_DECELERATE addUpdateListener(ValueAnimator.AnimatorUpdateListener { animation: ValueAnimator -> cameraProtectionProgress = animation.animatedValue as Float @@ -143,47 +168,73 @@ class FaceScanningOverlay( } } }) - start() } rimAnimator?.cancel() rimAnimator = AnimatorSet().apply { - val rimAppearOrDisappearAnimator = ValueAnimator.ofFloat(rimProgress, - if (showScanningAnim) PULSE_RADIUS_OUT else (PULSE_RADIUS_IN * 1.15f)).apply { - duration = if (showScanningAnim) PULSE_APPEAR_DURATION else PULSE_DISAPPEAR_DURATION - interpolator = Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } if (showScanningAnim) { - // appear and then pulse in/out - playSequentially(rimAppearOrDisappearAnimator, + val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE, + PULSE_RADIUS_OUT).apply { + duration = PULSE_APPEAR_DURATION + interpolator = Interpolators.STANDARD_DECELERATE + addUpdateListener(ValueAnimator.AnimatorUpdateListener { + animation: ValueAnimator -> + rimProgress = animation.animatedValue as Float + invalidate() + }) + } + + // animate in camera protection, rim, and then pulse in/out + playSequentially(cameraProtectionAnimator, rimAppearAnimator, createPulseAnimator(), createPulseAnimator(), createPulseAnimator(), createPulseAnimator(), createPulseAnimator(), createPulseAnimator()) } else { - val opacityAnimator = ValueAnimator.ofInt(255, 0).apply { - duration = PULSE_DISAPPEAR_DURATION - interpolator = Interpolators.LINEAR + val rimDisappearAnimator = ValueAnimator.ofFloat( + rimProgress, + if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS + else SHOW_CAMERA_PROTECTION_SCALE + ).apply { + duration = + if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION + else PULSE_ERROR_DISAPPEAR_DURATION + interpolator = + if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE + else Interpolators.STANDARD addUpdateListener(ValueAnimator.AnimatorUpdateListener { animation: ValueAnimator -> - rimPaint.alpha = animation.animatedValue as Int + rimProgress = animation.animatedValue as Float invalidate() }) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimProgress = HIDDEN_RIM_SCALE + invalidate() + } + }) } - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - rimProgress = HIDDEN_RIM_SCALE - rimPaint.alpha = 255 - invalidate() + if (faceAuthSucceeded) { + val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply { + duration = PULSE_SUCCESS_DISAPPEAR_DURATION + interpolator = Interpolators.LINEAR + addUpdateListener(ValueAnimator.AnimatorUpdateListener { + animation: ValueAnimator -> + rimPaint.alpha = animation.animatedValue as Int + invalidate() + }) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimPaint.alpha = 255 + invalidate() + } + }) } - }) - - // disappear - playTogether(rimAppearOrDisappearAnimator, opacityAnimator) + val rimSuccessAnimator = AnimatorSet() + rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator) + playTogether(rimSuccessAnimator, cameraProtectionAnimator) + } else { + playTogether(rimDisappearAnimator, cameraProtectionAnimator) + } } addListener(object : AnimatorListenerAdapter() { @@ -253,15 +304,72 @@ class FaceScanningOverlay( } } + private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { + override fun onBiometricAuthenticated( + userId: Int, + biometricSourceType: BiometricSourceType?, + isStrongBiometric: Boolean + ) { + if (biometricSourceType == BiometricSourceType.FACE) { + post { + faceAuthSucceeded = true + enableShowProtection(true) + } + } + } + + override fun onBiometricAcquired( + biometricSourceType: BiometricSourceType?, + acquireInfo: Int + ) { + if (biometricSourceType == BiometricSourceType.FACE) { + post { + faceAuthSucceeded = false // reset + } + } + } + + override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) { + if (biometricSourceType == BiometricSourceType.FACE) { + post { + faceAuthSucceeded = false + enableShowProtection(false) + } + } + } + + override fun onBiometricError( + msgId: Int, + errString: String?, + biometricSourceType: BiometricSourceType? + ) { + if (biometricSourceType == BiometricSourceType.FACE) { + post { + faceAuthSucceeded = false + enableShowProtection(false) + } + } + } + } + companion object { private const val HIDDEN_RIM_SCALE = HIDDEN_CAMERA_PROTECTION_SCALE + private const val SHOW_CAMERA_PROTECTION_SCALE = 1f + + private const val PULSE_RADIUS_IN = 1.1f + private const val PULSE_RADIUS_OUT = 1.125f + private const val PULSE_RADIUS_SUCCESS = 1.25f + + private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L + private const val PULSE_APPEAR_DURATION = 250L // without start delay - private const val PULSE_APPEAR_DURATION = 350L private const val PULSE_DURATION_INWARDS = 500L private const val PULSE_DURATION_OUTWARDS = 500L - private const val PULSE_DISAPPEAR_DURATION = 850L - private const val CAMERA_PROTECTION_DISAPPEAR_DURATION = 700L // excluding start delay - private const val PULSE_RADIUS_IN = 1.15f - private const val PULSE_RADIUS_OUT = 1.25f + + private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L + private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay + + private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L + private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java index ec4cf2fd8bd4..24b893340ae0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java @@ -510,6 +510,7 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud mKeyguardViewManager.isBouncerInTransit() ? BouncerPanelExpansionCalculator .aboutToShowBouncerProgress(fraction) : fraction; updateAlpha(); + updatePauseAuth(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index aeda20f16515..afc58ef70ee1 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -45,6 +45,7 @@ import android.content.pm.ShortcutManager; import android.content.res.Resources; import android.hardware.SensorManager; import android.hardware.SensorPrivacyManager; +import android.hardware.camera2.CameraManager; import android.hardware.devicestate.DeviceStateManager; import android.hardware.display.AmbientDisplayConfiguration; import android.hardware.display.ColorDisplayManager; @@ -60,6 +61,7 @@ import android.net.ConnectivityManager; import android.net.NetworkScoreManager; import android.net.wifi.WifiManager; import android.os.BatteryStats; +import android.os.PowerExemptionManager; import android.os.PowerManager; import android.os.ServiceManager; import android.os.UserManager; @@ -377,6 +379,13 @@ public class FrameworkServicesModule { /** */ @Provides + @Singleton + static PowerExemptionManager providePowerExemptionManager(Context context) { + return context.getSystemService(PowerExemptionManager.class); + } + + /** */ + @Provides @Main public SharedPreferences provideSharePreferences(Context context) { return Prefs.get(context); @@ -545,4 +554,10 @@ public class FrameworkServicesModule { static SafetyCenterManager provideSafetyCenterManager(Context context) { return context.getSystemService(SafetyCenterManager.class); } + + @Provides + @Singleton + static CameraManager provideCameraManager(Context context) { + return context.getSystemService(CameraManager.class); + } } diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt index adc0096eed37..81d3d6caebd7 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt @@ -32,10 +32,12 @@ import android.widget.FrameLayout import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.FaceScanningOverlay import com.android.systemui.biometrics.AuthController +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.plugins.statusbar.StatusBarStateController +import java.util.concurrent.Executor import javax.inject.Inject @SysUISingleton @@ -44,6 +46,7 @@ class FaceScanningProviderFactory @Inject constructor( private val context: Context, private val statusBarStateController: StatusBarStateController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + @Main private val mainExecutor: Executor, private val featureFlags: FeatureFlags ) : DecorProviderFactory() { private val display = context.display @@ -82,7 +85,9 @@ class FaceScanningProviderFactory @Inject constructor( bound.baseOnRotation0(displayInfo.rotation), authController, statusBarStateController, - keyguardUpdateMonitor) + keyguardUpdateMonitor, + mainExecutor + ) ) } } @@ -102,7 +107,8 @@ class FaceScanningOverlayProviderImpl( override val alignedBound: Int, private val authController: AuthController, private val statusBarStateController: StatusBarStateController, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val mainExecutor: Executor ) : BoundDecorProvider() { override val viewId: Int = com.android.systemui.R.id.face_scanning_anim @@ -127,7 +133,9 @@ class FaceScanningOverlayProviderImpl( context, alignedBound, statusBarStateController, - keyguardUpdateMonitor) + keyguardUpdateMonitor, + mainExecutor + ) view.id = viewId FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT).let { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java index fc71e2fb2329..69e41ba9b284 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java @@ -101,6 +101,9 @@ public class DreamOverlayStateController implements public void addComplication(Complication complication) { mExecutor.execute(() -> { if (mComplications.add(complication)) { + if (DEBUG) { + Log.d(TAG, "addComplication: added " + complication); + } mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged()); } }); @@ -112,6 +115,9 @@ public class DreamOverlayStateController implements public void removeComplication(Complication complication) { mExecutor.execute(() -> { if (mComplications.remove(complication)) { + if (DEBUG) { + Log.d(TAG, "removeComplication: removed " + complication); + } mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged()); } }); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java index 486fc893732f..be94e5031917 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java @@ -82,6 +82,7 @@ public class SmartSpaceComplication implements Complication { mSmartSpaceController.addListener(mSmartspaceListener); } else { mSmartSpaceController.removeListener(mSmartspaceListener); + mDreamOverlayStateController.removeComplication(mComplication); } } }); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java index c1173aea8b15..fd6cfc0700ad 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java @@ -21,6 +21,7 @@ import static com.android.systemui.dreams.complication.dagger.ComplicationModule import android.graphics.Rect; import android.graphics.Region; +import android.os.Debug; import android.util.Log; import android.view.View; @@ -44,7 +45,8 @@ import javax.inject.Named; * a {@link ComplicationLayoutEngine}. */ public class ComplicationHostViewController extends ViewController<ConstraintLayout> { - public static final String TAG = "ComplicationHostVwCtrl"; + private static final String TAG = "ComplicationHostVwCtrl"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final ComplicationLayoutEngine mLayoutEngine; private final LifecycleOwner mLifecycleOwner; @@ -90,6 +92,11 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay } private void updateComplications(Collection<ComplicationViewModel> complications) { + if (DEBUG) { + Log.d(TAG, "updateComplications called. Callers = " + Debug.getCallers(25)); + Log.d(TAG, " mComplications = " + mComplications.toString()); + Log.d(TAG, " complications = " + complications.toString()); + } final Collection<ComplicationId> ids = complications.stream() .map(complicationViewModel -> complicationViewModel.getId()) .collect(Collectors.toSet()); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java index ded61a8b80d9..9cd149b9bfee 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java @@ -54,7 +54,7 @@ import javax.inject.Named; */ @DreamOverlayComponent.DreamOverlayScope public class ComplicationLayoutEngine implements Complication.VisibilityController { - public static final String TAG = "ComplicationLayoutEngine"; + public static final String TAG = "ComplicationLayoutEng"; /** * {@link ViewEntry} is an internal container, capturing information necessary for working with @@ -529,7 +529,7 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll */ public void addComplication(ComplicationId id, View view, ComplicationLayoutParams lp, @Complication.Category int category) { - Log.d(TAG, "engine: " + this + " addComplication"); + Log.d(TAG, "@" + Integer.toHexString(this.hashCode()) + " addComplication: " + id); // If the complication is present, remove. if (mEntries.containsKey(id)) { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java index f0239371ee63..00cf58c0c665 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java @@ -64,4 +64,9 @@ public class ComplicationViewModel extends ViewModel { public void exitDream() { mHost.requestExitDream(); } + + @Override + public String toString() { + return mId + "=" + mComplication.toString(); + } } 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 9789cef949d3..63f63a5093d2 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt @@ -145,9 +145,6 @@ class DreamSmartspaceController @Inject constructor( if (view !is View) { return null } - - view.setIsDreaming(true) - return view } else { null diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 47c678b4b8cf..47a68bbbc8c0 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -60,6 +60,9 @@ public class Flags { public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS = new ResourceBooleanFlag(108, R.bool.config_notificationToContents); + public static final BooleanFlag REMOVE_UNRANKED_NOTIFICATIONS = + new BooleanFlag(109, false); + /***************************************/ // 200 - keyguard/lockscreen diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 550f393f4ca3..ab30db297fce 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -54,6 +54,7 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.os.Binder; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -1044,7 +1045,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene @Override public boolean showBeforeProvisioning() { - return false; + return Build.isDebuggable() && mGlobalSettings.getInt( + Settings.Global.BUGREPORT_IN_POWER_MENU, 0) != 0; } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index b96eee717260..95b3b3f6452f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -55,6 +55,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemProperties; import android.os.Trace; +import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.view.IRemoteAnimationFinishedCallback; @@ -157,7 +158,7 @@ public class KeyguardService extends Service { Rect localBounds = new Rect(change.getEndAbsBounds()); localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y); - out.add(new RemoteAnimationTarget( + final RemoteAnimationTarget target = new RemoteAnimationTarget( taskId, newModeToLegacyMode(change.getMode()), change.getLeash(), @@ -168,7 +169,15 @@ public class KeyguardService extends Service { info.getChanges().size() - i, new Point(), localBounds, new Rect(change.getEndAbsBounds()), windowConfiguration, isNotInRecents, null /* startLeash */, - change.getStartAbsBounds(), taskInfo, false /* allowEnterPip */)); + change.getStartAbsBounds(), taskInfo, false /* allowEnterPip */); + // Use hasAnimatingParent to mark the anything below root task + if (taskId != -1 && change.getParent() != null) { + final TransitionInfo.Change parentChange = info.getChange(change.getParent()); + if (parentChange != null && parentChange.getTaskInfo() != null) { + target.hasAnimatingParent = true; + } + } + out.add(target); } return out.toArray(new RemoteAnimationTarget[out.size()]); } @@ -189,8 +198,12 @@ public class KeyguardService extends Service { } } + // Wrap Keyguard going away animation private static IRemoteTransition wrap(IRemoteAnimationRunner runner) { return new IRemoteTransition.Stub() { + final ArrayMap<IBinder, IRemoteTransitionFinishedCallback> mFinishCallbacks = + new ArrayMap<>(); + @Override public void startAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) @@ -200,16 +213,37 @@ public class KeyguardService extends Service { final RemoteAnimationTarget[] wallpapers = wrap(info, true /* wallpapers */); final RemoteAnimationTarget[] nonApps = new RemoteAnimationTarget[0]; - // TODO: Remove this, and update alpha value in the IAnimationRunner. - for (TransitionInfo.Change change : info.getChanges()) { - t.setAlpha(change.getLeash(), 1.0f); + // Sets the alpha to 0 for the opening root task for fade in animation. And since + // the fade in animation can only apply on the first opening app, so set alpha to 1 + // for anything else. + boolean foundOpening = false; + for (RemoteAnimationTarget target : apps) { + if (target.taskId != -1 + && target.mode == RemoteAnimationTarget.MODE_OPENING + && !target.hasAnimatingParent) { + if (foundOpening) { + Log.w(TAG, "More than one opening target"); + t.setAlpha(target.leash, 1.0f); + continue; + } + t.setAlpha(target.leash, 0.0f); + foundOpening = true; + } else { + t.setAlpha(target.leash, 1.0f); + } } t.apply(); + synchronized (mFinishCallbacks) { + mFinishCallbacks.put(transition, finishCallback); + } runner.onAnimationStart(getTransitionOldType(info.getType(), info.getFlags(), apps), apps, wallpapers, nonApps, new IRemoteAnimationFinishedCallback.Stub() { @Override public void onAnimationFinished() throws RemoteException { + synchronized (mFinishCallbacks) { + if (mFinishCallbacks.remove(transition) == null) return; + } Slog.d(TAG, "Finish IRemoteAnimationRunner."); finishCallback.onTransitionFinished(null /* wct */, null /* t */); } @@ -220,7 +254,20 @@ public class KeyguardService extends Service { public void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishCallback) { - + try { + final IRemoteTransitionFinishedCallback origFinishCB; + synchronized (mFinishCallbacks) { + origFinishCB = mFinishCallbacks.remove(transition); + } + if (origFinishCB == null) { + // already finished (or not started yet), so do nothing. + return; + } + runner.onAnimationCancelled(false /* isKeyguardOccluded */); + origFinishCB.onTransitionFinished(null /* wct */, null /* t */); + } catch (RemoteException e) { + // nothing, we'll just let it finish on its own I guess. + } } }; } @@ -349,7 +396,7 @@ public class KeyguardService extends Service { } @Override // Binder interface - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { mKeyguardViewMediator.cancelKeyguardExitAnimation(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 99b57200a397..382323f3aed9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -26,6 +26,7 @@ import android.os.Handler import android.os.RemoteException import android.util.Log import android.view.RemoteAnimationTarget +import android.view.SurfaceControl import android.view.SyncRtSurfaceTransactionApplier import android.view.View import androidx.annotation.VisibleForTesting @@ -293,6 +294,8 @@ class KeyguardUnlockAnimationController @Inject constructor( private val handler = Handler() + private val tmpFloat = FloatArray(9) + init { with(surfaceBehindAlphaAnimator) { duration = SURFACE_BEHIND_SWIPE_FADE_DURATION_MS @@ -723,13 +726,27 @@ class KeyguardUnlockAnimationController @Inject constructor( if (keyguardStateController.isSnappingKeyguardBackAfterSwipe) amount else surfaceBehindAlpha - applyParamsToSurface( - SyncRtSurfaceTransactionApplier.SurfaceParams.Builder( - surfaceBehindRemoteAnimationTarget!!.leash) - .withMatrix(surfaceBehindMatrix) - .withCornerRadius(roundedCornerRadius) - .withAlpha(animationAlpha) - .build()) + // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is unable + // to draw + val sc: SurfaceControl? = surfaceBehindRemoteAnimationTarget?.leash + if (keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE && + sc?.isValid == true) { + with(SurfaceControl.Transaction()) { + setMatrix(sc, surfaceBehindMatrix, tmpFloat) + setCornerRadius(sc, roundedCornerRadius) + setAlpha(sc, animationAlpha) + apply() + } + } else { + applyParamsToSurface( + SyncRtSurfaceTransactionApplier.SurfaceParams.Builder( + surfaceBehindRemoteAnimationTarget!!.leash) + .withMatrix(surfaceBehindMatrix) + .withCornerRadius(roundedCornerRadius) + .withAlpha(animationAlpha) + .build() + ) + } } /** @@ -744,8 +761,11 @@ class KeyguardUnlockAnimationController @Inject constructor( handler.removeCallbacksAndMessages(null) // Make sure we made the surface behind fully visible, just in case. It should already be - // fully visible. If the launcher is doing its own animation, let it continue without - // forcing it to 1f. + // fully visible. The exit animation is finished, and we should not hold the leash anymore, + // so forcing it to 1f. + surfaceBehindAlphaAnimator.cancel() + surfaceBehindEntryAnimator.cancel() + surfaceBehindAlpha = 1f setSurfaceBehindAppearAmount(1f) launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */) @@ -910,4 +930,4 @@ class KeyguardUnlockAnimationController @Inject constructor( return context.resources.getIntArray(R.array.config_foldedDeviceStates).isNotEmpty() } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index f9a1c66af41d..340cde11c6e7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -843,7 +843,6 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, @Override public void onLaunchAnimationCancelled() { - setOccluded(true /* occluded */, false /* animate */); Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: " + mOccluded); } @@ -911,12 +910,12 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, private final Matrix mUnoccludeMatrix = new Matrix(); @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { if (mUnoccludeAnimator != null) { mUnoccludeAnimator.cancel(); } - setOccluded(false /* isOccluded */, false /* animate */); + setOccluded(isKeyguardOccluded /* isOccluded */, false /* animate */); Log.d(TAG, "Unocclude animation cancelled. Occluded state is now: " + mOccluded); } @@ -2503,10 +2502,18 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, mInteractionJankMonitor.begin( createInteractionJankMonitorConf("DismissPanel")); + // Apply the opening animation on root task if exists + RemoteAnimationTarget aniTarget = apps[0]; + for (RemoteAnimationTarget tmpTarget : apps) { + if (tmpTarget.taskId != -1 && !tmpTarget.hasAnimatingParent) { + aniTarget = tmpTarget; + break; + } + } // Pass the surface and metadata to the unlock animation controller. mKeyguardUnlockAnimationControllerLazy.get() .notifyStartSurfaceBehindRemoteAnimation( - apps[0], startTime, mSurfaceBehindRemoteAnimationRequested); + aniTarget, startTime, mSurfaceBehindRemoteAnimationRequested); } else { mInteractionJankMonitor.begin( createInteractionJankMonitorConf("RemoteAnimationDisabled")); @@ -3167,9 +3174,9 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } @Override - public void onAnimationCancelled() throws RemoteException { + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { if (mRunner != null) { - mRunner.onAnimationCancelled(); + mRunner.onAnimationCancelled(isKeyguardOccluded); } } @@ -3210,9 +3217,12 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } @Override - public void onAnimationCancelled() throws RemoteException { - super.onAnimationCancelled(); - Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: " + mOccluded); + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { + super.onAnimationCancelled(isKeyguardOccluded); + setOccluded(isKeyguardOccluded /* occluded */, false /* animate */); + + Log.d(TAG, "Occlude animation cancelled by WM. " + + "Setting occluded state to: " + mOccluded); } } } diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 90cca15ddf21..d0da18aaba05 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -27,6 +27,8 @@ import com.android.systemui.log.LogBufferFactory; import com.android.systemui.log.LogcatEchoTracker; import com.android.systemui.log.LogcatEchoTrackerDebug; import com.android.systemui.log.LogcatEchoTrackerProd; +import com.android.systemui.statusbar.notification.NotifPipelineFlags; +import com.android.systemui.util.Compile; import dagger.Module; import dagger.Provides; @@ -48,8 +50,14 @@ public class LogModule { @Provides @SysUISingleton @NotificationLog - public static LogBuffer provideNotificationsLogBuffer(LogBufferFactory factory) { - return factory.create("NotifLog", 1000 /* maxSize */, false /* systrace */); + public static LogBuffer provideNotificationsLogBuffer( + LogBufferFactory factory, + NotifPipelineFlags notifPipelineFlags) { + int maxSize = 1000; + if (Compile.IS_DEBUG && notifPipelineFlags.isDevLoggingEnabled()) { + maxSize *= 10; + } + return factory.create("NotifLog", maxSize, false /* systrace */); } /** Provides a logging buffer for logs related to heads up presentation of notifications. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index ed5c1933af25..2f732de50ea1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -23,6 +23,7 @@ import android.annotation.IntDef import android.content.Context import android.content.res.Configuration import android.graphics.Rect +import android.util.Log import android.util.MathUtils import android.view.View import android.view.ViewGroup @@ -48,6 +49,8 @@ import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.traceSection import javax.inject.Inject +private val TAG: String = MediaHierarchyManager::class.java.simpleName + /** * Similarly to isShown but also excludes views that have 0 alpha */ @@ -964,6 +967,14 @@ class MediaHierarchyManager @Inject constructor( top, left + currentBounds.width(), top + currentBounds.height()) + + if (mediaFrame.childCount > 0) { + val child = mediaFrame.getChildAt(0) + if (mediaFrame.height < child.height) { + Log.wtf(TAG, "mediaFrame height is too small for child: " + + "${mediaFrame.height} vs ${child.height}") + } + } } if (isCrossFadeAnimatorRunning) { // When cross-fading with an animation, we only notify the media carousel of the diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index f0ce30d2dc66..f2f275323d58 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -285,6 +285,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { Log.d(TAG, "This device is already connected! : " + device.getName()); return; } + mController.setTemporaryAllowListExceptionIfNeeded(device); mCurrentActivePosition = -1; mController.connectDevice(device); device.setState(MediaDeviceState.STATE_CONNECTING); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index ccc0a3db0611..bec67397a926 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -69,11 +69,13 @@ public abstract class MediaOutputBaseAdapter extends View mHolderView; boolean mIsDragging; int mCurrentActivePosition; + private boolean mIsInitVolumeFirstTime; public MediaOutputBaseAdapter(MediaOutputController controller) { mController = controller; mIsDragging = false; mCurrentActivePosition = -1; + mIsInitVolumeFirstTime = true; } @Override @@ -275,7 +277,7 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar.setMaxVolume(device.getMaxVolume()); final int currentVolume = device.getCurrentVolume(); if (mSeekBar.getVolume() != currentVolume) { - if (isCurrentSeekbarInvisible) { + if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) { animateCornerAndVolume(mSeekBar.getProgress(), MediaOutputSeekbar.scaleVolumeToProgress(currentVolume)); } else { @@ -284,6 +286,9 @@ public abstract class MediaOutputBaseAdapter extends } } } + if (mIsInitVolumeFirstTime) { + mIsInitVolumeFirstTime = false; + } mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt index 38005db28cf6..0fa326573c9c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt @@ -19,6 +19,7 @@ package com.android.systemui.media.dialog import android.content.Context import android.media.AudioManager import android.media.session.MediaSessionManager +import android.os.PowerExemptionManager import android.view.View import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.LocalBluetoothManager @@ -43,7 +44,8 @@ class MediaOutputBroadcastDialogFactory @Inject constructor( private val uiEventLogger: UiEventLogger, private val dialogLaunchAnimator: DialogLaunchAnimator, private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>, - private val audioManager: AudioManager + private val audioManager: AudioManager, + private val powerExemptionManager: PowerExemptionManager ) { var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null @@ -54,7 +56,8 @@ class MediaOutputBroadcastDialogFactory @Inject constructor( val controller = MediaOutputController(context, packageName, mediaSessionManager, lbm, starter, notifCollection, - dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager) + dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager, + powerExemptionManager) val dialog = MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller) mediaOutputBroadcastDialog = dialog 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 0b4b03619a13..247ffa7c2ef9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -44,6 +44,7 @@ import android.media.session.MediaController; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.IBinder; +import android.os.PowerExemptionManager; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; @@ -101,6 +102,9 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final String PAGE_CONNECTED_DEVICES_KEY = "top_level_connected_devices"; + private static final long ALLOWLIST_DURATION_MS = 20000; + private static final String ALLOWLIST_REASON = "mediaoutput:remote_transfer"; + private final String mPackageName; private final Context mContext; private final MediaSessionManager mMediaSessionManager; @@ -114,6 +118,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>(); final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>(); private final AudioManager mAudioManager; + private final PowerExemptionManager mPowerExemptionManager; private final NearbyMediaDevicesManager mNearbyMediaDevicesManager; private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>(); @@ -147,7 +152,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, CommonNotifCollection notifCollection, DialogLaunchAnimator dialogLaunchAnimator, Optional<NearbyMediaDevicesManager> nearbyMediaDevicesManagerOptional, - AudioManager audioManager) { + AudioManager audioManager, + PowerExemptionManager powerExemptionManager) { mContext = context; mPackageName = packageName; mMediaSessionManager = mediaSessionManager; @@ -155,6 +161,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mActivityStarter = starter; mNotifCollection = notifCollection; mAudioManager = audioManager; + mPowerExemptionManager = powerExemptionManager; InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); @@ -776,7 +783,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, MediaOutputController controller = new MediaOutputController(mContext, mPackageName, mMediaSessionManager, mLocalBluetoothManager, mActivityStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), - mAudioManager); + mAudioManager, mPowerExemptionManager); MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true, broadcastSender, controller); mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog); @@ -822,6 +829,17 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, broadcast.setBroadcastCode(broadcastCode.getBytes(StandardCharsets.UTF_8)); } + void setTemporaryAllowListExceptionIfNeeded(MediaDevice targetDevice) { + if (mPowerExemptionManager == null || mPackageName == null) { + Log.w(TAG, "powerExemptionManager or package name is null"); + return; + } + mPowerExemptionManager.addToTemporaryAllowList(mPackageName, + PowerExemptionManager.REASON_MEDIA_NOTIFICATION_TRANSFER, + ALLOWLIST_REASON, + ALLOWLIST_DURATION_MS); + } + String getBroadcastMetadata() { LocalBluetoothLeBroadcast broadcast = mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt index 8701d4aba720..8249a7c0779c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt @@ -19,6 +19,7 @@ package com.android.systemui.media.dialog import android.content.Context import android.media.AudioManager import android.media.session.MediaSessionManager +import android.os.PowerExemptionManager import android.view.View import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger @@ -45,7 +46,8 @@ class MediaOutputDialogFactory @Inject constructor( private val uiEventLogger: UiEventLogger, private val dialogLaunchAnimator: DialogLaunchAnimator, private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>, - private val audioManager: AudioManager + private val audioManager: AudioManager, + private val powerExemptionManager: PowerExemptionManager ) { companion object { private const val INTERACTION_JANK_TAG = "media_output" @@ -60,8 +62,8 @@ class MediaOutputDialogFactory @Inject constructor( val controller = MediaOutputController( context, packageName, mediaSessionManager, lbm, starter, notifCollection, - dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager - ) + dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager, + powerExemptionManager) val dialog = MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller, uiEventLogger) mediaOutputDialog = dialog diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamComplication.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamComplication.java index 7c0481049098..2c35db337cda 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamComplication.java +++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamComplication.java @@ -37,11 +37,6 @@ public class MediaDreamComplication implements Complication { } @Override - public int getRequiredTypeAvailability() { - return COMPLICATION_TYPE_CAST_INFO; - } - - @Override public ViewHolder createView(ComplicationViewModel model) { return mComponentFactory.create().getViewHolder(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt index 3cc99a8ef77e..e95976f555f8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt @@ -27,4 +27,4 @@ interface ChipInfoCommon { fun getTimeoutMs(): Long } -const val DEFAULT_TIMEOUT_MILLIS = 3000L +const val DEFAULT_TIMEOUT_MILLIS = 4000L diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt index 7cc52e428218..0f1cdcc3fa5b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt @@ -25,13 +25,14 @@ import android.graphics.drawable.Drawable import android.os.PowerManager import android.os.SystemClock import android.util.Log -import android.view.Gravity import android.view.LayoutInflater import android.view.MotionEvent -import android.view.View import android.view.ViewGroup import android.view.WindowManager -import android.widget.LinearLayout +import android.view.accessibility.AccessibilityManager +import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS +import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS +import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT import com.android.internal.widget.CachingIconView import com.android.settingslib.Utils import com.android.systemui.R @@ -56,16 +57,20 @@ abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>( private val windowManager: WindowManager, private val viewUtil: ViewUtil, @Main private val mainExecutor: DelayableExecutor, + private val accessibilityManager: AccessibilityManager, private val tapGestureDetector: TapGestureDetector, private val powerManager: PowerManager, @LayoutRes private val chipLayoutRes: Int ) { - /** The window layout parameters we'll use when attaching the view to a window. */ + + /** + * Window layout params that will be used as a starting point for the [windowLayoutParams] of + * all subclasses. + */ @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY - private val windowLayoutParams = WindowManager.LayoutParams().apply { + internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply { width = WindowManager.LayoutParams.WRAP_CONTENT height = WindowManager.LayoutParams.WRAP_CONTENT - gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL title = WINDOW_TITLE @@ -73,6 +78,14 @@ abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>( setTrustedOverlay() } + /** + * The window layout parameters we'll use when attaching the view to a window. + * + * Subclasses must override this to provide their specific layout params, and they should use + * [commonWindowLayoutParams] as part of their layout params. + */ + internal abstract val windowLayoutParams: WindowManager.LayoutParams + /** The chip view currently being displayed. Null if the chip is not being displayed. */ private var chipView: ViewGroup? = null @@ -110,10 +123,16 @@ abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>( } // Cancel and re-set the chip timeout each time we get a new state. + val timeout = accessibilityManager.getRecommendedTimeoutMillis( + chipInfo.getTimeoutMs().toInt(), + // Not all chips have controls so FLAG_CONTENT_CONTROLS might be superfluous, but + // include it just to be safe. + FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS + ) cancelChipViewTimeout?.run() cancelChipViewTimeout = mainExecutor.executeDelayed( { removeChip(MediaTttRemovalReason.REASON_TIMEOUT) }, - chipInfo.getTimeoutMs() + timeout.toLong() ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 072263fcf38c..f9818f0ad8be 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -24,8 +24,11 @@ import android.media.MediaRoute2Info import android.os.Handler import android.os.PowerManager import android.util.Log +import android.view.Gravity +import android.view.View import android.view.ViewGroup import android.view.WindowManager +import android.view.accessibility.AccessibilityManager import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main @@ -35,6 +38,7 @@ import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCom import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.gesture.TapGestureDetector +import com.android.systemui.util.animation.AnimationUtil.Companion.frames import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.view.ViewUtil import javax.inject.Inject @@ -52,6 +56,7 @@ class MediaTttChipControllerReceiver @Inject constructor( windowManager: WindowManager, viewUtil: ViewUtil, mainExecutor: DelayableExecutor, + accessibilityManager: AccessibilityManager, tapGestureDetector: TapGestureDetector, powerManager: PowerManager, @Main private val mainHandler: Handler, @@ -62,10 +67,16 @@ class MediaTttChipControllerReceiver @Inject constructor( windowManager, viewUtil, mainExecutor, + accessibilityManager, tapGestureDetector, powerManager, R.layout.media_ttt_chip_receiver ) { + override val windowLayoutParams = commonWindowLayoutParams.apply { + height = getWindowHeight() + gravity = Gravity.BOTTOM.or(Gravity.CENTER_HORIZONTAL) + } + private val commandQueueCallbacks = object : CommandQueue.Callbacks { override fun updateMediaTapToTransferReceiverDisplay( @StatusBarManager.MediaTransferReceiverState displayState: Int, @@ -128,6 +139,19 @@ class MediaTttChipControllerReceiver @Inject constructor( ) } + override fun animateChipIn(chipView: ViewGroup) { + val appIconView = chipView.requireViewById<View>(R.id.app_icon) + appIconView.animate() + .translationYBy(-1 * getTranslationAmount().toFloat()) + .setDuration(30.frames) + .start() + appIconView.animate() + .alpha(1f) + .setDuration(5.frames) + .start() + + } + override fun getIconSize(isAppIcon: Boolean): Int? = context.resources.getDimensionPixelSize( if (isAppIcon) { @@ -136,6 +160,17 @@ class MediaTttChipControllerReceiver @Inject constructor( R.dimen.media_ttt_generic_icon_size_receiver } ) + + private fun getWindowHeight(): Int { + return context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver) + + // Make the window large enough to accommodate the animation amount + getTranslationAmount() + } + + /** Returns the amount that the chip will be translated by in its intro animation. */ + private fun getTranslationAmount(): Int { + return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation) + } } data class ChipReceiverInfo( diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt index 54b4380e2443..797a7701413b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt @@ -21,9 +21,11 @@ import android.content.Context import android.media.MediaRoute2Info import android.os.PowerManager import android.util.Log +import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.WindowManager +import android.view.accessibility.AccessibilityManager import android.widget.TextView import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R @@ -53,6 +55,7 @@ class MediaTttChipControllerSender @Inject constructor( windowManager: WindowManager, viewUtil: ViewUtil, @Main mainExecutor: DelayableExecutor, + accessibilityManager: AccessibilityManager, tapGestureDetector: TapGestureDetector, powerManager: PowerManager, private val uiEventLogger: MediaTttSenderUiEventLogger @@ -62,10 +65,15 @@ class MediaTttChipControllerSender @Inject constructor( windowManager, viewUtil, mainExecutor, + accessibilityManager, tapGestureDetector, powerManager, R.layout.media_ttt_chip ) { + override val windowLayoutParams = commonWindowLayoutParams.apply { + gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) + } + private var currentlyDisplayedChipState: ChipStateSender? = null private val commandQueueCallbacks = object : CommandQueue.Callbacks { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index f1dd5ffd2077..8179d1763ea1 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -291,7 +291,8 @@ class BackPanelController private constructor( override fun onMotionEvent(event: MotionEvent) { backAnimation?.onBackMotion( - event, + event.x, + event.y, event.actionMasked, if (mView.isLeftPanel) BackEvent.EDGE_LEFT else BackEvent.EDGE_RIGHT ) diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index e210d685a370..3039d9d56c80 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -84,13 +84,16 @@ import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.EdgeBackGestureHandlerProto; import com.android.systemui.tracing.nano.SystemUiTraceProto; import com.android.wm.shell.back.BackAnimation; +import com.android.wm.shell.pip.Pip; import java.io.PrintWriter; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.Executor; +import java.util.function.Consumer; import javax.inject.Inject; @@ -147,16 +150,6 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mPackageName = "_UNKNOWN"; } } - - @Override - public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { - mIsInPipMode = true; - } - - @Override - public void onActivityUnpinned() { - mIsInPipMode = false; - } }; private DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener = @@ -188,6 +181,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private final ViewConfiguration mViewConfiguration; private final WindowManager mWindowManager; private final IWindowManager mWindowManagerService; + private final Optional<Pip> mPipOptional; private final FalsingManager mFalsingManager; // Activities which should not trigger Back gesture. private final List<ComponentName> mGestureBlockingActivities = new ArrayList<>(); @@ -218,6 +212,8 @@ public class EdgeBackGestureHandler extends CurrentUserTracker // We temporarily disable back gesture when user is quickswitching // between apps of different orientations private boolean mDisabledForQuickstep; + // This gets updated when the value of PipTransitionState#isInPip changes. + private boolean mIsInPip; private final PointF mDownPoint = new PointF(); private final PointF mEndPoint = new PointF(); @@ -233,7 +229,6 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private boolean mIsNavBarShownTransiently; private boolean mIsBackGestureAllowed; private boolean mGestureBlockingActivityRunning; - private boolean mIsInPipMode; private boolean mIsNewBackAffordanceEnabled; private InputMonitor mInputMonitor; @@ -302,6 +297,8 @@ public class EdgeBackGestureHandler extends CurrentUserTracker } }; + private final Consumer<Boolean> mOnIsInPipStateChangedListener = + (isInPip) -> mIsInPip = isInPip; EdgeBackGestureHandler( Context context, @@ -316,6 +313,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker ViewConfiguration viewConfiguration, WindowManager windowManager, IWindowManager windowManagerService, + Optional<Pip> pipOptional, FalsingManager falsingManager, LatencyTracker latencyTracker, FeatureFlags featureFlags) { @@ -332,6 +330,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mViewConfiguration = viewConfiguration; mWindowManager = windowManager; mWindowManagerService = windowManagerService; + mPipOptional = pipOptional; mFalsingManager = falsingManager; mLatencyTracker = latencyTracker; mFeatureFlags = featureFlags; @@ -491,6 +490,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mPluginManager.removePluginListener(this); TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener); + mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener(null)); try { mWindowManagerService.unregisterSystemGestureExclusionListener( @@ -508,6 +508,8 @@ public class EdgeBackGestureHandler extends CurrentUserTracker TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, mMainExecutor::execute, mOnPropertiesChangedListener); + mPipOptional.ifPresent( + pip -> pip.setOnIsInPipStateChangedListener(mOnIsInPipStateChangedListener)); try { mWindowManagerService.registerSystemGestureExclusionListener( @@ -680,7 +682,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private boolean isWithinTouchRegion(int x, int y) { // If the point is inside the PiP or Nav bar overlay excluded bounds, then ignore the back // gesture - final boolean isInsidePip = mIsInPipMode && mPipExcludedBounds.contains(x, y); + final boolean isInsidePip = mIsInPip && mPipExcludedBounds.contains(x, y); if (isInsidePip || mNavBarOverlayExcludedBounds.contains(x, y)) { return false; } @@ -933,7 +935,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker pw.println(" mInRejectedExclusion=" + mInRejectedExclusion); pw.println(" mExcludeRegion=" + mExcludeRegion); pw.println(" mUnrestrictedExcludeRegion=" + mUnrestrictedExcludeRegion); - pw.println(" mIsInPipMode=" + mIsInPipMode); + pw.println(" mIsInPip=" + mIsInPip); pw.println(" mPipExcludedBounds=" + mPipExcludedBounds); pw.println(" mNavBarOverlayExcludedBounds=" + mNavBarOverlayExcludedBounds); pw.println(" mEdgeWidthLeft=" + mEdgeWidthLeft); @@ -1002,6 +1004,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private final ViewConfiguration mViewConfiguration; private final WindowManager mWindowManager; private final IWindowManager mWindowManagerService; + private final Optional<Pip> mPipOptional; private final FalsingManager mFalsingManager; private final LatencyTracker mLatencyTracker; private final FeatureFlags mFeatureFlags; @@ -1018,6 +1021,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker ViewConfiguration viewConfiguration, WindowManager windowManager, IWindowManager windowManagerService, + Optional<Pip> pipOptional, FalsingManager falsingManager, LatencyTracker latencyTracker, FeatureFlags featureFlags) { @@ -1032,6 +1036,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mViewConfiguration = viewConfiguration; mWindowManager = windowManager; mWindowManagerService = windowManagerService; + mPipOptional = pipOptional; mFalsingManager = falsingManager; mLatencyTracker = latencyTracker; mFeatureFlags = featureFlags; @@ -1052,6 +1057,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mViewConfiguration, mWindowManager, mWindowManagerService, + mPipOptional, mFalsingManager, mLatencyTracker, mFeatureFlags); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java index a74c59618c95..eba9d3fdcab8 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java @@ -486,7 +486,7 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl public void onMotionEvent(MotionEvent event) { if (mBackAnimation != null) { mBackAnimation.onBackMotion( - event, + event.getX(), event.getY(), event.getActionMasked(), mIsLeftPanel ? BackEvent.EDGE_LEFT : BackEvent.EDGE_RIGHT); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 892c28306f46..0288c9fce64a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -342,7 +342,8 @@ class FgsManagerController @Inject constructor( } val addedPackages = runningServiceTokens.keys.filter { - it.uiControl != UIControl.HIDE_ENTRY && runningApps[it]?.stopped != true + currentProfileIds.contains(it.userId) && + it.uiControl != UIControl.HIDE_ENTRY && runningApps[it]?.stopped != true } val removedPackages = runningApps.keys.filter { !runningServiceTokens.containsKey(it) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index 5d2060d8043e..7b1ddd62ec6e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -139,12 +139,11 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { void updateResources(QSPanelController qsPanelController, QuickStatusBarHeaderController quickStatusBarHeaderController) { - int bottomPadding = getResources().getDimensionPixelSize(R.dimen.qs_panel_padding_bottom); mQSPanelContainer.setPaddingRelative( mQSPanelContainer.getPaddingStart(), QSUtils.getQsHeaderSystemIconsAreaHeight(mContext), mQSPanelContainer.getPaddingEnd(), - bottomPadding); + mQSPanelContainer.getPaddingBottom()); int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin); int horizontalPadding = getResources().getDimensionPixelSize( diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 41724ef62683..324c01959084 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -362,11 +362,11 @@ public class QSPanel extends LinearLayout implements Tunable { protected void updatePadding() { final Resources res = mContext.getResources(); int paddingTop = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top); - // Bottom padding only when there's a new footer with its height. + int paddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom); setPaddingRelative(getPaddingStart(), paddingTop, getPaddingEnd(), - getPaddingBottom()); + paddingBottom); } void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index fbdabc74aba4..a1c66b35bacc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -396,7 +396,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D } void saveTilesToSettings(List<String> tileSpecs) { - if (tileSpecs.contains("work")) Log.wtfStack(TAG, "Saving work tile"); mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs), null /* tag */, false /* default */, mCurrentUser, true /* overrideable by restore */); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 5b6e5ce95b14..c213f192291a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -142,7 +142,7 @@ public class ScreenshotController { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } }; diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt index d3ae198e8e35..236ba1f92d1f 100644 --- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt @@ -56,6 +56,8 @@ interface SmartspaceViewComponent { ): BcSmartspaceDataPlugin.SmartspaceView { val ssView = plugin.getView(parent) + // Currently, this is only used to provide SmartspaceView on Dream surface. + ssView.setIsDreaming(true) ssView.registerDataProvider(plugin) ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter { @@ -81,4 +83,4 @@ interface SmartspaceViewComponent { return ssView } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java index 6cfbb43fa25a..07455a02b2cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java @@ -66,7 +66,7 @@ public abstract class AlertingNotificationManager implements NotificationLifetim * @param entry entry to show */ public void showNotification(@NonNull NotificationEntry entry) { - mLogger.logShowNotification(entry.getKey()); + mLogger.logShowNotification(entry); addAlertEntry(entry); updateNotification(entry.getKey(), true /* alert */); entry.setInterruption(); @@ -320,7 +320,7 @@ public abstract class AlertingNotificationManager implements NotificationLifetim * @param updatePostTime whether or not to refresh the post time */ public void updateEntry(boolean updatePostTime) { - mLogger.logUpdateEntry(mEntry.getKey(), updatePostTime); + mLogger.logUpdateEntry(mEntry, updatePostTime); long currentTime = mClock.currentTimeMillis(); mEarliestRemovaltime = currentTime + mMinimumDisplayTime; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 86f9fa155179..9e029095ea6b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar; import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED; import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_MANAGEMENT_DISCLOSURE; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_NAMED_MANAGEMENT_DISCLOSURE; import static android.view.View.GONE; import static android.view.View.VISIBLE; @@ -407,7 +408,7 @@ public class KeyguardIndicationController { organizationName); } else { return mDevicePolicyManager.getResources().getString( - KEYGUARD_MANAGEMENT_DISCLOSURE, + KEYGUARD_NAMED_MANAGEMENT_DISCLOSURE, () -> packageResources.getString( R.string.do_disclosure_with_name, organizationName), organizationName); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt index 5df593b64c24..558bcac681d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt @@ -61,7 +61,7 @@ class WiredChargingRippleController @Inject constructor( private val systemClock: SystemClock, private val uiEventLogger: UiEventLogger ) { - private var pluggedIn: Boolean? = null + private var pluggedIn: Boolean = false private val rippleEnabled: Boolean = featureFlags.isEnabled(Flags.CHARGING_RIPPLE) && !SystemProperties.getBoolean("persist.debug.suppress-charging-ripple", false) private var normalizedPortPosX: Float = context.resources.getFloat( @@ -99,15 +99,17 @@ class WiredChargingRippleController @Inject constructor( nowPluggedIn: Boolean, charging: Boolean ) { - // Suppresses the ripple when the state change comes from wireless charging. - if (batteryController.isPluggedInWireless) { + // Suppresses the ripple when the state change comes from wireless charging or + // its dock. + if (batteryController.isPluggedInWireless || + batteryController.isChargingSourceDock) { return } - val wasPluggedIn = pluggedIn - pluggedIn = nowPluggedIn - if ((wasPluggedIn == null || !wasPluggedIn) && nowPluggedIn) { + + if (!pluggedIn && nowPluggedIn) { startRippleWithDebounce() } + pluggedIn = nowPluggedIn } } batteryController.addCallback(batteryStateChangeCallback) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java index a0ccd5726c75..1be4c04ef804 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java @@ -80,7 +80,7 @@ public class DynamicPrivacyController implements KeyguardStateController.Callbac @VisibleForTesting boolean isDynamicPrivacyEnabled() { - return !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic( + return mLockscreenUserManager.userAllowsNotificationsInPublic( mLockscreenUserManager.getCurrentUserId()); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt index 478f7aa8ce09..c4947ca2dd90 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -56,4 +56,7 @@ class NotifPipelineFlags @Inject constructor( fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE) && featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING) -}
\ No newline at end of file + + fun removeUnrankedNotifs(): Boolean = + featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt index 2397005a1a61..52dcf02e3a19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt @@ -17,18 +17,32 @@ package com.android.systemui.statusbar.notification import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel import com.android.systemui.log.LogLevel.DEBUG import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.LogLevel.WARNING +import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.util.Compile import javax.inject.Inject /** Logger for [NotificationEntryManager]. */ class NotificationEntryManagerLogger @Inject constructor( + notifPipelineFlags: NotifPipelineFlags, @NotificationLog private val buffer: LogBuffer ) { + private val devLoggingEnabled by lazy { notifPipelineFlags.isDevLoggingEnabled() } + + private inline fun devLog( + level: LogLevel, + initializer: LogMessage.() -> Unit, + noinline printer: LogMessage.() -> String + ) { + if (Compile.IS_DEBUG && devLoggingEnabled) buffer.log(TAG, level, initializer, printer) + } + fun logNotifAdded(key: String) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = key }, { "NOTIF ADDED $str1" @@ -36,7 +50,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logNotifUpdated(key: String) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = key }, { "NOTIF UPDATED $str1" @@ -44,7 +58,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logInflationAborted(key: String, status: String, reason: String) { - buffer.log(TAG, DEBUG, { + devLog(DEBUG, { str1 = key str2 = status str3 = reason @@ -54,7 +68,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logNotifInflated(key: String, isNew: Boolean) { - buffer.log(TAG, DEBUG, { + devLog(DEBUG, { str1 = key bool1 = isNew }, { @@ -63,7 +77,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logRemovalIntercepted(key: String) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = key }, { "NOTIF REMOVE INTERCEPTED for $str1" @@ -71,7 +85,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logLifetimeExtended(key: String, extenderName: String, status: String) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = key str2 = extenderName str3 = status @@ -81,7 +95,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logNotifRemoved(key: String, removedByUser: Boolean) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = key bool1 = removedByUser }, { @@ -90,7 +104,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logFilterAndSort(reason: String) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = reason }, { "FILTER AND SORT reason=$str1" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS index 63c37e9584d6..ed80f33be701 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS @@ -9,4 +9,6 @@ juliacr@google.com juliatuttle@google.com lynhan@google.com steell@google.com -yurilin@google.com
\ No newline at end of file +yurilin@google.com + +per-file MediaNotificationProcessor.java = ethibodeau@google.com, asc@google.com diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java index 792ff8d20b97..f6a572ec6ce6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection; +import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED; @@ -52,7 +53,7 @@ public class ListDumper { sb, true, includeRecordKeeping, - interactionTracker.hasUserInteractedWith(entry.getKey())); + interactionTracker.hasUserInteractedWith(logKey(entry))); if (entry instanceof GroupEntry) { GroupEntry ge = (GroupEntry) entry; NotificationEntry summary = ge.getSummary(); @@ -63,7 +64,7 @@ public class ListDumper { sb, true, includeRecordKeeping, - interactionTracker.hasUserInteractedWith(summary.getKey())); + interactionTracker.hasUserInteractedWith(logKey(summary))); } List<NotificationEntry> children = ge.getChildren(); for (int childIndex = 0; childIndex < children.size(); childIndex++) { @@ -74,7 +75,7 @@ public class ListDumper { sb, true, includeRecordKeeping, - interactionTracker.hasUserInteractedWith(child.getKey())); + interactionTracker.hasUserInteractedWith(logKey(child))); } } } @@ -116,11 +117,11 @@ public class ListDumper { sb.append(indent) .append("[").append(index).append("] ") .append(index.length() == 1 ? " " : "") - .append(entry.getKey()); + .append(logKey(entry)); if (includeParent) { sb.append(" (parent=") - .append(entry.getParent() != null ? entry.getParent().getKey() : null) + .append(logKey(entry.getParent())) .append(")"); NotificationEntry notifEntry = entry.getRepresentativeEntry(); @@ -185,8 +186,8 @@ public class ListDumper { if (notifEntry.getAttachState().getSuppressedChanges().getParent() != null) { rksb.append("suppressedParent=") - .append(notifEntry.getAttachState().getSuppressedChanges() - .getParent().getKey()) + .append(logKey(notifEntry.getAttachState().getSuppressedChanges() + .getParent())) .append(" "); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index ecce1ba25702..d99b5f9df12a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -89,6 +89,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.In import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLoggerKt; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; @@ -110,6 +111,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Queue; +import java.util.Set; import java.util.concurrent.TimeUnit; import javax.inject.Inject; @@ -157,6 +159,8 @@ public class NotifCollection implements Dumpable { private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>(); private final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>(); + private Set<String> mNotificationsWithoutRankings = Collections.emptySet(); + private Queue<NotifEvent> mEventQueue = new ArrayDeque<>(); private boolean mAttached = false; @@ -267,13 +271,14 @@ public class NotifCollection implements Dumpable { requireNonNull(stats); NotificationEntry storedEntry = mNotificationSet.get(entry.getKey()); if (storedEntry == null) { - mLogger.logNonExistentNotifDismissed(entry.getKey()); + mLogger.logNonExistentNotifDismissed(entry); continue; } if (entry != storedEntry) { throw mEulogizer.record( new IllegalStateException("Invalid entry: " - + "different stored and dismissed entries for " + entry.getKey())); + + "different stored and dismissed entries for " + logKey(entry) + + " stored=@" + Integer.toHexString(storedEntry.hashCode()))); } if (entry.getDismissState() == DISMISSED) { @@ -282,7 +287,7 @@ public class NotifCollection implements Dumpable { updateDismissInterceptors(entry); if (isDismissIntercepted(entry)) { - mLogger.logNotifDismissedIntercepted(entry.getKey()); + mLogger.logNotifDismissedIntercepted(entry); continue; } @@ -299,13 +304,13 @@ public class NotifCollection implements Dumpable { stats.notificationVisibility); } catch (RemoteException e) { // system process is dead if we're here. - mLogger.logRemoteExceptionOnNotificationClear(entry.getKey(), e); + mLogger.logRemoteExceptionOnNotificationClear(entry, e); } } } locallyDismissNotifications(entriesToLocallyDismiss); - dispatchEventsAndRebuildList(); + dispatchEventsAndRebuildList("dismissNotifications"); } /** @@ -342,14 +347,14 @@ public class NotifCollection implements Dumpable { // interceptors the chance to filter the notification updateDismissInterceptors(entry); if (isDismissIntercepted(entry)) { - mLogger.logNotifClearAllDismissalIntercepted(entry.getKey()); + mLogger.logNotifClearAllDismissalIntercepted(entry); } entries.remove(i); } } locallyDismissNotifications(entries); - dispatchEventsAndRebuildList(); + dispatchEventsAndRebuildList("dismissAllNotifications"); } /** @@ -363,7 +368,7 @@ public class NotifCollection implements Dumpable { NotificationEntry entry = entries.get(i); entry.setDismissState(DISMISSED); - mLogger.logNotifDismissed(entry.getKey()); + mLogger.logNotifDismissed(entry); if (isCanceled(entry)) { canceledEntries.add(entry); @@ -396,7 +401,7 @@ public class NotifCollection implements Dumpable { postNotification(sbn, requireRanking(rankingMap, sbn.getKey())); applyRanking(rankingMap); - dispatchEventsAndRebuildList(); + dispatchEventsAndRebuildList("onNotificationPosted"); } private void onNotificationGroupPosted(List<CoalescedEvent> batch) { @@ -407,7 +412,7 @@ public class NotifCollection implements Dumpable { for (CoalescedEvent event : batch) { postNotification(event.getSbn(), event.getRanking()); } - dispatchEventsAndRebuildList(); + dispatchEventsAndRebuildList("onNotificationGroupPosted"); } private void onNotificationRemoved( @@ -416,26 +421,26 @@ public class NotifCollection implements Dumpable { int reason) { Assert.isMainThread(); - mLogger.logNotifRemoved(sbn.getKey(), reason); + mLogger.logNotifRemoved(sbn, reason); final NotificationEntry entry = mNotificationSet.get(sbn.getKey()); if (entry == null) { // TODO (b/160008901): Throw an exception here - mLogger.logNoNotificationToRemoveWithKey(sbn.getKey(), reason); + mLogger.logNoNotificationToRemoveWithKey(sbn, reason); return; } entry.mCancellationReason = reason; tryRemoveNotification(entry); applyRanking(rankingMap); - dispatchEventsAndRebuildList(); + dispatchEventsAndRebuildList("onNotificationRemoved"); } private void onNotificationRankingUpdate(RankingMap rankingMap) { Assert.isMainThread(); mEventQueue.add(new RankingUpdatedEvent(rankingMap)); applyRanking(rankingMap); - dispatchEventsAndRebuildList(); + dispatchEventsAndRebuildList("onNotificationRankingUpdate"); } private void onNotificationChannelModified( @@ -445,7 +450,7 @@ public class NotifCollection implements Dumpable { int modificationType) { Assert.isMainThread(); mEventQueue.add(new ChannelChangedEvent(pkgName, user, channel, modificationType)); - dispatchEventsAndRebuildList(); + dispatchEventsAndRebuildList("onNotificationChannelModified"); } private void onNotificationsInitialized() { @@ -464,7 +469,7 @@ public class NotifCollection implements Dumpable { mEventQueue.add(new BindEntryEvent(entry, sbn)); mNotificationSet.put(sbn.getKey(), entry); - mLogger.logNotifPosted(sbn.getKey()); + mLogger.logNotifPosted(entry); mEventQueue.add(new EntryAddedEvent(entry)); } else { @@ -483,7 +488,7 @@ public class NotifCollection implements Dumpable { entry.setSbn(sbn); mEventQueue.add(new BindEntryEvent(entry, sbn)); - mLogger.logNotifUpdated(sbn.getKey()); + mLogger.logNotifUpdated(entry); mEventQueue.add(new EntryUpdatedEvent(entry, true /* fromSystem */)); } } @@ -498,12 +503,12 @@ public class NotifCollection implements Dumpable { if (mNotificationSet.get(entry.getKey()) != entry) { throw mEulogizer.record( new IllegalStateException("No notification to remove with key " - + entry.getKey())); + + logKey(entry))); } if (!isCanceled(entry)) { throw mEulogizer.record( - new IllegalStateException("Cannot remove notification " + entry.getKey() + new IllegalStateException("Cannot remove notification " + logKey(entry) + ": has not been marked for removal")); } @@ -514,7 +519,7 @@ public class NotifCollection implements Dumpable { } if (!isLifetimeExtended(entry)) { - mLogger.logNotifReleased(entry.getKey()); + mLogger.logNotifReleased(entry); mNotificationSet.remove(entry.getKey()); cancelDismissInterception(entry); mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason)); @@ -559,6 +564,7 @@ public class NotifCollection implements Dumpable { } private void applyRanking(@NonNull RankingMap rankingMap) { + ArrayMap<String, NotificationEntry> currentEntriesWithoutRankings = null; for (NotificationEntry entry : mNotificationSet.values()) { if (!isCanceled(entry)) { @@ -580,14 +586,31 @@ public class NotifCollection implements Dumpable { } } } else { - mLogger.logRankingMissing(entry.getKey(), rankingMap); + if (currentEntriesWithoutRankings == null) { + currentEntriesWithoutRankings = new ArrayMap<>(); + } + currentEntriesWithoutRankings.put(entry.getKey(), entry); } } } + NotifCollectionLoggerKt.maybeLogInconsistentRankings( + mLogger, + mNotificationsWithoutRankings, + currentEntriesWithoutRankings, + rankingMap + ); + mNotificationsWithoutRankings = currentEntriesWithoutRankings == null + ? Collections.emptySet() : currentEntriesWithoutRankings.keySet(); + if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) { + for (NotificationEntry entry : currentEntriesWithoutRankings.values()) { + entry.mCancellationReason = REASON_UNKNOWN; + tryRemoveNotification(entry); + } + } mEventQueue.add(new RankingAppliedEvent()); } - private void dispatchEventsAndRebuildList() { + private void dispatchEventsAndRebuildList(String reason) { Trace.beginSection("NotifCollection.dispatchEventsAndRebuildList"); mAmDispatchingToOtherCode = true; while (!mEventQueue.isEmpty()) { @@ -596,7 +619,7 @@ public class NotifCollection implements Dumpable { mAmDispatchingToOtherCode = false; if (mBuildListener != null) { - mBuildListener.onBuildList(mReadOnlyNotificationSet); + mBuildListener.onBuildList(mReadOnlyNotificationSet, reason); } Trace.endSection(); } @@ -627,14 +650,11 @@ public class NotifCollection implements Dumpable { extender.getName(), logKey, collectionEntryIs))); } - mLogger.logLifetimeExtensionEnded( - entry.getKey(), - extender, - entry.mLifetimeExtenders.size()); + mLogger.logLifetimeExtensionEnded(entry, extender, entry.mLifetimeExtenders.size()); if (!isLifetimeExtended(entry)) { if (tryRemoveNotification(entry)) { - dispatchEventsAndRebuildList(); + dispatchEventsAndRebuildList("onEndLifetimeExtension"); } } } @@ -657,7 +677,7 @@ public class NotifCollection implements Dumpable { mAmDispatchingToOtherCode = true; for (NotifLifetimeExtender extender : mLifetimeExtenders) { if (extender.maybeExtendLifetime(entry, entry.mCancellationReason)) { - mLogger.logLifetimeExtended(entry.getKey(), extender); + mLogger.logLifetimeExtended(entry, extender); entry.mLifetimeExtenders.add(extender); } } @@ -838,6 +858,11 @@ public class NotifCollection implements Dumpable { entries, true, "\t\t")); + + pw.println("\n\tmNotificationsWithoutRankings: " + mNotificationsWithoutRankings.size()); + for (String key : mNotificationsWithoutRankings) { + pw.println("\t * : " + key); + } } private final BatchableNotificationHandler mNotifHandler = new BatchableNotificationHandler() { @@ -916,21 +941,21 @@ public class NotifCollection implements Dumpable { // Make sure we have the notification to update NotificationEntry entry = mNotificationSet.get(sbn.getKey()); if (entry == null) { - mLogger.logNotifInternalUpdateFailed(sbn.getKey(), name, reason); + mLogger.logNotifInternalUpdateFailed(sbn, name, reason); return; } - mLogger.logNotifInternalUpdate(sbn.getKey(), name, reason); + mLogger.logNotifInternalUpdate(entry, name, reason); // First do the pieces of postNotification which are not about assuming the notification // was sent by the app entry.setSbn(sbn); mEventQueue.add(new BindEntryEvent(entry, sbn)); - mLogger.logNotifUpdated(sbn.getKey()); + mLogger.logNotifUpdated(entry); mEventQueue.add(new EntryUpdatedEvent(entry, false /* fromSystem */)); // Skip the applyRanking step and go straight to dispatching the events - dispatchEventsAndRebuildList(); + dispatchEventsAndRebuildList("updateNotificationInternally"); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index aedbd1b56622..0a16fb65b1ac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -175,6 +175,10 @@ public final class NotificationEntry extends ListEntry { public boolean mRemoteEditImeAnimatingAway; public boolean mRemoteEditImeVisible; private boolean mExpandAnimationRunning; + /** + * Flag to determine if the entry is blockable by DnD filters + */ + private boolean mBlockable; /** * @param sbn the StatusBarNotification from system server @@ -253,6 +257,7 @@ public final class NotificationEntry extends ListEntry { } mRanking = ranking.withAudiblyAlertedInfo(mRanking); + updateIsBlockable(); } /* @@ -781,15 +786,20 @@ public final class NotificationEntry extends ListEntry { * or is not in an allowList). */ public boolean isBlockable() { + return mBlockable; + } + + private void updateIsBlockable() { if (getChannel() == null) { - return false; + mBlockable = false; + return; } if (getChannel().isImportanceLockedByCriticalDeviceFunction() && !getChannel().isBlockable()) { - return false; + mBlockable = false; + return; } - - return true; + mBlockable = true; } private boolean shouldSuppressVisualEffect(int effect) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index df2fe4e8511f..93761f580dd4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -304,11 +304,11 @@ public class ShadeListBuilder implements Dumpable { private final CollectionReadyForBuildListener mReadyForBuildListener = new CollectionReadyForBuildListener() { @Override - public void onBuildList(Collection<NotificationEntry> entries) { + public void onBuildList(Collection<NotificationEntry> entries, String reason) { Assert.isMainThread(); mPipelineState.requireIsBefore(STATE_BUILD_STARTED); - mLogger.logOnBuildList(); + mLogger.logOnBuildList(reason); mAllEntries = entries; mChoreographer.schedule(); } @@ -456,7 +456,8 @@ public class ShadeListBuilder implements Dumpable { mLogger.logEndBuildList( mIterationCount, mReadOnlyNotifList.size(), - countChildren(mReadOnlyNotifList)); + countChildren(mReadOnlyNotifList), + /* enforcedVisualStability */ !mNotifStabilityManager.isEveryChangeAllowed()); if (mAlwaysLogList || mIterationCount % 10 == 0) { Trace.beginSection("ShadeListBuilder.logFinalList"); mLogger.logFinalList(mNotifList); @@ -579,11 +580,7 @@ public class ShadeListBuilder implements Dumpable { if (existingSummary == null) { group.setSummary(entry); } else { - mLogger.logDuplicateSummary( - mIterationCount, - group.getKey(), - existingSummary.getKey(), - entry.getKey()); + mLogger.logDuplicateSummary(mIterationCount, group, existingSummary, entry); // Use whichever one was posted most recently if (entry.getSbn().getPostTime() @@ -990,7 +987,7 @@ public class ShadeListBuilder implements Dumpable { // Check for suppressed order changes if (!getStabilityManager().isEveryChangeAllowed()) { mForceReorderable = true; - boolean isSorted = isSorted(mNotifList, mTopLevelComparator); + boolean isSorted = isShadeSorted(); mForceReorderable = false; if (!isSorted) { getStabilityManager().onEntryReorderSuppressed(); @@ -999,9 +996,23 @@ public class ShadeListBuilder implements Dumpable { Trace.endSection(); } + private boolean isShadeSorted() { + if (!isSorted(mNotifList, mTopLevelComparator)) { + return false; + } + for (ListEntry entry : mNotifList) { + if (entry instanceof GroupEntry) { + if (!isSorted(((GroupEntry) entry).getChildren(), mGroupChildrenComparator)) { + return false; + } + } + } + return true; + } + /** Determine whether the items in the list are sorted according to the comparator */ @VisibleForTesting - public static <T> boolean isSorted(List<T> items, Comparator<T> comparator) { + public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) { if (items.size() <= 1) { return true; } @@ -1070,7 +1081,7 @@ public class ShadeListBuilder implements Dumpable { if (!Objects.equals(curr, prev)) { mLogger.logEntryAttachStateChanged( mIterationCount, - entry.getKey(), + entry, prev.getParent(), curr.getParent()); @@ -1209,7 +1220,7 @@ public class ShadeListBuilder implements Dumpable { }; - private final Comparator<ListEntry> mGroupChildrenComparator = (o1, o2) -> { + private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { int index1 = canReorder(o1) ? -1 : o1.getPreviousAttachState().getStableIndex(); int index2 = canReorder(o2) ? -1 : o2.getPreviousAttachState().getStableIndex(); int cmp = Integer.compare(index1, index2); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java index ff1c70ca740b..ef63be0633bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java @@ -16,15 +16,15 @@ package com.android.systemui.statusbar.notification.collection.coordinator; -import com.android.keyguard.KeyguardUpdateMonitor; +import androidx.annotation.NonNull; + import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.notification.SectionHeaderVisibilityProvider; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; -import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; +import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider; import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider; import javax.inject.Inject; @@ -36,27 +36,21 @@ import javax.inject.Inject; @CoordinatorScope public class KeyguardCoordinator implements Coordinator { private static final String TAG = "KeyguardCoordinator"; - private final StatusBarStateController mStatusBarStateController; - private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private final HighPriorityProvider mHighPriorityProvider; - private final SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider; private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; + private final SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider; private final SharedCoordinatorLogger mLogger; + private final StatusBarStateController mStatusBarStateController; @Inject public KeyguardCoordinator( - StatusBarStateController statusBarStateController, - KeyguardUpdateMonitor keyguardUpdateMonitor, - HighPriorityProvider highPriorityProvider, - SectionHeaderVisibilityProvider sectionHeaderVisibilityProvider, KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, - SharedCoordinatorLogger logger) { - mStatusBarStateController = statusBarStateController; - mKeyguardUpdateMonitor = keyguardUpdateMonitor; - mHighPriorityProvider = highPriorityProvider; - mSectionHeaderVisibilityProvider = sectionHeaderVisibilityProvider; + SectionHeaderVisibilityProvider sectionHeaderVisibilityProvider, + SharedCoordinatorLogger logger, + StatusBarStateController statusBarStateController) { mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider; + mSectionHeaderVisibilityProvider = sectionHeaderVisibilityProvider; mLogger = logger; + mStatusBarStateController = statusBarStateController; } @Override @@ -72,7 +66,7 @@ public class KeyguardCoordinator implements Coordinator { private final NotifFilter mNotifFilter = new NotifFilter(TAG) { @Override - public boolean shouldFilterOut(NotificationEntry entry, long now) { + public boolean shouldFilterOut(@NonNull NotificationEntry entry, long now) { return mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry); } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 8f37bafa45e6..023c4ef2b8b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -360,13 +360,13 @@ public class PreparationCoordinator implements Coordinator { } private void abortInflation(NotificationEntry entry, String reason) { - mLogger.logInflationAborted(entry.getKey(), reason); + mLogger.logInflationAborted(entry, reason); mNotifInflater.abortInflation(entry); mInflatingNotifs.remove(entry); } private void onInflationFinished(NotificationEntry entry, NotifViewController controller) { - mLogger.logNotifInflated(entry.getKey()); + mLogger.logNotifInflated(entry); mInflatingNotifs.remove(entry); mViewBarn.registerViewForEntry(entry, controller); mInflationStates.put(entry, STATE_INFLATED); @@ -398,20 +398,20 @@ public class PreparationCoordinator implements Coordinator { return false; } if (isBeyondGroupInitializationWindow(group, now)) { - mLogger.logGroupInflationTookTooLong(group.getKey()); + mLogger.logGroupInflationTookTooLong(group); return false; } if (mInflatingNotifs.contains(group.getSummary())) { - mLogger.logDelayingGroupRelease(group.getKey(), group.getSummary().getKey()); + mLogger.logDelayingGroupRelease(group, group.getSummary()); return true; } for (NotificationEntry child : group.getChildren()) { if (mInflatingNotifs.contains(child) && !child.wasAttachedInPreviousPass()) { - mLogger.logDelayingGroupRelease(group.getKey(), child.getKey()); + mLogger.logDelayingGroupRelease(group, child); return true; } } - mLogger.logDoneWaitingForGroupInflation(group.getKey()); + mLogger.logDoneWaitingForGroupInflation(group); return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt index f8352500923e..30f13152126c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt @@ -19,48 +19,51 @@ 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.NotificationLog +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject class PreparationCoordinatorLogger @Inject constructor( @NotificationLog private val buffer: LogBuffer ) { - fun logNotifInflated(key: String) { + fun logNotifInflated(entry: NotificationEntry) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = key + str1 = entry.logKey }, { "NOTIF INFLATED $str1" }) } - fun logInflationAborted(key: String, reason: String) { + fun logInflationAborted(entry: NotificationEntry, reason: String) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = key + str1 = entry.logKey str2 = reason }, { "NOTIF INFLATION ABORTED $str1 reason=$str2" }) } - fun logDoneWaitingForGroupInflation(groupKey: String) { + fun logDoneWaitingForGroupInflation(group: GroupEntry) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = groupKey + str1 = group.logKey }, { "Finished inflating all members of group $str1, releasing group" }) } - fun logGroupInflationTookTooLong(groupKey: String) { + fun logGroupInflationTookTooLong(group: GroupEntry) { buffer.log(TAG, LogLevel.WARNING, { - str1 = groupKey + str1 = group.logKey }, { "Group inflation took too long for $str1, releasing children early" }) } - fun logDelayingGroupRelease(groupKey: String, childKey: String) { + fun logDelayingGroupRelease(group: GroupEntry, child: NotificationEntry) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = groupKey - str2 = childKey + str1 = group.logKey + str2 = child.logKey }, { "Delaying release of group $str1 because child $str2 is still inflating" }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java index 57fd1975e13a..5ac481341d43 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -20,7 +20,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.notification.SectionClassifier; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -28,6 +27,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; +import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider; import com.android.systemui.statusbar.notification.collection.render.NodeController; import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import com.android.systemui.statusbar.notification.dagger.AlertingHeader; @@ -51,7 +51,7 @@ public class RankingCoordinator implements Coordinator { public static final boolean SHOW_ALL_SECTIONS = false; private final StatusBarStateController mStatusBarStateController; private final HighPriorityProvider mHighPriorityProvider; - private final SectionClassifier mSectionClassifier; + private final SectionStyleProvider mSectionStyleProvider; private final NodeController mSilentNodeController; private final SectionHeaderController mSilentHeaderController; private final NodeController mAlertingHeaderController; @@ -62,13 +62,13 @@ public class RankingCoordinator implements Coordinator { public RankingCoordinator( StatusBarStateController statusBarStateController, HighPriorityProvider highPriorityProvider, - SectionClassifier sectionClassifier, + SectionStyleProvider sectionStyleProvider, @AlertingHeader NodeController alertingHeaderController, @SilentHeader SectionHeaderController silentHeaderController, @SilentHeader NodeController silentNodeController) { mStatusBarStateController = statusBarStateController; mHighPriorityProvider = highPriorityProvider; - mSectionClassifier = sectionClassifier; + mSectionStyleProvider = sectionStyleProvider; mAlertingHeaderController = alertingHeaderController; mSilentNodeController = silentNodeController; mSilentHeaderController = silentHeaderController; @@ -77,7 +77,7 @@ public class RankingCoordinator implements Coordinator { @Override public void attach(NotifPipeline pipeline) { mStatusBarStateController.addCallback(mStatusBarStateCallback); - mSectionClassifier.setMinimizedSections(Collections.singleton(mMinimizedNotifSectioner)); + mSectionStyleProvider.setMinimizedSections(Collections.singleton(mMinimizedNotifSectioner)); pipeline.addPreGroupFilter(mSuspendedFilter); pipeline.addPreGroupFilter(mDndVisualEffectsFilter); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt index 4e9d3ac07a96..1494574b26f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt @@ -19,11 +19,11 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.content.Context import com.android.systemui.R import com.android.systemui.statusbar.notification.AssistantFeedbackController -import com.android.systemui.statusbar.notification.SectionClassifier import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider import com.android.systemui.statusbar.notification.collection.render.NotifRowController import javax.inject.Inject @@ -35,7 +35,7 @@ import javax.inject.Inject class RowAppearanceCoordinator @Inject internal constructor( context: Context, private var mAssistantFeedbackController: AssistantFeedbackController, - private var mSectionClassifier: SectionClassifier + private var mSectionStyleProvider: SectionStyleProvider ) : Coordinator { private var entryToExpand: NotificationEntry? = null @@ -55,7 +55,7 @@ class RowAppearanceCoordinator @Inject internal constructor( private fun onBeforeRenderList(list: List<ListEntry>) { entryToExpand = list.firstOrNull()?.representativeEntry?.takeIf { entry -> - !mSectionClassifier.isMinimizedSection(entry.section!!) + !mSectionStyleProvider.isMinimizedSection(entry.section!!) } } @@ -68,4 +68,4 @@ class RowAppearanceCoordinator @Inject internal constructor( // Show the "alerted" bell icon controller.setLastAudiblyAlertedMs(entry.lastAudiblyAlertedMs) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt index 3475fcf7c12f..ee0b00807e27 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt @@ -31,6 +31,7 @@ class NotifUiAdjustment internal constructor( val smartActions: List<Notification.Action>, val smartReplies: List<CharSequence>, val isConversation: Boolean, + val isSnoozeEnabled: Boolean, val isMinimized: Boolean, val needsRedaction: Boolean, ) { @@ -42,6 +43,7 @@ class NotifUiAdjustment internal constructor( ): Boolean = when { oldAdjustment === newAdjustment -> false oldAdjustment.isConversation != newAdjustment.isConversation -> true + oldAdjustment.isSnoozeEnabled != newAdjustment.isSnoozeEnabled -> true oldAdjustment.isMinimized != newAdjustment.isMinimized -> true oldAdjustment.needsRedaction != newAdjustment.needsRedaction -> true areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions) -> true @@ -57,9 +59,9 @@ class NotifUiAdjustment internal constructor( first.size != second.size -> true else -> first.asSequence().zip(second.asSequence()).any { (!TextUtils.equals(it.first.title, it.second.title)) || - (areDifferent(it.first.getIcon(), it.second.getIcon())) || - (it.first.actionIntent != it.second.actionIntent) || - (areDifferent(it.first.remoteInputs, it.second.remoteInputs)) + (areDifferent(it.first.getIcon(), it.second.getIcon())) || + (it.first.actionIntent != it.second.actionIntent) || + (areDifferent(it.first.remoteInputs, it.second.remoteInputs)) } } @@ -78,7 +80,7 @@ class NotifUiAdjustment internal constructor( first.size != second.size -> true else -> first.asSequence().zip(second.asSequence()).any { (!TextUtils.equals(it.first.label, it.second.label)) || - (areDifferent(it.first.choices, it.second.choices)) + (areDifferent(it.first.choices, it.second.choices)) } } @@ -94,4 +96,4 @@ class NotifUiAdjustment internal constructor( } } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt index f7b6376c717c..745d6fe1d624 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt @@ -16,12 +16,18 @@ package com.android.systemui.statusbar.notification.collection.inflation +import android.database.ContentObserver +import android.os.Handler +import android.os.UserHandle +import android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.NotificationLockscreenUserManager -import com.android.systemui.statusbar.notification.SectionClassifier import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider import com.android.systemui.util.ListenerSet +import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject /** @@ -30,14 +36,23 @@ import javax.inject.Inject */ @SysUISingleton class NotifUiAdjustmentProvider @Inject constructor( + @Main private val handler: Handler, + private val secureSettings: SecureSettings, private val lockscreenUserManager: NotificationLockscreenUserManager, - private val sectionClassifier: SectionClassifier, + private val sectionStyleProvider: SectionStyleProvider ) { private val dirtyListeners = ListenerSet<Runnable>() + private var isSnoozeEnabled = false fun addDirtyListener(listener: Runnable) { if (dirtyListeners.isEmpty()) { lockscreenUserManager.addNotificationStateChangedListener(notifStateChangedListener) + updateSnoozeEnabled() + secureSettings.registerContentObserverForUser( + SHOW_NOTIFICATION_SNOOZE, + settingsObserver, + UserHandle.USER_ALL + ) } dirtyListeners.addIfAbsent(listener) } @@ -46,6 +61,7 @@ class NotifUiAdjustmentProvider @Inject constructor( dirtyListeners.remove(listener) if (dirtyListeners.isEmpty()) { lockscreenUserManager.removeNotificationStateChangedListener(notifStateChangedListener) + secureSettings.unregisterContentObserver(settingsObserver) } } @@ -54,10 +70,21 @@ class NotifUiAdjustmentProvider @Inject constructor( dirtyListeners.forEach(Runnable::run) } + private val settingsObserver = object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean) { + updateSnoozeEnabled() + dirtyListeners.forEach(Runnable::run) + } + } + + private fun updateSnoozeEnabled() { + isSnoozeEnabled = secureSettings.getInt(SHOW_NOTIFICATION_SNOOZE, 0) == 1 + } + private fun isEntryMinimized(entry: NotificationEntry): Boolean { val section = entry.section ?: error("Entry must have a section to determine if minimized") val parent = entry.parent ?: error("Entry must have a parent to determine if minimized") - val isMinimizedSection = sectionClassifier.isMinimizedSection(section) + val isMinimizedSection = sectionStyleProvider.isMinimizedSection(section) val isTopLevelEntry = parent == GroupEntry.ROOT_ENTRY val isGroupSummary = parent.summary == entry return isMinimizedSection && (isTopLevelEntry || isGroupSummary) @@ -73,7 +100,8 @@ class NotifUiAdjustmentProvider @Inject constructor( smartActions = entry.ranking.smartActions, smartReplies = entry.ranking.smartReplies, isConversation = entry.ranking.isConversation, + isSnoozeEnabled = isSnoozeEnabled, isMinimized = isEntryMinimized(entry), needsRedaction = lockscreenUserManager.needsRedaction(entry), ) -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt index f8bf85f92eae..10a627d65b83 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt @@ -21,29 +21,42 @@ import com.android.systemui.log.LogLevel.DEBUG import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter +import com.android.systemui.statusbar.notification.logKey +import com.android.systemui.util.Compile import javax.inject.Inject class ShadeListBuilderLogger @Inject constructor( + notifPipelineFlags: NotifPipelineFlags, @NotificationLog private val buffer: LogBuffer ) { - fun logOnBuildList() { + fun logOnBuildList(reason: String?) { buffer.log(TAG, INFO, { + str1 = reason }, { - "Request received from NotifCollection" + "Request received from NotifCollection for $str1" }) } - fun logEndBuildList(buildId: Int, topLevelEntries: Int, numChildren: Int) { + fun logEndBuildList( + buildId: Int, + topLevelEntries: Int, + numChildren: Int, + enforcedVisualStability: Boolean + ) { buffer.log(TAG, INFO, { long1 = buildId.toLong() int1 = topLevelEntries int2 = numChildren + bool1 = enforcedVisualStability }, { - "(Build $long1) Build complete ($int1 top-level entries, $int2 children)" + "(Build $long1) Build complete ($int1 top-level entries, $int2 children)" + + " enforcedVisualStability=$bool1" }) } @@ -110,12 +123,17 @@ class ShadeListBuilderLogger @Inject constructor( }) } - fun logDuplicateSummary(buildId: Int, groupKey: String, existingKey: String, newKey: String) { + fun logDuplicateSummary( + buildId: Int, + group: GroupEntry, + existingSummary: NotificationEntry, + newSummary: NotificationEntry + ) { buffer.log(TAG, WARNING, { long1 = buildId.toLong() - str1 = groupKey - str2 = existingKey - str3 = newKey + str1 = group.logKey + str2 = existingSummary.logKey + str3 = newSummary.logKey }, { """(Build $long1) Duplicate summary for group "$str1": "$str2" vs. "$str3"""" }) @@ -124,7 +142,7 @@ class ShadeListBuilderLogger @Inject constructor( fun logDuplicateTopLevelKey(buildId: Int, topLevelKey: String) { buffer.log(TAG, WARNING, { long1 = buildId.toLong() - str1 = topLevelKey + str1 = logKey(topLevelKey) }, { "(Build $long1) Duplicate top-level key: $str1" }) @@ -132,15 +150,15 @@ class ShadeListBuilderLogger @Inject constructor( fun logEntryAttachStateChanged( buildId: Int, - key: String, + entry: ListEntry, prevParent: GroupEntry?, newParent: GroupEntry? ) { buffer.log(TAG, INFO, { long1 = buildId.toLong() - str1 = key - str2 = prevParent?.key - str3 = newParent?.key + str1 = entry.logKey + str2 = prevParent?.logKey + str3 = newParent?.logKey }, { val action = if (str2 == null && str3 != null) { @@ -160,8 +178,8 @@ class ShadeListBuilderLogger @Inject constructor( fun logParentChanged(buildId: Int, prevParent: GroupEntry?, newParent: GroupEntry?) { buffer.log(TAG, INFO, { long1 = buildId.toLong() - str1 = prevParent?.key - str2 = newParent?.key + str1 = prevParent?.logKey + str2 = newParent?.logKey }, { if (str1 == null && str2 != null) { "(Build $long1) Parent is {$str2}" @@ -180,8 +198,8 @@ class ShadeListBuilderLogger @Inject constructor( ) { buffer.log(TAG, INFO, { long1 = buildId.toLong() - str1 = suppressedParent?.key - str2 = keepingParent?.key + str1 = suppressedParent?.logKey + str2 = keepingParent?.logKey }, { "(Build $long1) Change of parent to '$str1' suppressed; keeping parent '$str2'" }) @@ -193,7 +211,7 @@ class ShadeListBuilderLogger @Inject constructor( ) { buffer.log(TAG, INFO, { long1 = buildId.toLong() - str1 = keepingParent?.key + str1 = keepingParent?.logKey }, { "(Build $long1) Group pruning suppressed; keeping parent '$str1'" }) @@ -273,6 +291,8 @@ class ShadeListBuilderLogger @Inject constructor( }) } + val logRankInFinalList = Compile.IS_DEBUG && notifPipelineFlags.isDevLoggingEnabled() + fun logFinalList(entries: List<ListEntry>) { if (entries.isEmpty()) { buffer.log(TAG, DEBUG, {}, { "(empty list)" }) @@ -281,26 +301,32 @@ class ShadeListBuilderLogger @Inject constructor( val entry = entries[i] buffer.log(TAG, DEBUG, { int1 = i - str1 = entry.key + str1 = entry.logKey + bool1 = logRankInFinalList + int2 = entry.representativeEntry!!.ranking.rank }, { - "[$int1] $str1" + "[$int1] $str1".let { if (bool1) "$it rank=$int2" else it } }) if (entry is GroupEntry) { entry.summary?.let { buffer.log(TAG, DEBUG, { - str1 = it.key + str1 = it.logKey + bool1 = logRankInFinalList + int2 = it.ranking.rank }, { - " [*] $str1 (summary)" + " [*] $str1 (summary)".let { if (bool1) "$it rank=$int2" else it } }) } for (j in entry.children.indices) { val child = entry.children[j] buffer.log(TAG, DEBUG, { int1 = j - str1 = child.key + str1 = child.logKey + bool1 = logRankInFinalList + int2 = child.ranking.rank }, { - " [$int1] $str1" + " [$int1] $str1".let { if (bool1) "$it rank=$int2" else it } }) } } @@ -308,7 +334,7 @@ class ShadeListBuilderLogger @Inject constructor( } fun logPipelineRunSuppressed() = - buffer.log(TAG, INFO, {}) { "Suppressing pipeline run during animation." } + buffer.log(TAG, INFO, {}) { "Suppressing pipeline run during animation." } } -private const val TAG = "ShadeListBuilder"
\ No newline at end of file +private const val TAG = "ShadeListBuilder" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/CollectionReadyForBuildListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/CollectionReadyForBuildListener.java index 4023474bf6a7..941b2ae4f771 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/CollectionReadyForBuildListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/CollectionReadyForBuildListener.java @@ -29,5 +29,5 @@ public interface CollectionReadyForBuildListener { * Called by the NotifCollection to indicate that something in the collection has changed and * that the list builder should regenerate the list. */ - void onBuildList(Collection<NotificationEntry> entries); + void onBuildList(Collection<NotificationEntry> entries, String reason); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt index ac0b1ee6c442..ebcac6b6afb3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.notifcollection import android.os.RemoteException import android.service.notification.NotificationListenerService import android.service.notification.NotificationListenerService.RankingMap +import android.service.notification.StatusBarNotification import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel.DEBUG import com.android.systemui.log.LogLevel.ERROR @@ -65,9 +66,9 @@ fun cancellationReasonDebugString(@CancellationReason reason: Int) = class NotifCollectionLogger @Inject constructor( @NotificationLog private val buffer: LogBuffer ) { - fun logNotifPosted(key: String) { + fun logNotifPosted(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "POSTED $str1" }) @@ -75,49 +76,49 @@ class NotifCollectionLogger @Inject constructor( fun logNotifGroupPosted(groupKey: String, batchSize: Int) { buffer.log(TAG, INFO, { - str1 = groupKey + str1 = logKey(groupKey) int1 = batchSize }, { "POSTED GROUP $str1 ($int1 events)" }) } - fun logNotifUpdated(key: String) { + fun logNotifUpdated(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "UPDATED $str1" }) } - fun logNotifRemoved(key: String, @CancellationReason reason: Int) { + fun logNotifRemoved(sbn: StatusBarNotification, @CancellationReason reason: Int) { buffer.log(TAG, INFO, { - str1 = key + str1 = sbn.logKey int1 = reason }, { "REMOVED $str1 reason=${cancellationReasonDebugString(int1)}" }) } - fun logNotifReleased(key: String) { + fun logNotifReleased(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "RELEASED $str1" }) } - fun logNotifDismissed(key: String) { + fun logNotifDismissed(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "DISMISSED $str1" }) } - fun logNonExistentNotifDismissed(key: String) { + fun logNonExistentNotifDismissed(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "DISMISSED Non Existent $str1" }) @@ -125,7 +126,7 @@ class NotifCollectionLogger @Inject constructor( fun logChildDismissed(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = entry.key + str1 = entry.logKey }, { "CHILD DISMISSED (inferred): $str1" }) @@ -141,31 +142,31 @@ class NotifCollectionLogger @Inject constructor( fun logDismissOnAlreadyCanceledEntry(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = entry.key + str1 = entry.logKey }, { "Dismiss on $str1, which was already canceled. Trying to remove..." }) } - fun logNotifDismissedIntercepted(key: String) { + fun logNotifDismissedIntercepted(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "DISMISS INTERCEPTED $str1" }) } - fun logNotifClearAllDismissalIntercepted(key: String) { + fun logNotifClearAllDismissalIntercepted(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "CLEAR ALL DISMISSAL INTERCEPTED $str1" }) } - fun logNotifInternalUpdate(key: String, name: String, reason: String) { + fun logNotifInternalUpdate(entry: NotificationEntry, name: String, reason: String) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey str2 = name str3 = reason }, { @@ -173,9 +174,9 @@ class NotifCollectionLogger @Inject constructor( }) } - fun logNotifInternalUpdateFailed(key: String, name: String, reason: String) { + fun logNotifInternalUpdateFailed(sbn: StatusBarNotification, name: String, reason: String) { buffer.log(TAG, INFO, { - str1 = key + str1 = sbn.logKey str2 = name str3 = reason }, { @@ -183,26 +184,49 @@ class NotifCollectionLogger @Inject constructor( }) } - fun logNoNotificationToRemoveWithKey(key: String, @CancellationReason reason: Int) { + fun logNoNotificationToRemoveWithKey( + sbn: StatusBarNotification, + @CancellationReason reason: Int + ) { buffer.log(TAG, ERROR, { - str1 = key + str1 = sbn.logKey int1 = reason }, { "No notification to remove with key $str1 reason=${cancellationReasonDebugString(int1)}" }) } - fun logRankingMissing(key: String, rankingMap: RankingMap) { - buffer.log(TAG, WARNING, { str1 = key }, { "Ranking update is missing ranking for $str1" }) - buffer.log(TAG, DEBUG, {}, { "Ranking map contents:" }) - for (entry in rankingMap.orderedKeys) { - buffer.log(TAG, DEBUG, { str1 = entry }, { " $str1" }) - } + fun logMissingRankings( + newlyInconsistentEntries: List<NotificationEntry>, + totalInconsistent: Int, + rankingMap: RankingMap + ) { + buffer.log(TAG, WARNING, { + int1 = totalInconsistent + int2 = newlyInconsistentEntries.size + str1 = newlyInconsistentEntries.joinToString { it.logKey ?: "null" } + }, { + "Ranking update is missing ranking for $int1 entries ($int2 new): $str1" + }) + buffer.log(TAG, DEBUG, { + str1 = rankingMap.orderedKeys.map { logKey(it) ?: "null" }.toString() + }, { + "Ranking map contents: $str1" + }) + } + + fun logRecoveredRankings(newlyConsistentKeys: List<String>) { + buffer.log(TAG, INFO, { + int1 = newlyConsistentKeys.size + str1 = newlyConsistentKeys.joinToString { logKey(it) ?: "null" } + }, { + "Ranking update now contains rankings for $int1 previously inconsistent entries: $str1" + }) } - fun logRemoteExceptionOnNotificationClear(key: String, e: RemoteException) { + fun logRemoteExceptionOnNotificationClear(entry: NotificationEntry, e: RemoteException) { buffer.log(TAG, WTF, { - str1 = key + str1 = entry.logKey str2 = e.toString() }, { "RemoteException while attempting to clear $str1:\n$str2" @@ -217,9 +241,9 @@ class NotifCollectionLogger @Inject constructor( }) } - fun logLifetimeExtended(key: String, extender: NotifLifetimeExtender) { + fun logLifetimeExtended(entry: NotificationEntry, extender: NotifLifetimeExtender) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey str2 = extender.name }, { "LIFETIME EXTENDED: $str1 by $str2" @@ -227,12 +251,12 @@ class NotifCollectionLogger @Inject constructor( } fun logLifetimeExtensionEnded( - key: String, + entry: NotificationEntry, extender: NotifLifetimeExtender, totalExtenders: Int ) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey str2 = extender.name int1 = totalExtenders }, { @@ -338,4 +362,29 @@ class NotifCollectionLogger @Inject constructor( } } +fun maybeLogInconsistentRankings( + logger: NotifCollectionLogger, + oldKeysWithoutRankings: Set<String>, + newEntriesWithoutRankings: Map<String, NotificationEntry>?, + rankingMap: RankingMap +) { + if (oldKeysWithoutRankings.isEmpty() && newEntriesWithoutRankings == null) return + val newlyConsistent: List<String> = oldKeysWithoutRankings + .mapNotNull { key -> + key.takeIf { key !in (newEntriesWithoutRankings ?: emptyMap()) } + .takeIf { key in rankingMap.orderedKeys } + }.sorted() + if (newlyConsistent.isNotEmpty()) { + logger.logRecoveredRankings(newlyConsistent) + } + val newlyInconsistent: List<NotificationEntry> = newEntriesWithoutRankings + ?.mapNotNull { (key, entry) -> + entry.takeIf { key !in oldKeysWithoutRankings } + }?.sortedBy { it.key } ?: emptyList() + if (newlyInconsistent.isNotEmpty()) { + val totalInconsistent: Int = newEntriesWithoutRankings?.size ?: 0 + logger.logMissingRankings(newlyInconsistent, totalInconsistent, rankingMap) + } +} + private const val TAG = "NotifCollection" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/SectionHeaderVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt index 68bdd18c9881..82c7aae08f6a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/SectionHeaderVisibilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification +package com.android.systemui.statusbar.notification.collection.provider import android.content.Context -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject /** @@ -34,7 +34,7 @@ import javax.inject.Inject class SectionHeaderVisibilityProvider @Inject constructor( context: Context ) { - var neverShowSectionHeaders = context.resources.getBoolean(R.bool.config_notification_never_show_section_headers) - private set + val neverShowSectionHeaders = + context.resources.getBoolean(R.bool.config_notification_never_show_section_headers) var sectionHeadersVisible = true } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/SectionClassifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt index 1f2d0fe6c46e..7b9483022fd8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/SectionClassifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification +package com.android.systemui.statusbar.notification.collection.provider import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection @@ -26,7 +26,7 @@ import javax.inject.Inject * NOTE: This class exists to avoid putting metadata like "isMinimized" on the NotifSection */ @SysUISingleton -class SectionClassifier @Inject constructor() { +class SectionStyleProvider @Inject constructor() { private lateinit var lowPrioritySections: Set<NotifSectioner> /** @@ -43,4 +43,4 @@ class SectionClassifier @Inject constructor() { fun isMinimizedSection(section: NotifSection): Boolean { return lowPrioritySections.contains(section.sectioner) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt index 8be710c8842c..d234e54e6725 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt @@ -16,12 +16,12 @@ package com.android.systemui.statusbar.notification.collection.render -import com.android.systemui.statusbar.notification.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection +import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider import com.android.systemui.util.Compile import com.android.systemui.util.traceSection @@ -107,4 +107,4 @@ class NodeSpecBuilder( .apply { entry.children.forEach { children.add(buildNotifNode(this, it)) } } else -> throw RuntimeException("Unexpected entry: $entry") } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt index 3501b44a2c2e..38e3d496a60c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt @@ -19,20 +19,24 @@ package com.android.systemui.statusbar.notification.collection.render import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.util.Compile import javax.inject.Inject class NodeSpecBuilderLogger @Inject constructor( + notifPipelineFlags: NotifPipelineFlags, @NotificationLog private val buffer: LogBuffer ) { + private val devLoggingEnabled by lazy { notifPipelineFlags.isDevLoggingEnabled() } + fun logBuildNodeSpec( oldSections: Set<NotifSection?>, newHeaders: Map<NotifSection?, NodeController?>, newCounts: Map<NotifSection?, Int>, newSectionOrder: List<NotifSection?> ) { - if (!Compile.IS_DEBUG) + if (!(Compile.IS_DEBUG && devLoggingEnabled)) return buffer.log(TAG, LogLevel.DEBUG, { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt index 6ed81078c3a4..51dc72848d9e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt @@ -19,10 +19,10 @@ package com.android.systemui.statusbar.notification.collection.render import android.content.Context import android.view.View import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager -import com.android.systemui.statusbar.notification.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.util.traceSection import dagger.assisted.Assisted diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java index 19cf9dc135b1..5ef2b9e55d9e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java @@ -83,7 +83,7 @@ public class HeadsUpViewBinder { params.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp); params.requireContentViews(FLAG_CONTENT_VIEW_HEADS_UP); CancellationSignal signal = mStage.requestRebind(entry, en -> { - mLogger.entryBoundSuccessfully(entry.getKey()); + mLogger.entryBoundSuccessfully(entry); en.getRow().setUsesIncreasedHeadsUpHeight(params.useIncreasedHeadsUpHeight()); // requestRebing promises that if we called cancel before this callback would be // invoked, then we will not enter this callback, and because we always cancel before @@ -94,7 +94,7 @@ public class HeadsUpViewBinder { } }); abortBindCallback(entry); - mLogger.startBindingHun(entry.getKey()); + mLogger.startBindingHun(entry); mOngoingBindCallbacks.put(entry, signal); } @@ -105,7 +105,7 @@ public class HeadsUpViewBinder { public void abortBindCallback(NotificationEntry entry) { CancellationSignal ongoingBindCallback = mOngoingBindCallbacks.remove(entry); if (ongoingBindCallback != null) { - mLogger.currentOngoingBindingAborted(entry.getKey()); + mLogger.currentOngoingBindingAborted(entry); ongoingBindCallback.cancel(); } } @@ -116,7 +116,7 @@ public class HeadsUpViewBinder { public void unbindHeadsUpView(NotificationEntry entry) { abortBindCallback(entry); mStage.getStageParams(entry).markContentViewsFreeable(FLAG_CONTENT_VIEW_HEADS_UP); - mLogger.entryContentViewMarkedFreeable(entry.getKey()); - mStage.requestRebind(entry, e -> mLogger.entryUnbound(e.getKey())); + mLogger.entryContentViewMarkedFreeable(entry); + mStage.requestRebind(entry, e -> mLogger.entryUnbound(e)); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt index 50a6207efe0b..d1feaa05c653 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt @@ -3,44 +3,46 @@ package com.android.systemui.statusbar.notification.interruption import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject class HeadsUpViewBinderLogger @Inject constructor(@NotificationHeadsUpLog val buffer: LogBuffer) { - fun startBindingHun(key: String) { + fun startBindingHun(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "start binding heads up entry $str1 " }) } - fun currentOngoingBindingAborted(key: String) { + fun currentOngoingBindingAborted(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "aborted potential ongoing heads up entry binding $str1 " }) } - fun entryBoundSuccessfully(key: String) { + fun entryBoundSuccessfully(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "heads up entry bound successfully $str1 " }) } - fun entryUnbound(key: String) { + fun entryUnbound(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "heads up entry unbound successfully $str1 " }) } - fun entryContentViewMarkedFreeable(key: String) { + fun entryContentViewMarkedFreeable(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "start unbinding heads up entry $str1 " }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt index 1d18ca3dfade..016b388ff60a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt @@ -16,11 +16,12 @@ package com.android.systemui.statusbar.notification.interruption -import android.service.notification.StatusBarNotification import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel.DEBUG import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationInterruptLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject class NotificationInterruptLogger @Inject constructor( @@ -41,17 +42,17 @@ class NotificationInterruptLogger @Inject constructor( }) } - fun logNoBubbleNotAllowed(sbn: StatusBarNotification) { + fun logNoBubbleNotAllowed(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No bubble up: not allowed to bubble: $str1" }) } - fun logNoBubbleNoMetadata(sbn: StatusBarNotification) { + fun logNoBubbleNoMetadata(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No bubble up: notification: $str1 doesn't have valid metadata" }) @@ -64,89 +65,89 @@ class NotificationInterruptLogger @Inject constructor( }) } - fun logNoHeadsUpPackageSnoozed(sbn: StatusBarNotification) { + fun logNoHeadsUpPackageSnoozed(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No alerting: snoozed package: $str1" }) } - fun logNoHeadsUpAlreadyBubbled(sbn: StatusBarNotification) { + fun logNoHeadsUpAlreadyBubbled(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No heads up: in unlocked shade where notification is shown as a bubble: $str1" }) } - fun logNoHeadsUpSuppressedByDnd(sbn: StatusBarNotification) { + fun logNoHeadsUpSuppressedByDnd(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No heads up: suppressed by DND: $str1" }) } - fun logNoHeadsUpNotImportant(sbn: StatusBarNotification) { + fun logNoHeadsUpNotImportant(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No heads up: unimportant notification: $str1" }) } - fun logNoHeadsUpNotInUse(sbn: StatusBarNotification) { + fun logNoHeadsUpNotInUse(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No heads up: not in use: $str1" }) } fun logNoHeadsUpSuppressedBy( - sbn: StatusBarNotification, + entry: NotificationEntry, suppressor: NotificationInterruptSuppressor ) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey str2 = suppressor.name }, { "No heads up: aborted by suppressor: $str2 sbnKey=$str1" }) } - fun logHeadsUp(sbn: StatusBarNotification) { + fun logHeadsUp(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "Heads up: $str1" }) } - fun logNoAlertingFilteredOut(sbn: StatusBarNotification) { + fun logNoAlertingFilteredOut(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No alerting: filtered notification: $str1" }) } - fun logNoAlertingGroupAlertBehavior(sbn: StatusBarNotification) { + fun logNoAlertingGroupAlertBehavior(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No alerting: suppressed due to group alert behavior: $str1" }) } fun logNoAlertingSuppressedBy( - sbn: StatusBarNotification, + entry: NotificationEntry, suppressor: NotificationInterruptSuppressor, awake: Boolean ) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey str2 = suppressor.name bool1 = awake }, { @@ -154,65 +155,65 @@ class NotificationInterruptLogger @Inject constructor( }) } - fun logNoAlertingRecentFullscreen(sbn: StatusBarNotification) { + fun logNoAlertingRecentFullscreen(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No alerting: recent fullscreen: $str1" }) } - fun logNoPulsingSettingDisabled(sbn: StatusBarNotification) { + fun logNoPulsingSettingDisabled(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No pulsing: disabled by setting: $str1" }) } - fun logNoPulsingBatteryDisabled(sbn: StatusBarNotification) { + fun logNoPulsingBatteryDisabled(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No pulsing: disabled by battery saver: $str1" }) } - fun logNoPulsingNoAlert(sbn: StatusBarNotification) { + fun logNoPulsingNoAlert(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No pulsing: notification shouldn't alert: $str1" }) } - fun logNoPulsingNoAmbientEffect(sbn: StatusBarNotification) { + fun logNoPulsingNoAmbientEffect(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No pulsing: ambient effect suppressed: $str1" }) } - fun logNoPulsingNotImportant(sbn: StatusBarNotification) { + fun logNoPulsingNotImportant(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "No pulsing: not important enough: $str1" }) } - fun logPulsing(sbn: StatusBarNotification) { + fun logPulsing(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = sbn.key + str1 = entry.logKey }, { "Pulsing: $str1" }) } - fun keyguardHideNotification(key: String) { + fun keyguardHideNotification(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = key + str1 = entry.logKey }, { "Keyguard Hide Notification: $str1" }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index a063dbd99626..8378b69bee9a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -147,14 +147,14 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter } if (!entry.canBubble()) { - mLogger.logNoBubbleNotAllowed(sbn); + mLogger.logNoBubbleNotAllowed(entry); return false; } if (entry.getBubbleMetadata() == null || (entry.getBubbleMetadata().getShortcutId() == null && entry.getBubbleMetadata().getIntent() == null)) { - mLogger.logNoBubbleNoMetadata(sbn); + mLogger.logNoBubbleNoMetadata(entry); return false; } @@ -203,23 +203,23 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter } if (isSnoozedPackage(sbn)) { - mLogger.logNoHeadsUpPackageSnoozed(sbn); + mLogger.logNoHeadsUpPackageSnoozed(entry); return false; } boolean inShade = mStatusBarStateController.getState() == SHADE; if (entry.isBubble() && inShade) { - mLogger.logNoHeadsUpAlreadyBubbled(sbn); + mLogger.logNoHeadsUpAlreadyBubbled(entry); return false; } if (entry.shouldSuppressPeek()) { - mLogger.logNoHeadsUpSuppressedByDnd(sbn); + mLogger.logNoHeadsUpSuppressedByDnd(entry); return false; } if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) { - mLogger.logNoHeadsUpNotImportant(sbn); + mLogger.logNoHeadsUpNotImportant(entry); return false; } @@ -232,17 +232,17 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter boolean inUse = mPowerManager.isScreenOn() && !isDreaming; if (!inUse) { - mLogger.logNoHeadsUpNotInUse(sbn); + mLogger.logNoHeadsUpNotInUse(entry); return false; } for (int i = 0; i < mSuppressors.size(); i++) { if (mSuppressors.get(i).suppressAwakeHeadsUp(entry)) { - mLogger.logNoHeadsUpSuppressedBy(sbn, mSuppressors.get(i)); + mLogger.logNoHeadsUpSuppressedBy(entry, mSuppressors.get(i)); return false; } } - mLogger.logHeadsUp(sbn); + mLogger.logHeadsUp(entry); return true; } @@ -254,38 +254,36 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter * @return true if the entry should ambient pulse, false otherwise */ private boolean shouldHeadsUpWhenDozing(NotificationEntry entry) { - StatusBarNotification sbn = entry.getSbn(); - if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) { - mLogger.logNoPulsingSettingDisabled(sbn); + mLogger.logNoPulsingSettingDisabled(entry); return false; } if (mBatteryController.isAodPowerSave()) { - mLogger.logNoPulsingBatteryDisabled(sbn); + mLogger.logNoPulsingBatteryDisabled(entry); return false; } if (!canAlertCommon(entry)) { - mLogger.logNoPulsingNoAlert(sbn); + mLogger.logNoPulsingNoAlert(entry); return false; } if (!canAlertHeadsUpCommon(entry)) { - mLogger.logNoPulsingNoAlert(sbn); + mLogger.logNoPulsingNoAlert(entry); return false; } if (entry.shouldSuppressAmbient()) { - mLogger.logNoPulsingNoAmbientEffect(sbn); + mLogger.logNoPulsingNoAmbientEffect(entry); return false; } if (entry.getImportance() < NotificationManager.IMPORTANCE_DEFAULT) { - mLogger.logNoPulsingNotImportant(sbn); + mLogger.logNoPulsingNotImportant(entry); return false; } - mLogger.logPulsing(sbn); + mLogger.logPulsing(entry); return true; } @@ -296,22 +294,20 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter * @return true if these checks pass, false if the notification should not alert */ private boolean canAlertCommon(NotificationEntry entry) { - StatusBarNotification sbn = entry.getSbn(); - if (!mFlags.isNewPipelineEnabled() && mNotificationFilter.shouldFilterOut(entry)) { - mLogger.logNoAlertingFilteredOut(sbn); + mLogger.logNoAlertingFilteredOut(entry); return false; } for (int i = 0; i < mSuppressors.size(); i++) { if (mSuppressors.get(i).suppressInterruptions(entry)) { - mLogger.logNoAlertingSuppressedBy(sbn, mSuppressors.get(i), /* awake */ false); + mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ false); return false; } } if (mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry)) { - mLogger.keyguardHideNotification(entry.getKey()); + mLogger.keyguardHideNotification(entry); return false; } @@ -329,12 +325,12 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter // Don't alert notifications that are suppressed due to group alert behavior if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) { - mLogger.logNoAlertingGroupAlertBehavior(sbn); + mLogger.logNoAlertingGroupAlertBehavior(entry); return false; } if (entry.hasJustLaunchedFullScreenIntent()) { - mLogger.logNoAlertingRecentFullscreen(sbn); + mLogger.logNoAlertingRecentFullscreen(entry); return false; } @@ -352,7 +348,7 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter for (int i = 0; i < mSuppressors.size(); i++) { if (mSuppressors.get(i).suppressAwakeInterruptions(entry)) { - mLogger.logNoAlertingSuppressedBy(sbn, mSuppressors.get(i), /* awake */ true); + mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ true); return false; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index 599039d46556..a493a676e3d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.row; import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; +import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; import android.util.Log; import android.view.View; @@ -247,7 +248,7 @@ public class ExpandableNotificationRowController implements NotifViewController @Override @NonNull public String getNodeLabel() { - return mView.getEntry().getKey(); + return logKey(mView.getEntry()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java index c66140822d92..99a24cb3e9ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java @@ -67,6 +67,7 @@ public class HybridConversationNotificationView extends HybridNotificationView { mConversationIconView = requireViewById(com.android.internal.R.id.conversation_icon); mConversationFacePile = requireViewById(com.android.internal.R.id.conversation_face_pile); mConversationSenderName = requireViewById(R.id.conversation_notification_sender); + applyTextColor(mConversationSenderName, mSecondaryTextColor); mFacePileSize = getResources() .getDimensionPixelSize(R.dimen.conversation_single_line_face_pile_size); mFacePileAvatarSize = getResources() @@ -75,6 +76,9 @@ public class HybridConversationNotificationView extends HybridNotificationView { .getDimensionPixelSize(R.dimen.conversation_single_line_avatar_size); mFacePileProtectionWidth = getResources().getDimensionPixelSize( R.dimen.conversation_single_line_face_pile_protection_width); + mTransformationHelper.setCustomTransformation( + new FadeOutAndDownWithTitleTransformation(mConversationSenderName), + mConversationSenderName.getId()); mTransformationHelper.addViewTransformingToSimilar(mConversationIconView); mTransformationHelper.addTransformedView(mConversationSenderName); } 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 40a44ffd7fe3..77fd05186090 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 @@ -24,7 +24,6 @@ import android.content.Context; import android.content.res.Resources; import android.service.notification.StatusBarNotification; import android.util.TypedValue; -import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -57,10 +56,8 @@ public class HybridGroupManager { mOverflowNumberPadding = res.getDimensionPixelSize(R.dimen.group_overflow_number_padding); } - private HybridNotificationView inflateHybridViewWithStyle(int style, - View contentView, ViewGroup parent) { - LayoutInflater inflater = new ContextThemeWrapper(mContext, style) - .getSystemService(LayoutInflater.class); + private HybridNotificationView inflateHybridView(View contentView, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(mContext); int layout = contentView instanceof ConversationLayout ? R.layout.hybrid_conversation_notification : R.layout.hybrid_notification; @@ -93,16 +90,8 @@ public class HybridGroupManager { public HybridNotificationView bindFromNotification(HybridNotificationView reusableView, View contentView, StatusBarNotification notification, ViewGroup parent) { - return bindFromNotificationWithStyle(reusableView, contentView, notification, - R.style.HybridNotification, parent); - } - - private HybridNotificationView bindFromNotificationWithStyle( - HybridNotificationView reusableView, View contentView, - StatusBarNotification notification, - int style, ViewGroup parent) { if (reusableView == null) { - reusableView = inflateHybridViewWithStyle(style, contentView, parent); + reusableView = inflateHybridView(contentView, parent); } CharSequence titleText = resolveTitle(notification.getNotification()); CharSequence contentText = resolveText(notification.getNotification()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java index c0d85a6a16ef..fc9d9e8b736c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java @@ -16,13 +16,18 @@ package com.android.systemui.statusbar.notification.row; +import static android.app.Notification.COLOR_INVALID; + import android.annotation.Nullable; import android.content.Context; +import android.content.res.TypedArray; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; +import androidx.annotation.ColorInt; + import com.android.keyguard.AlphaOptimizedLinearLayout; import com.android.systemui.R; import com.android.systemui.statusbar.CrossFadeHelper; @@ -40,6 +45,8 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout protected final ViewTransformationHelper mTransformationHelper = new ViewTransformationHelper(); protected TextView mTitleView; protected TextView mTextView; + protected int mPrimaryTextColor = COLOR_INVALID; + protected int mSecondaryTextColor = COLOR_INVALID; public HybridNotificationView(Context context) { this(context, null); @@ -69,42 +76,37 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout @Override protected void onFinishInflate() { super.onFinishInflate(); + resolveThemeTextColors(); mTitleView = findViewById(R.id.notification_title); mTextView = findViewById(R.id.notification_text); + applyTextColor(mTitleView, mPrimaryTextColor); + applyTextColor(mTextView, mSecondaryTextColor); mTransformationHelper.setCustomTransformation( - new ViewTransformationHelper.CustomTransformation() { - @Override - public boolean transformTo(TransformState ownState, TransformableView notification, - float transformationAmount) { - // We want to transform to the same y location as the title - TransformState otherState = notification.getCurrentState( - TRANSFORMING_VIEW_TITLE); - CrossFadeHelper.fadeOut(mTextView, transformationAmount); - if (otherState != null) { - ownState.transformViewVerticalTo(otherState, transformationAmount); - otherState.recycle(); - } - return true; - } - - @Override - public boolean transformFrom(TransformState ownState, - TransformableView notification, float transformationAmount) { - // We want to transform from the same y location as the title - TransformState otherState = notification.getCurrentState( - TRANSFORMING_VIEW_TITLE); - CrossFadeHelper.fadeIn(mTextView, transformationAmount, true /* remap */); - if (otherState != null) { - ownState.transformViewVerticalFrom(otherState, transformationAmount); - otherState.recycle(); - } - return true; - } - }, TRANSFORMING_VIEW_TEXT); + new FadeOutAndDownWithTitleTransformation(mTextView), + TRANSFORMING_VIEW_TEXT); mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TITLE, mTitleView); mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TEXT, mTextView); } + protected void applyTextColor(TextView textView, @ColorInt int textColor) { + if (textColor != COLOR_INVALID) { + textView.setTextColor(textColor); + } + } + + private void resolveThemeTextColors() { + try (TypedArray ta = mContext.getTheme().obtainStyledAttributes( + android.R.style.Theme_DeviceDefault_DayNight, new int[]{ + android.R.attr.textColorPrimary, + android.R.attr.textColorSecondary + })) { + if (ta != null) { + mPrimaryTextColor = ta.getColor(0, mPrimaryTextColor); + mSecondaryTextColor = ta.getColor(1, mSecondaryTextColor); + } + } + } + public void bind(@Nullable CharSequence title, @Nullable CharSequence text, @Nullable View contentView) { mTitleView.setText(title); @@ -152,4 +154,40 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout @Override public void setNotificationFaded(boolean faded) {} + + protected static class FadeOutAndDownWithTitleTransformation extends + ViewTransformationHelper.CustomTransformation { + + private final View mView; + + public FadeOutAndDownWithTitleTransformation(View view) { + mView = view; + } + + @Override + public boolean transformTo(TransformState ownState, TransformableView notification, + float transformationAmount) { + // We want to transform to the same y location as the title + TransformState otherState = notification.getCurrentState(TRANSFORMING_VIEW_TITLE); + CrossFadeHelper.fadeOut(mView, transformationAmount); + if (otherState != null) { + ownState.transformViewVerticalTo(otherState, transformationAmount); + otherState.recycle(); + } + return true; + } + + @Override + public boolean transformFrom(TransformState ownState, + TransformableView notification, float transformationAmount) { + // We want to transform from the same y location as the title + TransformState otherState = notification.getCurrentState(TRANSFORMING_VIEW_TITLE); + CrossFadeHelper.fadeIn(mView, transformationAmount, true /* remap */); + if (otherState != null) { + ownState.transformViewVerticalFrom(otherState, transformationAmount); + otherState.recycle(); + } + return true; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java index f693ebbb7830..ea564ddb9193 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java @@ -112,7 +112,8 @@ public final class NotifBindPipeline { public void manageRow( @NonNull NotificationEntry entry, @NonNull ExpandableNotificationRow row) { - mLogger.logManagedRow(entry.getKey()); + mLogger.logManagedRow(entry); + mLogger.logManagedRow(entry); final BindEntry bindEntry = getBindEntry(entry); if (bindEntry == null) { @@ -154,12 +155,12 @@ public final class NotifBindPipeline { * the real work once rather than repeatedly start and cancel it. */ private void requestPipelineRun(NotificationEntry entry) { - mLogger.logRequestPipelineRun(entry.getKey()); + mLogger.logRequestPipelineRun(entry); final BindEntry bindEntry = getBindEntry(entry); if (bindEntry.row == null) { // Row is not managed yet but may be soon. Stop for now. - mLogger.logRequestPipelineRowNotSet(entry.getKey()); + mLogger.logRequestPipelineRowNotSet(entry); return; } @@ -177,7 +178,7 @@ public final class NotifBindPipeline { * callbacks when the run finishes. If a run is already in progress, it is restarted. */ private void startPipeline(NotificationEntry entry) { - mLogger.logStartPipeline(entry.getKey()); + mLogger.logStartPipeline(entry); if (mStage == null) { throw new IllegalStateException("No stage was ever set on the pipeline"); @@ -193,7 +194,7 @@ public final class NotifBindPipeline { final BindEntry bindEntry = getBindEntry(entry); final Set<BindCallback> callbacks = bindEntry.callbacks; - mLogger.logFinishedPipeline(entry.getKey(), callbacks.size()); + mLogger.logFinishedPipeline(entry, callbacks.size()); bindEntry.invalidated = false; // Move all callbacks to separate list as callbacks may themselves add/remove callbacks. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt index ec406f0524ff..ab91926d466a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt @@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.row import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject class NotifBindPipelineLogger @Inject constructor( @@ -32,41 +34,41 @@ class NotifBindPipelineLogger @Inject constructor( }) } - fun logManagedRow(notifKey: String) { + fun logManagedRow(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = notifKey + str1 = entry.logKey }, { "Row set for notif: $str1" }) } - fun logRequestPipelineRun(notifKey: String) { + fun logRequestPipelineRun(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = notifKey + str1 = entry.logKey }, { "Request pipeline run for notif: $str1" }) } - fun logRequestPipelineRowNotSet(notifKey: String) { + fun logRequestPipelineRowNotSet(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = notifKey + str1 = entry.logKey }, { "Row is not set so pipeline will not run. notif = $str1" }) } - fun logStartPipeline(notifKey: String) { + fun logStartPipeline(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = notifKey + str1 = entry.logKey }, { "Start pipeline for notif: $str1" }) } - fun logFinishedPipeline(notifKey: String, numCallbacks: Int) { + fun logFinishedPipeline(entry: NotificationEntry, numCallbacks: Int) { buffer.log(TAG, INFO, { - str1 = notifKey + str1 = entry.logKey int1 = numCallbacks }, { "Finished pipeline for notif $str1 with $int1 callbacks" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 134f24e7e646..27aa4b38e0ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -266,9 +266,14 @@ public class NotificationConversationInfo extends LinearLayout implements snooze.setOnClickListener(mOnSnoozeClick); */ - if (mAppBubble == BUBBLE_PREFERENCE_ALL) { - ((TextView) findViewById(R.id.default_summary)).setText(getResources().getString( + TextView defaultSummaryTextView = findViewById(R.id.default_summary); + if (mAppBubble == BUBBLE_PREFERENCE_ALL + && BubblesManager.areBubblesEnabled(mContext, mSbn.getUser())) { + defaultSummaryTextView.setText(getResources().getString( R.string.notification_channel_summary_default_with_bubbles, mAppName)); + } else { + defaultSummaryTextView.setText(getResources().getString( + R.string.notification_channel_summary_default)); } findViewById(R.id.priority).setOnClickListener(mOnFavoriteClick); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index 3616f8faee1e..81cf14646465 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -57,7 +57,7 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { @NonNull StageCallback callback) { RowContentBindParams params = getStageParams(entry); - mLogger.logStageParams(entry.getKey(), params.toString()); + mLogger.logStageParams(entry, params); // Resolve content to bind/unbind. @InflationFlag int inflationFlags = params.getContentViews(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt index 29cce3375c8a..f9923b2254d7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt @@ -19,17 +19,19 @@ package com.android.systemui.statusbar.notification.row import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject class RowContentBindStageLogger @Inject constructor( @NotificationLog private val buffer: LogBuffer ) { - fun logStageParams(notifKey: String, stageParams: String) { + fun logStageParams(entry: NotificationEntry, stageParams: RowContentBindParams) { buffer.log(TAG, INFO, { - str1 = notifKey - str2 = stageParams + str1 = entry.logKey + str2 = stageParams.toString() }, { - "Invalidated notif $str1 with params: \n$str2" + "Invalidated notif $str1 with params: $str2" }) } } 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 213f00b8e03d..2fd02d9f1cd9 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 @@ -748,19 +748,20 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } - private void logHunSkippedForUnexpectedState(String key, boolean expected, boolean actual) { + private void logHunSkippedForUnexpectedState(ExpandableNotificationRow enr, + boolean expected, boolean actual) { if (mLogger == null) return; - mLogger.hunSkippedForUnexpectedState(key, expected, actual); + mLogger.hunSkippedForUnexpectedState(enr.getEntry(), expected, actual); } - private void logHunAnimationSkipped(String key, String reason) { + private void logHunAnimationSkipped(ExpandableNotificationRow enr, String reason) { if (mLogger == null) return; - mLogger.hunAnimationSkipped(key, reason); + mLogger.hunAnimationSkipped(enr.getEntry(), reason); } - private void logHunAnimationEventAdded(String key, int type) { + private void logHunAnimationEventAdded(ExpandableNotificationRow enr, int type) { if (mLogger == null) return; - mLogger.hunAnimationEventAdded(key, type); + mLogger.hunAnimationEventAdded(enr.getEntry(), type); } private void onDrawDebug(Canvas canvas) { @@ -3174,7 +3175,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (isHeadsUp != row.isHeadsUp()) { // For cases where we have a heads up showing and appearing again we shouldn't // do the animations at all. - logHunSkippedForUnexpectedState(key, isHeadsUp, row.isHeadsUp()); + logHunSkippedForUnexpectedState(row, isHeadsUp, row.isHeadsUp()); continue; } int type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER; @@ -3192,7 +3193,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (row.isChildInGroup()) { // We can otherwise get stuck in there if it was just isolated row.setHeadsUpAnimatingAway(false); - logHunAnimationSkipped(key, "row is child in group"); + logHunAnimationSkipped(row, "row is child in group"); continue; } } else { @@ -3200,7 +3201,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (viewState == null) { // A view state was never generated for this view, so we don't need to animate // this. This may happen with notification children. - logHunAnimationSkipped(key, "row has no viewState"); + logHunAnimationSkipped(row, "row has no viewState"); continue; } if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) { @@ -3224,7 +3225,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable + " onBottom=" + onBottom + " row=" + row.getEntry().getKey()); } - logHunAnimationEventAdded(key, type); + logHunAnimationEventAdded(row, type); } mHeadsUpChangeAnimations.clear(); mAddedHeadsUpChildren.clear(); @@ -4360,8 +4361,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /** * Update colors of "dismiss" and "empty shade" views. - * - * @param lightTheme True if light theme should be used. */ @ShadeViewRefactor(RefactorComponent.DECORATOR) void updateDecorViews() { @@ -4777,8 +4776,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (SPEW) { Log.v(TAG, "generateHeadsUpAnimation: previous hun appear animation cancelled"); } - logHunAnimationSkipped(row.getEntry().getKey(), - "previous hun appear animation cancelled"); + logHunAnimationSkipped(row, "previous hun appear animation cancelled"); return; } mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt index 04bf62104f66..5f79c0e3913a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt @@ -3,21 +3,27 @@ package com.android.systemui.statusbar.notification.stack import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationHeadsUpLog -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.* +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER import javax.inject.Inject class NotificationStackScrollLogger @Inject constructor( @NotificationHeadsUpLog private val buffer: LogBuffer ) { - fun hunAnimationSkipped(key: String, reason: String) { + fun hunAnimationSkipped(entry: NotificationEntry, reason: String) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey str2 = reason }, { "heads up animation skipped: key: $str1 reason: $str2" }) } - fun hunAnimationEventAdded(key: String, type: Int) { + fun hunAnimationEventAdded(entry: NotificationEntry, type: Int) { val reason: String reason = if (type == ANIMATION_TYPE_HEADS_UP_DISAPPEAR) { "HEADS_UP_DISAPPEAR" @@ -33,16 +39,16 @@ class NotificationStackScrollLogger @Inject constructor( type.toString() } buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey str2 = reason }, { "heads up animation added: $str1 with type $str2" }) } - fun hunSkippedForUnexpectedState(key: String, expected: Boolean, actual: Boolean) { + fun hunSkippedForUnexpectedState(entry: NotificationEntry, expected: Boolean, actual: Boolean) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey bool1 = expected bool2 = actual }, { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt index 77377af9ddfb..cb4a0884fea4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt @@ -3,6 +3,7 @@ package com.android.systemui.statusbar.notification.stack import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject class StackStateLogger @Inject constructor( @@ -10,7 +11,7 @@ class StackStateLogger @Inject constructor( ) { fun logHUNViewDisappearing(key: String) { buffer.log(TAG, LogLevel.INFO, { - str1 = key + str1 = logKey(key) }, { "Heads up view disappearing $str1 " }) @@ -18,7 +19,7 @@ class StackStateLogger @Inject constructor( fun logHUNViewAppearing(key: String) { buffer.log(TAG, LogLevel.INFO, { - str1 = key + str1 = logKey(key) }, { "Heads up notification view appearing $str1 " }) @@ -26,7 +27,7 @@ class StackStateLogger @Inject constructor( fun logHUNViewDisappearingWithRemoveEvent(key: String) { buffer.log(TAG, LogLevel.ERROR, { - str1 = key + str1 = logKey(key) }, { "Heads up view disappearing $str1 for ANIMATION_TYPE_REMOVE" }) @@ -34,7 +35,7 @@ class StackStateLogger @Inject constructor( fun logHUNViewAppearingWithAddEvent(key: String) { buffer.log(TAG, LogLevel.ERROR, { - str1 = key + str1 = logKey(key) }, { "Heads up view disappearing $str1 for ANIMATION_TYPE_ADD" }) @@ -42,7 +43,7 @@ class StackStateLogger @Inject constructor( fun disappearAnimationEnded(key: String) { buffer.log(TAG, LogLevel.INFO, { - str1 = key + str1 = logKey(key) }, { "Heads up notification disappear animation ended $str1 " }) @@ -50,7 +51,7 @@ class StackStateLogger @Inject constructor( fun appearAnimationEnded(key: String) { buffer.log(TAG, LogLevel.INFO, { - str1 = key + str1 = logKey(key) }, { "Heads up notification appear animation ended $str1 " }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index fd307df8d304..93b2e41e595b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -28,17 +28,13 @@ import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_RIGHT_UNL import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE; import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE; -import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.ColorStateList; @@ -46,13 +42,8 @@ import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; -import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; import android.os.RemoteException; import android.os.UserHandle; -import android.provider.MediaStore; -import android.service.media.CameraPrewarmService; import android.service.quickaccesswallet.GetWalletCardsError; import android.service.quickaccesswallet.GetWalletCardsResponse; import android.service.quickaccesswallet.QuickAccessWalletClient; @@ -172,20 +163,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private KeyguardAffordanceHelper mAffordanceHelper; private FalsingManager mFalsingManager; private boolean mUserSetupComplete; - private boolean mPrewarmBound; - private Messenger mPrewarmMessenger; - private final ServiceConnection mPrewarmConnection = new ServiceConnection() { - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - mPrewarmMessenger = new Messenger(service); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - mPrewarmMessenger = null; - } - }; private boolean mLeftIsVoiceAssist; private Drawable mLeftAssistIcon; @@ -602,46 +579,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } } - public void bindCameraPrewarmService() { - Intent intent = getCameraIntent(); - ActivityInfo targetInfo = mActivityIntentHelper.getTargetActivityInfo(intent, - KeyguardUpdateMonitor.getCurrentUser(), true /* onlyDirectBootAware */); - if (targetInfo != null && targetInfo.metaData != null) { - String clazz = targetInfo.metaData.getString( - MediaStore.META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE); - if (clazz != null) { - Intent serviceIntent = new Intent(); - serviceIntent.setClassName(targetInfo.packageName, clazz); - serviceIntent.setAction(CameraPrewarmService.ACTION_PREWARM); - try { - if (getContext().bindServiceAsUser(serviceIntent, mPrewarmConnection, - Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, - new UserHandle(UserHandle.USER_CURRENT))) { - mPrewarmBound = true; - } - } catch (SecurityException e) { - Log.w(TAG, "Unable to bind to prewarm service package=" + targetInfo.packageName - + " class=" + clazz, e); - } - } - } - } - - public void unbindCameraPrewarmService(boolean launched) { - if (mPrewarmBound) { - if (mPrewarmMessenger != null && launched) { - try { - mPrewarmMessenger.send(Message.obtain(null /* handler */, - CameraPrewarmService.MSG_CAMERA_FIRED)); - } catch (RemoteException e) { - Log.w(TAG, "Error sending camera fired message", e); - } - } - mContext.unbindService(mPrewarmConnection); - mPrewarmBound = false; - } - } - public void launchCamera(String source) { final Intent intent = getCameraIntent(); intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source); @@ -651,8 +588,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL AsyncTask.execute(new Runnable() { @Override public void run() { - int result = ActivityManager.START_CANCELED; - // Normally an activity will set it's requested rotation // animation on its window. However when launching an activity // causes the orientation to change this is too late. In these cases @@ -666,7 +601,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL o.setRotationAnimationHint( WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS); try { - result = ActivityTaskManager.getService().startActivityAsUser( + ActivityTaskManager.getService().startActivityAsUser( null, getContext().getBasePackageName(), getContext().getAttributionTag(), intent, intent.resolveTypeIfNeeded(getContext().getContentResolver()), @@ -675,25 +610,12 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } catch (RemoteException e) { Log.w(TAG, "Unable to start camera activity", e); } - final boolean launched = isSuccessfulLaunch(result); - post(new Runnable() { - @Override - public void run() { - unbindCameraPrewarmService(launched); - } - }); } }); } else { // We need to delay starting the activity because ResolverActivity finishes itself if // launched behind lockscreen. - mActivityStarter.startActivity(intent, false /* dismissShade */, - new ActivityStarter.Callback() { - @Override - public void onActivityStarted(int resultCode) { - unbindCameraPrewarmService(isSuccessfulLaunch(resultCode)); - } - }); + mActivityStarter.startActivity(intent, false /* dismissShade */); } } @@ -705,12 +627,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL dozeTimeTick(); } - private static boolean isSuccessfulLaunch(int result) { - return result == ActivityManager.START_SUCCESS - || result == ActivityManager.START_DELIVERED_TO_TOP - || result == ActivityManager.START_TASK_TO_FRONT; - } - public void launchLeftAffordance() { if (mLeftIsVoiceAssist) { launchVoiceAssist(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 6009eba24f5b..fbbb587872b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -1670,7 +1670,6 @@ public class NotificationPanelViewController extends PanelViewController { setQsExpansionEnabled(); } - @Override public void resetViews(boolean animate) { mIsLaunchTransitionFinished = false; mBlockTouches = false; @@ -4592,13 +4591,6 @@ public class NotificationPanelViewController extends PanelViewController { @Override public void onSwipingStarted(boolean rightIcon) { mFalsingCollector.onAffordanceSwipingStarted(rightIcon); - boolean - camera = - mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? !rightIcon - : rightIcon; - if (camera) { - mKeyguardBottomArea.bindCameraPrewarmService(); - } mView.requestDisallowInterceptTouchEvent(true); mOnlyAffordanceInThisMotion = true; mQsTracking = false; @@ -4607,7 +4599,6 @@ public class NotificationPanelViewController extends PanelViewController { @Override public void onSwipingAborted() { mFalsingCollector.onAffordanceSwipingAborted(); - mKeyguardBottomArea.unbindCameraPrewarmService(false /* launched */); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS index 18f0fb38999c..f5828f914eab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS @@ -1,3 +1,16 @@ per-file *Notification* = set noparent per-file *Notification* = file:../notification/OWNERS -per-file NotificationIcon* = ccassidy@google.com, evanlaird@google.com, pixel@google.com
\ No newline at end of file + +per-file NotificationIcon* = ccassidy@google.com, evanlaird@google.com, pixel@google.com + +per-file NotificationsQuickSettingsContainer.java = kozynski@google.com, asc@google.com +per-file NotificationsQSContainerController.kt = kozynski@google.com, asc@google.com + +per-file NotificationShadeWindowControllerImpl.java = dupin@google.com, cinek@google.com, beverlyt@google.com, pixel@google.com, juliacr@google.com +per-file NotificationShadeWindowViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com +per-file NotificationShadeWindowView.java = pixel@google.com, cinek@google.com, juliacr@google.com + +per-file NotificationPanelUnfoldAnimationController.kt = alexflo@google.com, jeffdq@google.com, juliacr@google.com + +per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com +per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java index 9f0ecb9d4096..ed12b00cc644 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java @@ -1163,8 +1163,6 @@ public abstract class PanelViewController { mTouchDisabled ? "T" : "f")); } - public abstract void resetViews(boolean animate); - public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) { mHeadsUpManager = headsUpManager; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index 36a045637a87..26bc3e3c0920 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -93,6 +93,17 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh } public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock) { + // TODO(b/219008720): Remove those calls to Dependency.get by introducing a + // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set + // the content and attach listeners. + this(context, theme, dismissOnDeviceLock, Dependency.get(SystemUIDialogManager.class), + Dependency.get(SysUiState.class), Dependency.get(BroadcastDispatcher.class), + Dependency.get(DialogLaunchAnimator.class)); + } + + public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock, + SystemUIDialogManager dialogManager, SysUiState sysUiState, + BroadcastDispatcher broadcastDispatcher, DialogLaunchAnimator dialogLaunchAnimator) { super(context, theme); mContext = context; @@ -101,13 +112,10 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh attrs.setTitle(getClass().getSimpleName()); getWindow().setAttributes(attrs); - mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this) : null; - - // TODO(b/219008720): Remove those calls to Dependency.get by introducing a - // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set - // the content and attach listeners. - mDialogManager = Dependency.get(SystemUIDialogManager.class); - mSysUiState = Dependency.get(SysUiState.class); + mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this, broadcastDispatcher, + dialogLaunchAnimator) : null; + mDialogManager = dialogManager; + mSysUiState = sysUiState; } @Override @@ -326,7 +334,10 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh * @param dismissAction An action to run when the dialog is dismissed. */ public static void registerDismissListener(Dialog dialog, @Nullable Runnable dismissAction) { - DismissReceiver dismissReceiver = new DismissReceiver(dialog); + // TODO(b/219008720): Remove those calls to Dependency.get. + DismissReceiver dismissReceiver = new DismissReceiver(dialog, + Dependency.get(BroadcastDispatcher.class), + Dependency.get(DialogLaunchAnimator.class)); dialog.setOnDismissListener(d -> { dismissReceiver.unregister(); if (dismissAction != null) dismissAction.run(); @@ -408,11 +419,11 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh private final BroadcastDispatcher mBroadcastDispatcher; private final DialogLaunchAnimator mDialogLaunchAnimator; - DismissReceiver(Dialog dialog) { + DismissReceiver(Dialog dialog, BroadcastDispatcher broadcastDispatcher, + DialogLaunchAnimator dialogLaunchAnimator) { mDialog = dialog; - // TODO(b/219008720): Remove those calls to Dependency.get. - mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class); - mDialogLaunchAnimator = Dependency.get(DialogLaunchAnimator.class); + mBroadcastDispatcher = broadcastDispatcher; + mDialogLaunchAnimator = dialogLaunchAnimator; } void register() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionController.kt index 1740bcbbefc8..16f28e7d1a21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionController.kt @@ -2,10 +2,13 @@ package com.android.systemui.statusbar.phone.shade.transition import android.content.res.Configuration import android.content.res.Resources +import android.util.MathUtils.constrain import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.ScrimController import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent import com.android.systemui.statusbar.policy.ConfigurationController @@ -21,7 +24,8 @@ constructor( configurationController: ConfigurationController, dumpManager: DumpManager, private val scrimController: ScrimController, - @Main private val resources: Resources + @Main private val resources: Resources, + private val statusBarStateController: SysuiStatusBarStateController, ) { private var inSplitShade = false @@ -55,19 +59,23 @@ constructor( } private fun calculateScrimExpansionFraction(expansionEvent: PanelExpansionChangeEvent): Float { - return if (inSplitShade) { - expansionEvent.dragDownPxAmount / splitShadeScrimTransitionDistance + return if (inSplitShade && isScreenUnlocked()) { + constrain(expansionEvent.dragDownPxAmount / splitShadeScrimTransitionDistance, 0f, 1f) } else { expansionEvent.fraction } } + private fun isScreenUnlocked() = + statusBarStateController.currentOrUpcomingState == StatusBarState.SHADE + private fun dump(printWriter: PrintWriter, args: Array<String>) { printWriter.println( """ ScrimShadeTransitionController: Resources: inSplitShade: $inSplitShade + isScreenUnlocked: ${isScreenUnlocked()} splitShadeScrimTransitionDistance: $splitShadeScrimTransitionDistance State: lastExpansionFraction: $lastExpansionFraction diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java index a89c128dd584..753e94015751 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java @@ -119,6 +119,17 @@ public interface BatteryController extends DemoMode, Dumpable, } /** + * Returns {@code true} if the charging source is + * {@link android.os.BatteryManager#BATTERY_PLUGGED_DOCK}. + * + * <P>Note that charging from dock is not considered as wireless charging. In other words, + * {@link BatteryController#isWirelessCharging()} and this are mutually exclusive. + */ + default boolean isChargingSourceDock() { + return false; + } + + /** * A listener that will be notified whenever a change in battery level or power save mode has * occurred. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index 917a5e0b9374..33ddf7eed006 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -76,7 +76,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC protected int mLevel; protected boolean mPluggedIn; - private boolean mPluggedInWireless; + private int mPluggedChargingSource; protected boolean mCharging; private boolean mStateUnknown = false; private boolean mCharged; @@ -195,10 +195,8 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC mLevel = (int)(100f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); - mPluggedIn = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; - mPluggedInWireless = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) - == BatteryManager.BATTERY_PLUGGED_WIRELESS; - + mPluggedChargingSource = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + mPluggedIn = mPluggedChargingSource != 0; final int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN); mCharged = status == BatteryManager.BATTERY_STATUS_FULL; @@ -284,7 +282,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC @Override public boolean isPluggedInWireless() { - return mPluggedInWireless; + return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_WIRELESS; } @Override @@ -441,4 +439,9 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC registerReceiver(); updatePowerSave(); } + + @Override + public boolean isChargingSourceDock() { + return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_DOCK; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java index 4cf1d2b3f91d..9946b4b9ecaa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java @@ -17,15 +17,11 @@ package com.android.systemui.statusbar.policy; import android.annotation.WorkerThread; -import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Process; import android.provider.Settings; import android.provider.Settings.Secure; import android.text.TextUtils; @@ -33,12 +29,19 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.android.internal.annotations.GuardedBy; +import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; +import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; @@ -59,65 +62,88 @@ public class FlashlightControllerImpl implements FlashlightController { "com.android.settings.flashlight.action.FLASHLIGHT_CHANGED"; private final CameraManager mCameraManager; - private final Context mContext; - /** Call {@link #ensureHandler()} before using */ - private Handler mHandler; + private final Executor mExecutor; + private final SecureSettings mSecureSettings; + private final DumpManager mDumpManager; + private final BroadcastSender mBroadcastSender; - /** Lock on mListeners when accessing */ + private final boolean mHasFlashlight; + + @GuardedBy("mListeners") private final ArrayList<WeakReference<FlashlightListener>> mListeners = new ArrayList<>(1); - /** Lock on {@code this} when accessing */ + @GuardedBy("this") private boolean mFlashlightEnabled; - - private String mCameraId; + @GuardedBy("this") private boolean mTorchAvailable; + private final AtomicReference<String> mCameraId; + private final AtomicBoolean mInitted = new AtomicBoolean(false); + @Inject - public FlashlightControllerImpl(Context context, DumpManager dumpManager) { - mContext = context; - mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); + public FlashlightControllerImpl( + DumpManager dumpManager, + CameraManager cameraManager, + @Background Executor bgExecutor, + SecureSettings secureSettings, + BroadcastSender broadcastSender, + PackageManager packageManager + ) { + mCameraManager = cameraManager; + mExecutor = bgExecutor; + mCameraId = new AtomicReference<>(null); + mSecureSettings = secureSettings; + mDumpManager = dumpManager; + mBroadcastSender = broadcastSender; + + mHasFlashlight = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH); + init(); + } - dumpManager.registerDumpable(getClass().getSimpleName(), this); - tryInitCamera(); + private void init() { + if (!mInitted.getAndSet(true)) { + mDumpManager.registerDumpable(getClass().getSimpleName(), this); + mExecutor.execute(this::tryInitCamera); + } } + @WorkerThread private void tryInitCamera() { + if (!mHasFlashlight || mCameraId.get() != null) return; try { - mCameraId = getCameraId(); + mCameraId.set(getCameraId()); } catch (Throwable e) { Log.e(TAG, "Couldn't initialize.", e); return; } - if (mCameraId != null) { - ensureHandler(); - mCameraManager.registerTorchCallback(mTorchCallback, mHandler); + if (mCameraId.get() != null) { + mCameraManager.registerTorchCallback(mExecutor, mTorchCallback); } } public void setFlashlight(boolean enabled) { - boolean pendingError = false; - synchronized (this) { - if (mCameraId == null) return; - if (mFlashlightEnabled != enabled) { - mFlashlightEnabled = enabled; - try { - mCameraManager.setTorchMode(mCameraId, enabled); - } catch (CameraAccessException e) { - Log.e(TAG, "Couldn't set torch mode", e); - mFlashlightEnabled = false; - pendingError = true; + if (!mHasFlashlight) return; + if (mCameraId.get() == null) { + mExecutor.execute(this::tryInitCamera); + } + mExecutor.execute(() -> { + if (mCameraId.get() == null) return; + synchronized (this) { + if (mFlashlightEnabled != enabled) { + try { + mCameraManager.setTorchMode(mCameraId.get(), enabled); + } catch (CameraAccessException e) { + Log.e(TAG, "Couldn't set torch mode", e); + dispatchError(); + } } } - } - dispatchModeChanged(mFlashlightEnabled); - if (pendingError) { - dispatchError(); - } + }); } public boolean hasFlashlight() { - return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH); + return mHasFlashlight; } public synchronized boolean isEnabled() { @@ -131,13 +157,13 @@ public class FlashlightControllerImpl implements FlashlightController { @Override public void addCallback(@NonNull FlashlightListener l) { synchronized (mListeners) { - if (mCameraId == null) { - tryInitCamera(); + if (mCameraId.get() == null) { + mExecutor.execute(this::tryInitCamera); } cleanUpListenersLocked(l); mListeners.add(new WeakReference<>(l)); - l.onFlashlightAvailabilityChanged(mTorchAvailable); - l.onFlashlightChanged(mFlashlightEnabled); + l.onFlashlightAvailabilityChanged(isAvailable()); + l.onFlashlightChanged(isEnabled()); } } @@ -148,14 +174,7 @@ public class FlashlightControllerImpl implements FlashlightController { } } - private synchronized void ensureHandler() { - if (mHandler == null) { - HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); - thread.start(); - mHandler = new Handler(thread.getLooper()); - } - } - + @WorkerThread private String getCameraId() throws CameraAccessException { String[] ids = mCameraManager.getCameraIdList(); for (String id : ids) { @@ -221,10 +240,9 @@ public class FlashlightControllerImpl implements FlashlightController { @Override @WorkerThread public void onTorchModeUnavailable(String cameraId) { - if (TextUtils.equals(cameraId, mCameraId)) { + if (TextUtils.equals(cameraId, mCameraId.get())) { setCameraAvailable(false); - Settings.Secure.putInt( - mContext.getContentResolver(), Settings.Secure.FLASHLIGHT_AVAILABLE, 0); + mSecureSettings.putInt(Settings.Secure.FLASHLIGHT_AVAILABLE, 0); } } @@ -232,14 +250,12 @@ public class FlashlightControllerImpl implements FlashlightController { @Override @WorkerThread public void onTorchModeChanged(String cameraId, boolean enabled) { - if (TextUtils.equals(cameraId, mCameraId)) { + if (TextUtils.equals(cameraId, mCameraId.get())) { setCameraAvailable(true); setTorchMode(enabled); - Settings.Secure.putInt( - mContext.getContentResolver(), Settings.Secure.FLASHLIGHT_AVAILABLE, 1); - Settings.Secure.putInt( - mContext.getContentResolver(), Secure.FLASHLIGHT_ENABLED, enabled ? 1 : 0); - mContext.sendBroadcast(new Intent(ACTION_FLASHLIGHT_CHANGED)); + mSecureSettings.putInt(Settings.Secure.FLASHLIGHT_AVAILABLE, 1); + mSecureSettings.putInt(Secure.FLASHLIGHT_ENABLED, enabled ? 1 : 0); + mBroadcastSender.sendBroadcast(new Intent(ACTION_FLASHLIGHT_CHANGED)); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java index bce5a159f79c..d3837d7347dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java @@ -142,7 +142,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { protected void setEntryPinned( @NonNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned) { - mLogger.logSetEntryPinned(headsUpEntry.mEntry.getKey(), isPinned); + mLogger.logSetEntryPinned(headsUpEntry.mEntry, isPinned); NotificationEntry entry = headsUpEntry.mEntry; if (entry.isRowPinned() != isPinned) { entry.setRowPinned(isPinned); @@ -183,7 +183,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { entry.setHeadsUp(false); setEntryPinned((HeadsUpEntry) alertEntry, false /* isPinned */); EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */); - mLogger.logNotificationActuallyRemoved(entry.getKey()); + mLogger.logNotificationActuallyRemoved(entry); for (OnHeadsUpChangedListener listener : mListeners) { listener.onHeadsUpStateChanged(entry, false); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt index 6a74ba957b4b..d7c81af53d8b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt @@ -20,6 +20,8 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.LogLevel.VERBOSE import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject /** Logger for [HeadsUpManager]. */ @@ -56,9 +58,9 @@ class HeadsUpManagerLogger @Inject constructor( }) } - fun logShowNotification(key: String) { + fun logShowNotification(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "show notification $str1" }) @@ -66,16 +68,16 @@ class HeadsUpManagerLogger @Inject constructor( fun logRemoveNotification(key: String, releaseImmediately: Boolean) { buffer.log(TAG, INFO, { - str1 = key + str1 = logKey(key) bool1 = releaseImmediately }, { "remove notification $str1 releaseImmediately: $bool1" }) } - fun logNotificationActuallyRemoved(key: String) { + fun logNotificationActuallyRemoved(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "notification removed $str1 " }) @@ -83,7 +85,7 @@ class HeadsUpManagerLogger @Inject constructor( fun logUpdateNotification(key: String, alert: Boolean, hasEntry: Boolean) { buffer.log(TAG, INFO, { - str1 = key + str1 = logKey(key) bool1 = alert bool2 = hasEntry }, { @@ -91,12 +93,12 @@ class HeadsUpManagerLogger @Inject constructor( }) } - fun logUpdateEntry(key: String, updatePostTime: Boolean) { + fun logUpdateEntry(entry: NotificationEntry, updatePostTime: Boolean) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey bool1 = updatePostTime }, { - "update entry $key updatePostTime: $bool1" + "update entry $str1 updatePostTime: $bool1" }) } @@ -108,9 +110,9 @@ class HeadsUpManagerLogger @Inject constructor( }) } - fun logSetEntryPinned(key: String, isPinned: Boolean) { + fun logSetEntryPinned(entry: NotificationEntry, isPinned: Boolean) { buffer.log(TAG, VERBOSE, { - str1 = key + str1 = entry.logKey bool1 = isPinned }, { "set entry pinned $str1 pinned: $bool1" diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java index 4e9030f8045c..dac8a0bff28a 100644 --- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java +++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java @@ -92,7 +92,15 @@ public class Monitor { } private void updateConditionMetState(Condition condition) { - mConditions.get(condition).stream().forEach(token -> mSubscriptions.get(token).update()); + final ArraySet<Subscription.Token> subscriptions = mConditions.get(condition); + + // It's possible the condition was removed between the time the callback occurred and + // update was executed on the main thread. + if (subscriptions == null) { + return; + } + + subscriptions.stream().forEach(token -> mSubscriptions.get(token).update()); } /** diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index 7de4586e1901..bc351427310d 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -27,7 +27,6 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -68,8 +67,6 @@ import org.mockito.junit.MockitoRule; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper() public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { - private static final int VIEW_WIDTH = 1600; - @Rule public MockitoRule mRule = MockitoJUnit.rule(); @@ -141,7 +138,6 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { when(mResources.getConfiguration()).thenReturn(mConfiguration); when(mView.getContext()).thenReturn(mContext); when(mView.getResources()).thenReturn(mResources); - when(mView.getWidth()).thenReturn(VIEW_WIDTH); when(mAdminSecondaryLockScreenControllerFactory.create(any(KeyguardSecurityCallback.class))) .thenReturn(mAdminSecondaryLockScreenController); when(mSecurityViewFlipper.getWindowInsetsController()).thenReturn(mWindowInsetsController); @@ -212,49 +208,26 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { mUserSwitcherController); } - private void touchDownLeftSide() { + private void touchDown() { mKeyguardSecurityContainerController.mGlobalTouchListener.onTouchEvent( MotionEvent.obtain( /* downTime= */0, /* eventTime= */0, MotionEvent.ACTION_DOWN, - /* x= */VIEW_WIDTH / 3f, - /* y= */0, - /* metaState= */0)); - } - - private void touchDownRightSide() { - mKeyguardSecurityContainerController.mGlobalTouchListener.onTouchEvent( - MotionEvent.obtain( - /* downTime= */0, - /* eventTime= */0, - MotionEvent.ACTION_DOWN, - /* x= */(VIEW_WIDTH / 3f) * 2, + /* x= */0, /* y= */0, /* metaState= */0)); } @Test - public void onInterceptTap_inhibitsFalsingInOneHandedMode() { - when(mView.getMode()).thenReturn(MODE_ONE_HANDED); - when(mView.isOneHandedModeLeftAligned()).thenReturn(true); - - touchDownLeftSide(); - verify(mFalsingCollector, never()).avoidGesture(); - - // Now on the right. - touchDownRightSide(); - verify(mFalsingCollector).avoidGesture(); - - // Move and re-test - reset(mFalsingCollector); - when(mView.isOneHandedModeLeftAligned()).thenReturn(false); + public void onInterceptTap_inhibitsFalsingInSidedSecurityMode() { - // On the right... - touchDownRightSide(); + when(mView.isTouchOnTheOtherSideOfSecurity(any())).thenReturn(false); + touchDown(); verify(mFalsingCollector, never()).avoidGesture(); - touchDownLeftSide(); + when(mView.isTouchOnTheOtherSideOfSecurity(any())).thenReturn(true); + touchDown(); verify(mFalsingCollector).avoidGesture(); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index d49f4d8172dc..f2ac0c7a7736 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -16,6 +16,11 @@ package com.android.keyguard; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE; +import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT; +import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.systemBars; @@ -25,7 +30,9 @@ import static com.android.keyguard.KeyguardSecurityContainer.MODE_ONE_HANDED; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; @@ -34,14 +41,13 @@ import static org.mockito.Mockito.when; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.graphics.Insets; -import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.Gravity; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; -import android.view.WindowInsetsController; import android.widget.FrameLayout; import androidx.test.filters.SmallTest; @@ -70,6 +76,9 @@ import java.util.ArrayList; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper() public class KeyguardSecurityContainerTest extends SysuiTestCase { + + private static final int VIEW_WIDTH = 1600; + private int mScreenWidth; private int mFakeMeasureSpec; @@ -77,8 +86,6 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { public MockitoRule mRule = MockitoJUnit.rule(); @Mock - private WindowInsetsController mWindowInsetsController; - @Mock private KeyguardSecurityViewFlipper mSecurityViewFlipper; @Mock private GlobalSettings mGlobalSettings; @@ -102,7 +109,6 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { mSecurityViewFlipperLayoutParams = new FrameLayout.LayoutParams( MATCH_PARENT, MATCH_PARENT); - when(mSecurityViewFlipper.getWindowInsetsController()).thenReturn(mWindowInsetsController); when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); mKeyguardSecurityContainer = new KeyguardSecurityContainer(getContext()); mKeyguardSecurityContainer.mSecurityViewFlipper = mSecurityViewFlipper; @@ -212,14 +218,12 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { mKeyguardSecurityContainer.updatePositionByTouchX( mKeyguardSecurityContainer.getWidth() - 1f); - verify(mGlobalSettings).putInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE, - Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT); - verify(mSecurityViewFlipper).setTranslationX( + verify(mGlobalSettings).putInt(ONE_HANDED_KEYGUARD_SIDE, ONE_HANDED_KEYGUARD_SIDE_RIGHT); + assertSecurityTranslationX( mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth()); mKeyguardSecurityContainer.updatePositionByTouchX(1f); - verify(mGlobalSettings).putInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE, - Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT); + verify(mGlobalSettings).putInt(ONE_HANDED_KEYGUARD_SIDE, ONE_HANDED_KEYGUARD_SIDE_LEFT); verify(mSecurityViewFlipper).setTranslationX(0.0f); } @@ -237,49 +241,43 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { } @Test - public void testUserSwitcherModeViewGravityLandscape() { + public void testUserSwitcherModeViewPositionLandscape() { // GIVEN one user has been setup and in landscape when(mUserSwitcherController.getUsers()).thenReturn(buildUserRecords(1)); - Configuration config = new Configuration(); - config.orientation = Configuration.ORIENTATION_LANDSCAPE; - when(getContext().getResources().getConfiguration()).thenReturn(config); + Configuration landscapeConfig = configuration(ORIENTATION_LANDSCAPE); + when(getContext().getResources().getConfiguration()).thenReturn(landscapeConfig); // WHEN UserSwitcherViewMode is initialized and config has changed setupUserSwitcher(); - reset(mSecurityViewFlipper); - when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); - mKeyguardSecurityContainer.onConfigurationChanged(config); + mKeyguardSecurityContainer.onConfigurationChanged(landscapeConfig); // THEN views are oriented side by side - verify(mSecurityViewFlipper).setLayoutParams(mLayoutCaptor.capture()); - assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(Gravity.RIGHT | Gravity.BOTTOM); - ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById( - R.id.keyguard_bouncer_user_switcher); - assertThat(((FrameLayout.LayoutParams) userSwitcher.getLayoutParams()).gravity) - .isEqualTo(Gravity.LEFT | Gravity.CENTER_VERTICAL); + assertSecurityGravity(Gravity.LEFT | Gravity.BOTTOM); + assertUserSwitcherGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); + assertSecurityTranslationX( + mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth()); + assertUserSwitcherTranslationX(0f); + } @Test public void testUserSwitcherModeViewGravityPortrait() { // GIVEN one user has been setup and in landscape when(mUserSwitcherController.getUsers()).thenReturn(buildUserRecords(1)); - Configuration config = new Configuration(); - config.orientation = Configuration.ORIENTATION_PORTRAIT; - when(getContext().getResources().getConfiguration()).thenReturn(config); + Configuration portraitConfig = configuration(ORIENTATION_PORTRAIT); + when(getContext().getResources().getConfiguration()).thenReturn(portraitConfig); // WHEN UserSwitcherViewMode is initialized and config has changed setupUserSwitcher(); reset(mSecurityViewFlipper); when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); - mKeyguardSecurityContainer.onConfigurationChanged(config); + mKeyguardSecurityContainer.onConfigurationChanged(portraitConfig); // THEN views are both centered horizontally - verify(mSecurityViewFlipper).setLayoutParams(mLayoutCaptor.capture()); - assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(Gravity.CENTER_HORIZONTAL); - ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById( - R.id.keyguard_bouncer_user_switcher); - assertThat(((FrameLayout.LayoutParams) userSwitcher.getLayoutParams()).gravity) - .isEqualTo(Gravity.CENTER_HORIZONTAL); + assertSecurityGravity(Gravity.CENTER_HORIZONTAL); + assertUserSwitcherGravity(Gravity.CENTER_HORIZONTAL); + assertSecurityTranslationX(0); + assertUserSwitcherTranslationX(0); } @Test @@ -310,9 +308,102 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { assertThat(anchor.isClickable()).isTrue(); } + @Test + public void testTouchesAreRecognizedAsBeingOnTheOtherSideOfSecurity() { + setupUserSwitcher(); + setViewWidth(VIEW_WIDTH); + + // security is on the right side by default + assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity( + touchEventLeftSide())).isTrue(); + assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity( + touchEventRightSide())).isFalse(); + + // move security to the left side + when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_LEFT); + mKeyguardSecurityContainer.onConfigurationChanged(new Configuration()); + + assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity( + touchEventLeftSide())).isFalse(); + assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity( + touchEventRightSide())).isTrue(); + } + + @Test + public void testSecuritySwitchesSidesInLandscapeUserSwitcherMode() { + when(getContext().getResources().getConfiguration()) + .thenReturn(configuration(ORIENTATION_LANDSCAPE)); + setupUserSwitcher(); + + // switch sides + when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_LEFT); + mKeyguardSecurityContainer.onConfigurationChanged(new Configuration()); + + assertSecurityTranslationX(0); + assertUserSwitcherTranslationX( + mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth()); + } + + private Configuration configuration(@Configuration.Orientation int orientation) { + Configuration config = new Configuration(); + config.orientation = orientation; + return config; + } + + private void assertSecurityTranslationX(float translation) { + verify(mSecurityViewFlipper).setTranslationX(translation); + } + + private void assertUserSwitcherTranslationX(float translation) { + ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById( + R.id.keyguard_bouncer_user_switcher); + assertThat(userSwitcher.getTranslationX()).isEqualTo(translation); + } + + private void assertUserSwitcherGravity(@Gravity.GravityFlags int gravity) { + ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById( + R.id.keyguard_bouncer_user_switcher); + assertThat(((FrameLayout.LayoutParams) userSwitcher.getLayoutParams()).gravity) + .isEqualTo(gravity); + } + + private void assertSecurityGravity(@Gravity.GravityFlags int gravity) { + verify(mSecurityViewFlipper, atLeastOnce()).setLayoutParams(mLayoutCaptor.capture()); + assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(gravity); + } + + private void setViewWidth(int width) { + mKeyguardSecurityContainer.setRight(width); + mKeyguardSecurityContainer.setLeft(0); + } + + private MotionEvent touchEventLeftSide() { + return MotionEvent.obtain( + /* downTime= */0, + /* eventTime= */0, + MotionEvent.ACTION_DOWN, + /* x= */VIEW_WIDTH / 3f, + /* y= */0, + /* metaState= */0); + } + + private MotionEvent touchEventRightSide() { + return MotionEvent.obtain( + /* downTime= */0, + /* eventTime= */0, + MotionEvent.ACTION_DOWN, + /* x= */(VIEW_WIDTH / 3f) * 2, + /* y= */0, + /* metaState= */0); + } + private void setupUserSwitcher() { + when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_RIGHT); mKeyguardSecurityContainer.initMode(KeyguardSecurityContainer.MODE_USER_SWITCHER, mGlobalSettings, mFalsingManager, mUserSwitcherController); + // reset mSecurityViewFlipper so setup doesn't influence test verifications + reset(mSecurityViewFlipper); + when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); } private ArrayList<UserRecord> buildUserRecords(int count) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index a35efa995ddd..90609fa2772f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -210,7 +210,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { BOUNDS_POSITION_TOP, mAuthController, mStatusBarStateController, - mKeyguardUpdateMonitor)); + mKeyguardUpdateMonitor, + mExecutor)); mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings, mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt index d5df9fe0c2e8..c48cbb19b40a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt @@ -159,7 +159,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { @Test fun doesNotStartIfAnimationIsCancelled() { val runner = activityLaunchAnimator.createRunner(controller) - runner.onAnimationCancelled() + runner.onAnimationCancelled(false /* isKeyguardOccluded */) runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback) waitForIdleSync() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java index 0fdd9054e4bc..b61bda8edd10 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -468,6 +469,40 @@ public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { verify(mView).setUnpausedAlpha(255); } + @Test + public void testUpdatePanelExpansion_pauseAuth() { + // GIVEN view is attached + on the keyguard + mController.onViewAttached(); + captureStatusBarStateListeners(); + captureStatusBarExpansionListeners(); + sendStatusBarStateChanged(StatusBarState.KEYGUARD); + reset(mView); + + // WHEN panelViewExpansion changes to hide + when(mView.getUnpausedAlpha()).thenReturn(0); + updateStatusBarExpansion(0f, false); + + // THEN pause auth is updated to PAUSE + verify(mView, atLeastOnce()).setPauseAuth(true); + } + + @Test + public void testUpdatePanelExpansion_unpauseAuth() { + // GIVEN view is attached + on the keyguard + panel expansion is 0f + mController.onViewAttached(); + captureStatusBarStateListeners(); + captureStatusBarExpansionListeners(); + sendStatusBarStateChanged(StatusBarState.KEYGUARD); + reset(mView); + + // WHEN panelViewExpansion changes to expanded + when(mView.getUnpausedAlpha()).thenReturn(255); + updateStatusBarExpansion(1f, true); + + // THEN pause auth is updated to NOT pause + verify(mView, atLeastOnce()).setPauseAuth(false); + } + private void sendStatusBarStateChanged(int statusBarState) { mStatusBarStateListener.onStateChanged(statusBarState); } 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 fb64c7b58aac..2adf2857a385 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java @@ -207,4 +207,15 @@ public class DreamOverlayStateControllerTest extends SysuiTestCase { assertThat(complications.contains(weatherComplication)).isFalse(); } } + + @Test + public void testComplicationWithNoTypeNotFiltered() { + final Complication complication = Mockito.mock(Complication.class); + final DreamOverlayStateController stateController = + new DreamOverlayStateController(mExecutor); + stateController.addComplication(complication); + mExecutor.runAllReady(); + assertThat(stateController.getComplications(true).contains(complication)) + .isTrue(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java index dc1ae0e93757..964e6d79f0bf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java @@ -72,19 +72,67 @@ public class SmartSpaceComplicationTest extends SysuiTestCase { } /** - * Ensures {@link SmartSpaceComplication} is only registered when it is available. + * Ensures {@link SmartSpaceComplication} isn't registered right away on start. */ @Test - public void testAvailability() { + public void testRegistrantStart_doesNotAddComplication() { + final SmartSpaceComplication.Registrant registrant = getRegistrant(); + registrant.start(); + verify(mDreamOverlayStateController, never()).addComplication(eq(mComplication)); + } - final SmartSpaceComplication.Registrant registrant = new SmartSpaceComplication.Registrant( + private SmartSpaceComplication.Registrant getRegistrant() { + return new SmartSpaceComplication.Registrant( mContext, mDreamOverlayStateController, mComplication, mSmartspaceController); + } + + @Test + public void testOverlayActive_addsTargetListener() { + final SmartSpaceComplication.Registrant registrant = getRegistrant(); registrant.start(); - verify(mDreamOverlayStateController, never()).addComplication(eq(mComplication)); + final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture()); + + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true); + dreamCallbackCaptor.getValue().onStateChanged(); + + // Test + final ArgumentCaptor<BcSmartspaceDataPlugin.SmartspaceTargetListener> listenerCaptor = + ArgumentCaptor.forClass(BcSmartspaceDataPlugin.SmartspaceTargetListener.class); + verify(mSmartspaceController).addListener(listenerCaptor.capture()); + } + + @Test + public void testOverlayActive_targetsNonEmpty_addsComplication() { + final SmartSpaceComplication.Registrant registrant = getRegistrant(); + registrant.start(); + + final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture()); + + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true); + dreamCallbackCaptor.getValue().onStateChanged(); + + final ArgumentCaptor<BcSmartspaceDataPlugin.SmartspaceTargetListener> listenerCaptor = + ArgumentCaptor.forClass(BcSmartspaceDataPlugin.SmartspaceTargetListener.class); + verify(mSmartspaceController).addListener(listenerCaptor.capture()); + + // Test + final SmartspaceTarget target = Mockito.mock(SmartspaceTarget.class); + listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList(target)); + verify(mDreamOverlayStateController).addComplication(eq(mComplication)); + } + + @Test + public void testOverlayActive_targetsEmpty_removesComplication() { + final SmartSpaceComplication.Registrant registrant = getRegistrant(); + registrant.start(); final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor = ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); @@ -100,10 +148,41 @@ public class SmartSpaceComplicationTest extends SysuiTestCase { final SmartspaceTarget target = Mockito.mock(SmartspaceTarget.class); listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList(target)); verify(mDreamOverlayStateController).addComplication(eq(mComplication)); + + // Test + listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList()); + verify(mDreamOverlayStateController).removeComplication(eq(mComplication)); + } + + @Test + public void testOverlayInActive_removesTargetListener_removesComplication() { + final SmartSpaceComplication.Registrant registrant = getRegistrant(); + registrant.start(); + + final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture()); + + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true); + dreamCallbackCaptor.getValue().onStateChanged(); + + final ArgumentCaptor<BcSmartspaceDataPlugin.SmartspaceTargetListener> listenerCaptor = + ArgumentCaptor.forClass(BcSmartspaceDataPlugin.SmartspaceTargetListener.class); + verify(mSmartspaceController).addListener(listenerCaptor.capture()); + + final SmartspaceTarget target = Mockito.mock(SmartspaceTarget.class); + listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList(target)); + verify(mDreamOverlayStateController).addComplication(eq(mComplication)); + + // Test + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(false); + dreamCallbackCaptor.getValue().onStateChanged(); + verify(mSmartspaceController).removeListener(listenerCaptor.getValue()); + verify(mDreamOverlayStateController).removeComplication(eq(mComplication)); } @Test - public void testGetViewReusesSameView() { + public void testGetView_reusesSameView() { final SmartSpaceComplication complication = new SmartSpaceComplication(getContext(), mSmartspaceController); final Complication.ViewHolder viewHolder = complication.createView(mComplicationViewModel); 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 f00fbe6ac4d7..141a213a5b6a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; @@ -480,4 +481,13 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { // hide dialog again mGlobalActionsDialogLite.showOrHideDialog(true, true, null /* view */); } + + @Test + public void testBugreportAction_whenDebugMode_shouldOfferBugreportButtonBeforeProvisioning() { + doReturn(1).when(mGlobalSettings).getInt(anyString(), anyInt()); + + GlobalActionsDialogLite.BugReportAction bugReportAction = + mGlobalActionsDialogLite.makeBugReportActionForTesting(); + assertThat(bugReportAction.showBeforeProvisioning()).isTrue(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index 11326e76b25e..59475cf0cb90 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -19,6 +19,7 @@ package com.android.systemui.media.dialog; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -51,6 +52,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { private static final String TEST_DEVICE_ID_1 = "test_device_id_1"; private static final String TEST_DEVICE_ID_2 = "test_device_id_2"; private static final String TEST_SESSION_NAME = "test_session_name"; + private static final int TEST_MAX_VOLUME = 20; + private static final int TEST_CURRENT_VOLUME = 10; // Mock private MediaOutputController mMediaOutputController = mock(MediaOutputController.class); @@ -64,12 +67,14 @@ public class MediaOutputAdapterTest extends SysuiTestCase { private MediaOutputAdapter mMediaOutputAdapter; private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder; private List<MediaDevice> mMediaDevices = new ArrayList<>(); + MediaOutputSeekbar mSpyMediaOutputSeekbar; @Before public void setUp() { mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController, mMediaOutputDialog); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); + mSpyMediaOutputSeekbar = spy(mViewHolder.mSeekBar); when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices); when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false); @@ -169,6 +174,16 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test + public void onBindViewHolder_initSeekbar_setsVolume() { + when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME); + when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_CURRENT_VOLUME); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); + + assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_CURRENT_VOLUME); + } + + @Test public void onBindViewHolder_bindNonActiveConnectedDevice_verifyView() { mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index e3b5059131fa..9eaa20c2afed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -32,6 +32,7 @@ import android.media.session.MediaController; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.Bundle; +import android.os.PowerExemptionManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; @@ -86,6 +87,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { NearbyMediaDevicesManager.class); private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); private final AudioManager mAudioManager = mock(AudioManager.class); + private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class); private List<MediaController> mMediaControllers = new ArrayList<>(); private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl; @@ -110,7 +112,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotificationEntryManager, mDialogLaunchAnimator, - Optional.of(mNearbyMediaDevicesManager), mAudioManager); + Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager); mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender, mMediaOutputController); mMediaOutputBaseDialogImpl.onCreate(new Bundle()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index feed3347f3e2..2bf5f0fcbfcb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -19,6 +19,9 @@ package com.android.systemui.media.dialog; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -34,10 +37,12 @@ import android.graphics.drawable.Icon; import android.media.AudioManager; import android.media.MediaDescription; import android.media.MediaMetadata; +import android.media.MediaRoute2Info; import android.media.NearbyDevice; import android.media.RoutingSessionInfo; import android.media.session.MediaController; import android.media.session.MediaSessionManager; +import android.os.PowerExemptionManager; import android.os.RemoteException; import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; @@ -97,6 +102,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { private RoutingSessionInfo mRemoteSessionInfo = mock(RoutingSessionInfo.class); private ActivityStarter mStarter = mock(ActivityStarter.class); private AudioManager mAudioManager = mock(AudioManager.class); + private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class); private CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class); private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock( @@ -125,7 +131,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, - Optional.of(mNearbyMediaDevicesManager), mAudioManager); + Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager); mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; MediaDescription.Builder builder = new MediaDescription.Builder(); @@ -177,7 +183,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, - Optional.of(mNearbyMediaDevicesManager), mAudioManager); + Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager); mMediaOutputController.start(mCb); @@ -206,7 +212,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, - Optional.of(mNearbyMediaDevicesManager), mAudioManager); + Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager); mMediaOutputController.start(mCb); @@ -511,7 +517,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, - Optional.of(mNearbyMediaDevicesManager), mAudioManager); + Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager); assertThat(mMediaOutputController.getNotificationIcon()).isNull(); } @@ -591,4 +597,20 @@ public class MediaOutputControllerTest extends SysuiTestCase { assertThat(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).isTrue(); } + + @Test + public void setTemporaryAllowListExceptionIfNeeded_fromRemoteToBluetooth_addsAllowList() { + when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1); + when(mMediaDevice1.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE); + when(mMediaDevice1.getFeatures()).thenReturn( + ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK)); + when(mMediaDevice2.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE); + + mMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2); + + verify(mPowerExemptionManager).addToTemporaryAllowList(anyString(), anyInt(), anyString(), + anyLong()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java index b16c928adcd4..c45db05bacee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java @@ -29,6 +29,7 @@ import android.media.MediaRoute2Info; import android.media.session.MediaController; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; +import android.os.PowerExemptionManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; @@ -84,6 +85,7 @@ public class MediaOutputDialogTest extends SysuiTestCase { private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock( NearbyMediaDevicesManager.class); private final AudioManager mAudioManager = mock(AudioManager.class); + private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class); private List<MediaController> mMediaControllers = new ArrayList<>(); private MediaOutputDialog mMediaOutputDialog; @@ -103,7 +105,7 @@ public class MediaOutputDialogTest extends SysuiTestCase { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotificationEntryManager, mDialogLaunchAnimator, - Optional.of(mNearbyMediaDevicesManager), mAudioManager); + Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputDialog = new MediaOutputDialog(mContext, false, mBroadcastSender, mMediaOutputController, mUiEventLogger); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java index 379bb4fd6336..4534ae6448ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import android.media.AudioManager; import android.media.session.MediaSessionManager; +import android.os.PowerExemptionManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; @@ -70,6 +71,7 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase { private NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock( NearbyMediaDevicesManager.class); private final AudioManager mAudioManager = mock(AudioManager.class); + private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class); private MediaOutputGroupDialog mMediaOutputGroupDialog; private MediaOutputController mMediaOutputController; @@ -80,7 +82,7 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotificationEntryManager, mDialogLaunchAnimator, - Optional.of(mNearbyMediaDevicesManager), mAudioManager); + Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false, mBroadcastSender, mMediaOutputController); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt index b9a69bb8641a..2eb478303cb2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt @@ -25,6 +25,7 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.WindowManager +import android.view.accessibility.AccessibilityManager import android.widget.ImageView import androidx.test.filters.SmallTest import com.android.systemui.R @@ -65,6 +66,8 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { @Mock private lateinit var logger: MediaTttLogger @Mock + private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var viewUtil: ViewUtil @@ -88,11 +91,21 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { )).thenReturn(applicationInfo) context.setMockPackageManager(packageManager) + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())) + .thenReturn(TIMEOUT_MS.toInt()) + fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) controllerCommon = TestControllerCommon( - context, logger, windowManager, viewUtil, fakeExecutor, tapGestureDetector, powerManager + context, + logger, + windowManager, + viewUtil, + fakeExecutor, + accessibilityManager, + tapGestureDetector, + powerManager ) } @@ -344,6 +357,7 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { windowManager: WindowManager, viewUtil: ViewUtil, @Main mainExecutor: DelayableExecutor, + accessibilityManager: AccessibilityManager, tapGestureDetector: TapGestureDetector, powerManager: PowerManager ) : MediaTttChipControllerCommon<ChipInfo>( @@ -352,23 +366,22 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { windowManager, viewUtil, mainExecutor, + accessibilityManager, tapGestureDetector, powerManager, R.layout.media_ttt_chip ) { - override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) { - - } - - override fun getIconSize(isAppIcon: Boolean): Int? = ICON_SIZE + override val windowLayoutParams = commonWindowLayoutParams + override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) {} + override fun getIconSize(isAppIcon: Boolean): Int = ICON_SIZE } inner class ChipInfo : ChipInfoCommon { - override fun getTimeoutMs() = TIMEOUT_MS + override fun getTimeoutMs() = 1L } } private const val PACKAGE_NAME = "com.android.systemui" private const val APP_NAME = "Fake App Name" private const val TIMEOUT_MS = 10000L -private const val ICON_SIZE = 47
\ No newline at end of file +private const val ICON_SIZE = 47 diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt index 9edc4f4c71c3..bbc564193080 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt @@ -28,6 +28,7 @@ import android.testing.TestableLooper import android.view.View import android.view.ViewGroup import android.view.WindowManager +import android.view.accessibility.AccessibilityManager import android.widget.ImageView import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake @@ -65,6 +66,8 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { @Mock private lateinit var logger: MediaTttLogger @Mock + private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var powerManager: PowerManager @Mock private lateinit var windowManager: WindowManager @@ -99,6 +102,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { windowManager, viewUtil, FakeExecutor(FakeSystemClock()), + accessibilityManager, TapGestureDetector(context), powerManager, Handler.getMain(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt index a8c72ddfd5d7..7ca0cd34ab26 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt @@ -27,6 +27,7 @@ import android.testing.TestableLooper import android.view.View import android.view.ViewGroup import android.view.WindowManager +import android.view.accessibility.AccessibilityManager import android.widget.ImageView import android.widget.TextView import androidx.test.filters.SmallTest @@ -67,6 +68,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { @Mock private lateinit var logger: MediaTttLogger @Mock + private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var powerManager: PowerManager @Mock private lateinit var windowManager: WindowManager @@ -95,9 +98,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) + uiEventLoggerFake = UiEventLoggerFake() senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake) + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) + controllerSender = MediaTttChipControllerSender( commandQueue, context, @@ -105,6 +111,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { windowManager, viewUtil, fakeExecutor, + accessibilityManager, TapGestureDetector(context), powerManager, senderUiEventLogger @@ -592,7 +599,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { fakeClock.advanceTime(1000L) controllerSender.removeChip("fakeRemovalReason") - fakeClock.advanceTime(state.state.timeout + 1) + fakeClock.advanceTime(TIMEOUT + 1L) verify(windowManager).removeView(any()) } @@ -615,7 +622,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { fakeClock.advanceTime(1000L) controllerSender.removeChip("fakeRemovalReason") - fakeClock.advanceTime(state.state.timeout + 1) + fakeClock.advanceTime(TIMEOUT + 1L) verify(windowManager).removeView(any()) } @@ -674,6 +681,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { private const val APP_NAME = "Fake app name" private const val OTHER_DEVICE_NAME = "My Tablet" private const val PACKAGE_NAME = "com.android.systemui" +private const val TIMEOUT = 10000 private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) .addFeature("feature") diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt index 489c8c86028e..bf237abba8fa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt @@ -57,6 +57,7 @@ class QSContainerImplTest : SysuiTestCase() { @Test fun testContainerBottomPadding() { + val originalPadding = qsPanelContainer.paddingBottom qsContainer.updateResources( qsPanelController, quickStatusBarHeaderController @@ -66,7 +67,7 @@ class QSContainerImplTest : SysuiTestCase() { anyInt(), anyInt(), anyInt(), - eq(mContext.resources.getDimensionPixelSize(R.dimen.footer_actions_height)) + eq(originalPadding) ) } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt index 60cfd7249919..b98be75a51c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt @@ -150,6 +150,14 @@ class QSPanelTest : SysuiTestCase() { assertThat(footer.isVisibleToUser).isTrue() } + @Test + fun testBottomPadding() { + val padding = 10 + context.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_bottom, padding) + qsPanel.updatePadding() + assertThat(qsPanel.paddingBottom).isEqualTo(padding) + } + private infix fun View.isLeftOf(other: View): Boolean { val rect = Rect() getBoundsOnScreen(rect) diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt index 2f0f0a0a1b8f..37f96c8d7023 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt @@ -36,17 +36,17 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.concurrent.Executor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Spy import org.mockito.Mockito -import org.mockito.Mockito.`when` import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import java.util.Optional -import java.util.concurrent.Executor +import org.mockito.Spy @SmallTest @RunWith(AndroidTestingRunner::class) @@ -87,6 +87,34 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { private lateinit var controller: DreamSmartspaceController + /** + * A class which implements SmartspaceView and extends View. This is mocked to provide the right + * object inheritance and interface implementation used in DreamSmartspaceController + */ + private class TestView(context: Context?) : View(context), SmartspaceView { + override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {} + + override fun setPrimaryTextColor(color: Int) {} + + override fun setIsDreaming(isDreaming: Boolean) {} + + override fun setDozeAmount(amount: Float) {} + + override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {} + + override fun setFalsingManager(falsingManager: FalsingManager?) {} + + override fun setDnd(image: Drawable?, description: String?) {} + + override fun setNextAlarm(image: Drawable?, description: String?) {} + + override fun setMediaTarget(target: SmartspaceTarget?) {} + + override fun getSelectedPage(): Int { return 0; } + + override fun getCurrentCardTopPadding(): Int { return 0; } + } + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -130,34 +158,6 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { } /** - * A class which implements SmartspaceView and extends View. This is mocked to provide the right - * object inheritance and interface implementation used in DreamSmartspaceController - */ - private class TestView(context: Context?) : View(context), SmartspaceView { - override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {} - - override fun setPrimaryTextColor(color: Int) {} - - override fun setIsDreaming(isDreaming: Boolean) {} - - override fun setDozeAmount(amount: Float) {} - - override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {} - - override fun setFalsingManager(falsingManager: FalsingManager?) {} - - override fun setDnd(image: Drawable?, description: String?) {} - - override fun setNextAlarm(image: Drawable?, description: String?) {} - - override fun setMediaTarget(target: SmartspaceTarget?) {} - - override fun getSelectedPage(): Int { return 0; } - - override fun getCurrentCardTopPadding(): Int { return 0; } - } - - /** * Ensures session begins when a view is attached. */ @Test @@ -180,16 +180,4 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { verify(session).close() } - - /** - * Ensures setIsDreaming(true) is called when the view is built. - */ - @Test - fun testSetIsDreamingTrueOnViewCreate() { - `when`(precondition.conditionsMet()).thenReturn(true) - - controller.buildAndConnectView(Mockito.mock(ViewGroup::class.java)) - - verify(smartspaceView).setIsDreaming(true) - } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt index b4cae38d8b6e..d0cf792b698d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt @@ -74,9 +74,9 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { // Verify ripple added to window manager. captor.value.onBatteryLevelChanged( - 0 /* unusedBatteryLevel */, - true /* plugged in */, - false /* charging */) + /* unusedBatteryLevel= */ 0, + /* plugged in= */ true, + /* charging= */ false) val attachListenerCaptor = ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java) verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture()) @@ -144,4 +144,22 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { // Verify that ripple is triggered. verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any()) } + + @Test + fun testRipple_whenDocked_doesNotPlayRipple() { + `when`(batteryController.isChargingSourceDock).thenReturn(true) + val captor = ArgumentCaptor + .forClass(BatteryController.BatteryStateChangeCallback::class.java) + verify(batteryController).addCallback(captor.capture()) + + captor.value.onBatteryLevelChanged( + /* unusedBatteryLevel= */ 0, + /* plugged in= */ true, + /* charging= */ false) + + val attachListenerCaptor = + ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java) + verify(rippleView, never()).addOnAttachStateChangeListener(attachListenerCaptor.capture()) + verify(windowManager, never()).addView(eq(rippleView), any<WindowManager.LayoutParams>()) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java index 7d06abf5cd67..3fc0c8176f2f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java @@ -63,7 +63,7 @@ public class DynamicPrivacyControllerTest extends SysuiTestCase { mock(StatusBarKeyguardViewManager.class)); mDynamicPrivacyController.addListener(mListener); // Disable dynamic privacy by default - allowPrivateNotificationsInPublic(true); + allowNotificationsInPublic(false); } @Test @@ -108,24 +108,21 @@ public class DynamicPrivacyControllerTest extends SysuiTestCase { @Test public void dynamicPrivacyOnlyWhenHidingPrivate() { - // Verify that when only hiding notifications, this isn't enabled - allowPrivateNotificationsInPublic(true); - when(mLockScreenUserManager.shouldHideNotifications(any())).thenReturn( - false); - assertFalse("Dynamic privacy shouldn't be enabled when only hiding notifications", + // Verify that when hiding notifications, this isn't enabled + allowNotificationsInPublic(false); + assertFalse("Dynamic privacy shouldn't be enabled when hiding notifications", mDynamicPrivacyController.isDynamicPrivacyEnabled()); - allowPrivateNotificationsInPublic(false); - assertTrue("Should be enabled when hiding notification contents", + allowNotificationsInPublic(true); + assertTrue("Should be enabled whenever notifications are visible", mDynamicPrivacyController.isDynamicPrivacyEnabled()); } private void enableDynamicPrivacy() { - allowPrivateNotificationsInPublic(false); + allowNotificationsInPublic(true); } - private void allowPrivateNotificationsInPublic(boolean allow) { - when(mLockScreenUserManager.userAllowsPrivateNotificationsInPublic(anyInt())).thenReturn( - allow); + private void allowNotificationsInPublic(boolean allow) { + when(mLockScreenUserManager.userAllowsNotificationsInPublic(anyInt())).thenReturn(allow); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java index 4507366e3073..ee7d55864931 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java @@ -85,6 +85,11 @@ public class NoManSimulator { mRankings.put(key, ranking); } + /** This is for testing error cases: b/216384850 */ + public Ranking removeRankingWithoutEvent(String key) { + return mRankings.remove(key); + } + private RankingMap buildRankingMap() { return new RankingMap(mRankings.values().toArray(new Ranking[0])); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index 958d54230f1c..af43826091a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -1492,6 +1492,80 @@ public class NotifCollectionTest extends SysuiTestCase { } @Test + public void testMissingRankingWhenRemovalFeatureIsDisabled() { + // GIVEN a pipeline with one two notifications + when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false); + String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key; + String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key; + NotificationEntry entry1 = mCollectionListener.getEntry(key1); + NotificationEntry entry2 = mCollectionListener.getEntry(key2); + clearInvocations(mCollectionListener); + + // GIVEN the message for removing key1 gets does not reach NotifCollection + Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1); + // WHEN the message for removing key2 arrives + mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL); + + // THEN only entry2 gets removed + verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL)); + verify(mCollectionListener).onEntryCleanUp(eq(entry2)); + verify(mCollectionListener).onRankingApplied(); + verifyNoMoreInteractions(mCollectionListener); + verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any()); + verify(mLogger, never()).logRecoveredRankings(any()); + clearInvocations(mCollectionListener, mLogger); + + // WHEN a ranking update includes key1 again + mNoMan.setRanking(key1, ranking1); + mNoMan.issueRankingUpdate(); + + // VERIFY that we do nothing but log the 'recovery' + verify(mCollectionListener).onRankingUpdate(any()); + verify(mCollectionListener).onRankingApplied(); + verifyNoMoreInteractions(mCollectionListener); + verify(mLogger, never()).logMissingRankings(any(), anyInt(), any()); + verify(mLogger).logRecoveredRankings(eq(List.of(key1))); + } + + @Test + public void testMissingRankingWhenRemovalFeatureIsEnabled() { + // GIVEN a pipeline with one two notifications + when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true); + String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key; + String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key; + NotificationEntry entry1 = mCollectionListener.getEntry(key1); + NotificationEntry entry2 = mCollectionListener.getEntry(key2); + clearInvocations(mCollectionListener); + + // GIVEN the message for removing key1 gets does not reach NotifCollection + Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1); + // WHEN the message for removing key2 arrives + mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL); + + // THEN both entry1 and entry2 get removed + verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL)); + verify(mCollectionListener).onEntryRemoved(eq(entry1), eq(REASON_UNKNOWN)); + verify(mCollectionListener).onEntryCleanUp(eq(entry2)); + verify(mCollectionListener).onEntryCleanUp(eq(entry1)); + verify(mCollectionListener).onRankingApplied(); + verifyNoMoreInteractions(mCollectionListener); + verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any()); + verify(mLogger, never()).logRecoveredRankings(any()); + clearInvocations(mCollectionListener, mLogger); + + // WHEN a ranking update includes key1 again + mNoMan.setRanking(key1, ranking1); + mNoMan.issueRankingUpdate(); + + // VERIFY that we do nothing but log the 'recovery' + verify(mCollectionListener).onRankingUpdate(any()); + verify(mCollectionListener).onRankingApplied(); + verifyNoMoreInteractions(mCollectionListener); + verify(mLogger, never()).logMissingRankings(any(), anyInt(), any()); + verify(mLogger).logRecoveredRankings(eq(List.of(key1))); + } + + @Test public void testRegisterFutureDismissal() throws RemoteException { // GIVEN a pipeline with one notification NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); @@ -1610,9 +1684,9 @@ public class NotifCollectionTest extends SysuiTestCase { return new CollectionEvent(rawEvent, requireNonNull(mEntryCaptor.getValue())); } - private void verifyBuiltList(Collection<NotificationEntry> list) { - verify(mBuildListener).onBuildList(mBuildListCaptor.capture()); - assertEquals(new ArraySet<>(list), new ArraySet<>(mBuildListCaptor.getValue())); + private void verifyBuiltList(Collection<NotificationEntry> expectedList) { + verify(mBuildListener).onBuildList(mBuildListCaptor.capture(), any()); + assertThat(mBuildListCaptor.getValue()).containsExactly(expectedList.toArray()); } private static class RecordingCollectionListener implements NotifCollectionListener { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index 769143ddbc0d..d4add7547656 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -108,6 +108,7 @@ public class NotificationEntryTest extends SysuiTestCase { @Test public void testBlockableEntryWhenCritical() { doReturn(true).when(mChannel).isBlockable(); + mEntry.setRanking(mEntry.getRanking()); assertTrue(mEntry.isBlockable()); } @@ -117,6 +118,7 @@ public class NotificationEntryTest extends SysuiTestCase { public void testBlockableEntryWhenCriticalAndChannelNotBlockable() { doReturn(true).when(mChannel).isBlockable(); doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction(); + mEntry.setRanking(mEntry.getRanking()); assertTrue(mEntry.isBlockable()); } @@ -125,6 +127,7 @@ public class NotificationEntryTest extends SysuiTestCase { public void testNonBlockableEntryWhenCriticalAndChannelNotBlockable() { doReturn(false).when(mChannel).isBlockable(); doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction(); + mEntry.setRanking(mEntry.getRanking()); assertFalse(mEntry.isBlockable()); } @@ -164,6 +167,9 @@ public class NotificationEntryTest extends SysuiTestCase { doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction(); doReturn(false).when(mChannel).isBlockable(); + mEntry.setRanking(mEntry.getRanking()); + + assertFalse(mEntry.isBlockable()); assertTrue(mEntry.isExemptFromDndVisualSuppression()); assertFalse(mEntry.shouldSuppressAmbient()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index 4e7e79f2cb26..555adfdfdc31 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -53,6 +53,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.NotificationInteractionTracker; +import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.collection.ShadeListBuilder.OnRenderListListener; import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection; @@ -569,7 +570,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { assertTrue(entry.hasFinishedInitialization()); // WHEN the pipeline is kicked off - mReadyForBuildListener.onBuildList(singletonList(entry)); + mReadyForBuildListener.onBuildList(singletonList(entry), "test"); mPipelineChoreographer.runIfScheduled(); // THEN the entry's initialization time is reset @@ -1797,6 +1798,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { @Test public void testStableMultipleSectionOrdering() { + // WHEN the list is originally built with reordering disabled mListBuilder.setSectioners(asList( new PackageSectioner(PACKAGE_1), new PackageSectioner(PACKAGE_2))); mStabilityManager.setAllowEntryReordering(false); @@ -1807,12 +1809,94 @@ public class ShadeListBuilderTest extends SysuiTestCase { addNotif(3, PACKAGE_1).setRank(3); dispatchBuild(); + // VERIFY the order and that entry reordering has not been suppressed verifyBuiltList( notif(0), notif(1), notif(3), notif(2) ); + verify(mStabilityManager, never()).onEntryReorderSuppressed(); + + // WHEN the ranks change + setNewRank(notif(0).entry, 4); + dispatchBuild(); + + // VERIFY the order does not change that entry reordering has been suppressed + verifyBuiltList( + notif(0), + notif(1), + notif(3), + notif(2) + ); + verify(mStabilityManager).onEntryReorderSuppressed(); + + // WHEN reordering is now allowed again + mStabilityManager.setAllowEntryReordering(true); + dispatchBuild(); + + // VERIFY that list order changes + verifyBuiltList( + notif(1), + notif(3), + notif(0), + notif(2) + ); + } + + @Test + public void testStableChildOrdering() { + // WHEN the list is originally built with reordering disabled + mStabilityManager.setAllowEntryReordering(false); + addGroupSummary(0, PACKAGE_1, GROUP_1).setRank(0); + addGroupChild(1, PACKAGE_1, GROUP_1).setRank(1); + addGroupChild(2, PACKAGE_1, GROUP_1).setRank(2); + addGroupChild(3, PACKAGE_1, GROUP_1).setRank(3); + dispatchBuild(); + + // VERIFY the order and that entry reordering has not been suppressed + verifyBuiltList( + group( + summary(0), + child(1), + child(2), + child(3) + ) + ); + verify(mStabilityManager, never()).onEntryReorderSuppressed(); + + // WHEN the ranks change + setNewRank(notif(2).entry, 5); + dispatchBuild(); + + // VERIFY the order does not change that entry reordering has been suppressed + verifyBuiltList( + group( + summary(0), + child(1), + child(2), + child(3) + ) + ); + verify(mStabilityManager).onEntryReorderSuppressed(); + + // WHEN reordering is now allowed again + mStabilityManager.setAllowEntryReordering(true); + dispatchBuild(); + + // VERIFY that list order changes + verifyBuiltList( + group( + summary(0), + child(1), + child(3), + child(2) + ) + ); + } + + private static void setNewRank(NotificationEntry entry, int rank) { + entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build()); } @Test @@ -2008,7 +2092,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { mPendingSet.clear(); } - mReadyForBuildListener.onBuildList(mEntrySet); + mReadyForBuildListener.onBuildList(mEntrySet, "test"); mPipelineChoreographer.runIfScheduled(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java deleted file mode 100644 index d082d74078db..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.collection.coordinator; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import android.os.Handler; -import android.os.UserHandle; -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.notification.SectionHeaderVisibilityProvider; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; -import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; -import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider; -import com.android.systemui.statusbar.policy.KeyguardStateController; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * TODO(b/224771204) Create test cases - */ -@SmallTest -@RunWith(AndroidTestingRunner.class) -@Ignore -public class KeyguardCoordinatorTest extends SysuiTestCase { - private static final int NOTIF_USER_ID = 0; - private static final int CURR_USER_ID = 1; - - @Mock private Handler mMainHandler; - @Mock private KeyguardStateController mKeyguardStateController; - @Mock private BroadcastDispatcher mBroadcastDispatcher; - @Mock private StatusBarStateController mStatusBarStateController; - @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock private HighPriorityProvider mHighPriorityProvider; - @Mock private SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider; - @Mock private NotifPipeline mNotifPipeline; - @Mock private KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; - - private NotificationEntry mEntry; - private NotifFilter mKeyguardFilter; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - KeyguardCoordinator keyguardCoordinator = new KeyguardCoordinator( - mStatusBarStateController, - mKeyguardUpdateMonitor, mHighPriorityProvider, mSectionHeaderVisibilityProvider, - mKeyguardNotificationVisibilityProvider, mock(SharedCoordinatorLogger.class)); - - mEntry = new NotificationEntryBuilder() - .setUser(new UserHandle(NOTIF_USER_ID)) - .build(); - - ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class); - keyguardCoordinator.attach(mNotifPipeline); - verify(mNotifPipeline, times(1)).addFinalizeFilter(filterCaptor.capture()); - mKeyguardFilter = filterCaptor.getValue(); - } -} 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 new file mode 100644 index 000000000000..8c506a6d16ae --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.coordinator + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider +import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.withArgCaptor +import java.util.function.Consumer +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class KeyguardCoordinatorTest : SysuiTestCase() { + private val notifPipeline: NotifPipeline = mock() + private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() + private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock() + private val sharedCoordinatorLogger: SharedCoordinatorLogger = mock() + private val statusBarStateController: StatusBarStateController = mock() + + private lateinit var onStateChangeListener: Consumer<String> + private lateinit var keyguardFilter: NotifFilter + + @Before + fun setup() { + val keyguardCoordinator = KeyguardCoordinator( + keyguardNotifVisibilityProvider, + sectionHeaderVisibilityProvider, + sharedCoordinatorLogger, + statusBarStateController + ) + keyguardCoordinator.attach(notifPipeline) + onStateChangeListener = withArgCaptor { + verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) + } + keyguardFilter = withArgCaptor { + verify(notifPipeline).addFinalizeFilter(capture()) + } + } + + @Test + fun testSetSectionHeadersVisibleInShade() { + clearInvocations(sectionHeaderVisibilityProvider) + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + onStateChangeListener.accept("state change") + verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(true) + } + + @Test + fun testSetSectionHeadersNotVisibleOnKeyguard() { + clearInvocations(sectionHeaderVisibilityProvider) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + onStateChangeListener.accept("state change") + verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index 72d8ff3f35f5..a6d3719de5cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.when; import static java.util.Objects.requireNonNull; +import android.os.Handler; import android.os.RemoteException; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -42,7 +43,6 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.RankingBuilder; -import com.android.systemui.statusbar.notification.SectionClassifier; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder; import com.android.systemui.statusbar.notification.collection.ListEntry; @@ -57,8 +57,10 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; +import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider; import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Test; @@ -97,8 +99,10 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Mock private IStatusBarService mService; @Mock private BindEventManagerImpl mBindEventManagerImpl; @Mock private NotificationLockscreenUserManager mLockscreenUserManager; + @Mock private Handler mHandler; + @Mock private SecureSettings mSecureSettings; @Spy private FakeNotifInflater mNotifInflater = new FakeNotifInflater(); - private final SectionClassifier mSectionClassifier = new SectionClassifier(); + private final SectionStyleProvider mSectionStyleProvider = new SectionStyleProvider(); private NotifUiAdjustmentProvider mAdjustmentProvider; @@ -110,8 +114,11 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mAdjustmentProvider = - new NotifUiAdjustmentProvider(mLockscreenUserManager, mSectionClassifier); + mAdjustmentProvider = new NotifUiAdjustmentProvider( + mHandler, + mSecureSettings, + mLockscreenUserManager, + mSectionStyleProvider); mEntry = getNotificationEntryBuilder().setParent(ROOT_ENTRY).build(); mInflationError = new Exception(TEST_MESSAGE); mErrorManager = new NotifInflationErrorManager(); @@ -495,7 +502,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { private static final int TEST_MAX_GROUP_DELAY = 100; private void setSectionIsLowPriority(boolean minimized) { - mSectionClassifier.setMinimizedSections(minimized + mSectionStyleProvider.setMinimizedSections(minimized ? Collections.singleton(mNotifSection.getSectioner()) : Collections.emptyList()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java index 15c1cb7b123a..50b3fc7e1c42 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java @@ -42,7 +42,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SbnBuilder; -import com.android.systemui.statusbar.notification.SectionClassifier; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -50,6 +49,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; +import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider; import com.android.systemui.statusbar.notification.collection.render.NodeController; import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; @@ -70,7 +70,7 @@ public class RankingCoordinatorTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock private HighPriorityProvider mHighPriorityProvider; - @Mock private SectionClassifier mSectionClassifier; + @Mock private SectionStyleProvider mSectionStyleProvider; @Mock private NotifPipeline mNotifPipeline; @Mock private NodeController mAlertingHeaderController; @Mock private NodeController mSilentNodeController; @@ -94,7 +94,7 @@ public class RankingCoordinatorTest extends SysuiTestCase { mRankingCoordinator = new RankingCoordinator( mStatusBarStateController, mHighPriorityProvider, - mSectionClassifier, + mSectionStyleProvider, mAlertingHeaderController, mSilentHeaderController, mSilentNodeController); @@ -102,7 +102,7 @@ public class RankingCoordinatorTest extends SysuiTestCase { mEntry.setRanking(getRankingForUnfilteredNotif().build()); mRankingCoordinator.attach(mNotifPipeline); - verify(mSectionClassifier).setMinimizedSections(any()); + verify(mSectionStyleProvider).setMinimizedSections(any()); verify(mNotifPipeline, times(2)).addPreGroupFilter(mNotifFilterCaptor.capture()); mCapturedSuspendedFilter = mNotifFilterCaptor.getAllValues().get(0); mCapturedDozingFilter = mNotifFilterCaptor.getAllValues().get(1); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt index 447ba1510e13..3f3de009fb04 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt @@ -21,13 +21,13 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.AssistantFeedbackController import com.android.systemui.statusbar.notification.FeedbackIcon -import com.android.systemui.statusbar.notification.SectionClassifier import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderEntryListener import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener +import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider import com.android.systemui.statusbar.notification.collection.render.NotifRowController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -37,8 +37,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations.initMocks import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations.initMocks @SmallTest @RunWith(AndroidTestingRunner::class) @@ -53,7 +53,7 @@ class RowAppearanceCoordinatorTest : SysuiTestCase() { @Mock private lateinit var pipeline: NotifPipeline @Mock private lateinit var assistantFeedbackController: AssistantFeedbackController - @Mock private lateinit var sectionClassifier: SectionClassifier + @Mock private lateinit var sectionStyleProvider: SectionStyleProvider @Mock private lateinit var section1: NotifSection @Mock private lateinit var section2: NotifSection @@ -66,7 +66,7 @@ class RowAppearanceCoordinatorTest : SysuiTestCase() { coordinator = RowAppearanceCoordinator( mContext, assistantFeedbackController, - sectionClassifier + sectionStyleProvider ) coordinator.attach(pipeline) beforeRenderListListener = withArgCaptor { @@ -82,8 +82,8 @@ class RowAppearanceCoordinatorTest : SysuiTestCase() { @Test fun testSetSystemExpandedOnlyOnFirst() { - whenever(sectionClassifier.isMinimizedSection(eq(section1))).thenReturn(false) - whenever(sectionClassifier.isMinimizedSection(eq(section1))).thenReturn(false) + whenever(sectionStyleProvider.isMinimizedSection(eq(section1))).thenReturn(false) + whenever(sectionStyleProvider.isMinimizedSection(eq(section1))).thenReturn(false) beforeRenderListListener.onBeforeRenderList(listOf(entry1, entry2)) afterRenderEntryListener.onAfterRenderEntry(entry1, controller1) verify(controller1).setSystemExpanded(eq(true)) @@ -93,8 +93,8 @@ class RowAppearanceCoordinatorTest : SysuiTestCase() { @Test fun testSetSystemExpandedNeverIfMinimized() { - whenever(sectionClassifier.isMinimizedSection(eq(section1))).thenReturn(true) - whenever(sectionClassifier.isMinimizedSection(eq(section1))).thenReturn(true) + whenever(sectionStyleProvider.isMinimizedSection(eq(section1))).thenReturn(true) + whenever(sectionStyleProvider.isMinimizedSection(eq(section1))).thenReturn(true) beforeRenderListListener.onBeforeRenderList(listOf(entry1, entry2)) afterRenderEntryListener.onAfterRenderEntry(entry1, controller1) verify(controller1).setSystemExpanded(eq(false)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt index dd15caebf9e6..246943e3088e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt @@ -1,29 +1,87 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.systemui.statusbar.notification.collection.inflation +import android.database.ContentObserver +import android.os.Handler +import android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.NotificationLockscreenUserManager -import com.android.systemui.statusbar.notification.SectionClassifier +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection +import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider +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.android.systemui.util.settings.SecureSettings +import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.inOrder import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidTestingRunner::class) @RunWithLooper class NotifUiAdjustmentProviderTest : SysuiTestCase() { private val lockscreenUserManager: NotificationLockscreenUserManager = mock() - private val sectionClassifier: SectionClassifier = mock() + private val sectionStyleProvider: SectionStyleProvider = mock() + private val handler: Handler = mock() + private val secureSettings: SecureSettings = mock() + private val uri = FakeSettings().getUriFor(SHOW_NOTIFICATION_SNOOZE) + private val dirtyListener: Runnable = mock() + + private val section = NotifSection(mock(), 0) + private val entry = NotificationEntryBuilder() + .setSection(section) + .setParent(GroupEntry.ROOT_ENTRY) + .build() + + private lateinit var contentObserver: ContentObserver private val adjustmentProvider = NotifUiAdjustmentProvider( + handler, + secureSettings, lockscreenUserManager, - sectionClassifier, + sectionStyleProvider, ) + @Before + fun setup() { + verifyNoMoreInteractions(secureSettings) + adjustmentProvider.addDirtyListener(dirtyListener) + verify(secureSettings).getInt(eq(SHOW_NOTIFICATION_SNOOZE), any()) + contentObserver = withArgCaptor { + verify(secureSettings).registerContentObserverForUser( + eq(SHOW_NOTIFICATION_SNOOZE), capture(), any() + ) + } + verifyNoMoreInteractions(secureSettings, dirtyListener) + } + @Test fun notifLockscreenStateChangeWillNotifDirty() { val dirtyListener = mock<Runnable>() @@ -33,6 +91,35 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { verify(lockscreenUserManager).addNotificationStateChangedListener(capture()) } notifLocksreenStateChangeListener.onNotificationStateChanged() - verify(dirtyListener).run(); + verify(dirtyListener).run() + } + + @Test + fun additionalAddDoesNotRegisterAgain() { + clearInvocations(secureSettings) + adjustmentProvider.addDirtyListener(mock()) + verifyNoMoreInteractions(secureSettings) + } + + @Test + fun onChangeWillQueryThenNotifyDirty() { + contentObserver.onChange(false, listOf(uri), 0, 0) + with(inOrder(secureSettings, dirtyListener)) { + verify(secureSettings).getInt(eq(SHOW_NOTIFICATION_SNOOZE), any()) + verify(dirtyListener).run() + } + } + + @Test + fun changingSnoozeChangesProvidedAdjustment() { + whenever(secureSettings.getInt(eq(SHOW_NOTIFICATION_SNOOZE), any())).thenReturn(0) + val original = adjustmentProvider.calculateAdjustment(entry) + assertThat(original.isSnoozeEnabled).isFalse() + + whenever(secureSettings.getInt(eq(SHOW_NOTIFICATION_SNOOZE), any())).thenReturn(1) + contentObserver.onChange(false, listOf(uri), 0, 0) + val withSnoozing = adjustmentProvider.calculateAdjustment(entry) + assertThat(withSnoozing.isSnoozeEnabled).isTrue() + assertThat(withSnoozing).isNotEqualTo(original) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt new file mode 100644 index 000000000000..6c07174bce65 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.notifcollection + +import android.service.notification.NotificationListenerService.RankingMap +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class NotifCollectionLoggerTest : SysuiTestCase() { + private val logger: NotifCollectionLogger = mock() + private val entry1: NotificationEntry = NotificationEntryBuilder().setId(1).build() + private val entry2: NotificationEntry = NotificationEntryBuilder().setId(2).build() + + private fun mapOfEntries(vararg entries: NotificationEntry): Map<String, NotificationEntry> = + entries.associateBy { it.key } + + private fun rankingMapOf(vararg entries: NotificationEntry): RankingMap = + RankingMap(entries.map { it.ranking }.toTypedArray()) + + @Test + fun testMaybeLogInconsistentRankings_logsNewlyInconsistentRanking() { + val rankingMap = rankingMapOf(entry1) + maybeLogInconsistentRankings( + logger = logger, + oldKeysWithoutRankings = emptySet(), + newEntriesWithoutRankings = mapOfEntries(entry2), + rankingMap = rankingMap + ) + verify(logger).logMissingRankings( + newlyInconsistentEntries = eq(listOf(entry2)), + totalInconsistent = eq(1), + rankingMap = eq(rankingMap), + ) + verifyNoMoreInteractions(logger) + } + + @Test + fun testMaybeLogInconsistentRankings_doesNotLogAlreadyInconsistentRanking() { + maybeLogInconsistentRankings( + logger = logger, + oldKeysWithoutRankings = setOf(entry2.key), + newEntriesWithoutRankings = mapOfEntries(entry2), + rankingMap = rankingMapOf(entry1) + ) + verifyNoMoreInteractions(logger) + } + + @Test + fun testMaybeLogInconsistentRankings_logsWhenRankingIsAdded() { + maybeLogInconsistentRankings( + logger = logger, + oldKeysWithoutRankings = setOf(entry2.key), + newEntriesWithoutRankings = mapOfEntries(), + rankingMap = rankingMapOf(entry1, entry2) + ) + verify(logger).logRecoveredRankings( + newlyConsistentKeys = eq(listOf(entry2.key)), + ) + verifyNoMoreInteractions(logger) + } + + @Test + fun testMaybeLogInconsistentRankings_doesNotLogsWhenEntryIsRemoved() { + maybeLogInconsistentRankings( + logger = logger, + oldKeysWithoutRankings = setOf(entry2.key), + newEntriesWithoutRankings = mapOfEntries(), + rankingMap = rankingMapOf(entry1) + ) + verifyNoMoreInteractions(logger) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt index 0e1865861cae..ac254abe60b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.collection.render import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager -import com.android.systemui.statusbar.notification.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.ListEntry @@ -28,6 +27,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.collection.getAttachState import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.statusbar.notification.stack.BUCKET_PEOPLE import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT @@ -413,4 +413,4 @@ private fun buildSection( return nodeController } }, index) -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java index 0d5a5fe086a3..3f641df376ed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java @@ -72,32 +72,32 @@ public class HeadsUpViewBinderTest extends SysuiTestCase { }); mViewBinder.bindHeadsUpView(mEntry, null); - verify(mLogger).startBindingHun(eq("key")); + verify(mLogger).startBindingHun(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); callback.get().onBindFinished(mEntry); - verify(mLogger).entryBoundSuccessfully(eq("key")); + verify(mLogger).entryBoundSuccessfully(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); mViewBinder.bindHeadsUpView(mEntry, null); - verify(mLogger).startBindingHun(eq("key")); + verify(mLogger).startBindingHun(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); callback.get().onBindFinished(mEntry); - verify(mLogger).entryBoundSuccessfully(eq("key")); + verify(mLogger).entryBoundSuccessfully(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); mViewBinder.unbindHeadsUpView(mEntry); - verify(mLogger).entryContentViewMarkedFreeable(eq("key")); + verify(mLogger).entryContentViewMarkedFreeable(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); callback.get().onBindFinished(mEntry); - verify(mLogger).entryUnbound(eq("key")); + verify(mLogger).entryUnbound(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); } @@ -111,12 +111,12 @@ public class HeadsUpViewBinderTest extends SysuiTestCase { }); mViewBinder.bindHeadsUpView(mEntry, null); - verify(mLogger).startBindingHun(eq("key")); + verify(mLogger).startBindingHun(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); mViewBinder.abortBindCallback(mEntry); - verify(mLogger).currentOngoingBindingAborted(eq("key")); + verify(mLogger).currentOngoingBindingAborted(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); @@ -135,18 +135,18 @@ public class HeadsUpViewBinderTest extends SysuiTestCase { }); mViewBinder.bindHeadsUpView(mEntry, null); - verify(mLogger).startBindingHun(eq("key")); + verify(mLogger).startBindingHun(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); mViewBinder.unbindHeadsUpView(mEntry); - verify(mLogger).currentOngoingBindingAborted(eq("key")); - verify(mLogger).entryContentViewMarkedFreeable(eq("key")); + verify(mLogger).currentOngoingBindingAborted(eq(mEntry)); + verify(mLogger).entryContentViewMarkedFreeable(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); callback.get().onBindFinished(mEntry); - verify(mLogger).entryUnbound(eq("key")); + verify(mLogger).entryUnbound(eq(mEntry)); verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionControllerTest.kt index b24b348ac316..cafe113e7872 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionControllerTest.kt @@ -5,6 +5,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.ScrimController import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent import com.android.systemui.statusbar.policy.FakeConfigurationController @@ -13,6 +15,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -21,6 +24,7 @@ class ScrimShadeTransitionControllerTest : SysuiTestCase() { @Mock private lateinit var scrimController: ScrimController @Mock private lateinit var dumpManager: DumpManager + @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController private val configurationController = FakeConfigurationController() private lateinit var controller: ScrimShadeTransitionController @@ -31,9 +35,14 @@ class ScrimShadeTransitionControllerTest : SysuiTestCase() { context.ensureTestableResources() controller = ScrimShadeTransitionController( - configurationController, dumpManager, scrimController, context.resources + configurationController, + dumpManager, + scrimController, + context.resources, + statusBarStateController ) } + @Test fun onPanelExpansionChanged_inSingleShade_setsFractionEqualToEventFraction() { setSplitShadeEnabled(false) @@ -44,7 +53,9 @@ class ScrimShadeTransitionControllerTest : SysuiTestCase() { } @Test - fun onPanelExpansionChanged_inSplitShade_setsFractionBasedOnDragDownAmount() { + fun onPanelExpansionChanged_inSplitShade_unlockedShade_setsFractionBasedOnDragDownAmount() { + whenever(statusBarStateController.currentOrUpcomingState) + .thenReturn(StatusBarState.SHADE) val scrimShadeTransitionDistance = context.resources.getDimensionPixelSize(R.dimen.split_shade_scrim_transition_distance) setSplitShadeEnabled(true) @@ -55,6 +66,54 @@ class ScrimShadeTransitionControllerTest : SysuiTestCase() { verify(scrimController).setRawPanelExpansionFraction(expectedFraction) } + @Test + fun onPanelExpansionChanged_inSplitShade_largeDragDownAmount_fractionIsNotGreaterThan1() { + whenever(statusBarStateController.currentOrUpcomingState) + .thenReturn(StatusBarState.SHADE) + val scrimShadeTransitionDistance = + context.resources.getDimensionPixelSize(R.dimen.split_shade_scrim_transition_distance) + setSplitShadeEnabled(true) + + controller.onPanelExpansionChanged( + EXPANSION_EVENT.copy(dragDownPxAmount = 100f * scrimShadeTransitionDistance) + ) + + verify(scrimController).setRawPanelExpansionFraction(1f) + } + + @Test + fun onPanelExpansionChanged_inSplitShade_negativeDragDownAmount_fractionIsNotLessThan0() { + whenever(statusBarStateController.currentOrUpcomingState) + .thenReturn(StatusBarState.SHADE) + setSplitShadeEnabled(true) + + controller.onPanelExpansionChanged(EXPANSION_EVENT.copy(dragDownPxAmount = -100f)) + + verify(scrimController).setRawPanelExpansionFraction(0f) + } + + @Test + fun onPanelExpansionChanged_inSplitShade_onLockedShade_setsFractionEqualToEventFraction() { + whenever(statusBarStateController.currentOrUpcomingState) + .thenReturn(StatusBarState.SHADE_LOCKED) + setSplitShadeEnabled(true) + + controller.onPanelExpansionChanged(EXPANSION_EVENT) + + verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction) + } + + @Test + fun onPanelExpansionChanged_inSplitShade_onKeyguard_setsFractionEqualToEventFraction() { + whenever(statusBarStateController.currentOrUpcomingState) + .thenReturn(StatusBarState.KEYGUARD) + setSplitShadeEnabled(true) + + controller.onPanelExpansionChanged(EXPANSION_EVENT) + + verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction) + } + private fun setSplitShadeEnabled(enabled: Boolean) { overrideResource(R.bool.config_use_split_notification_shade, enabled) configurationController.notifyConfigurationChanged() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java index fec2123b304a..fda80a2f9ed8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Intent; +import android.os.BatteryManager; import android.os.Handler; import android.os.PowerManager; import android.os.PowerSaveState; @@ -196,4 +197,26 @@ public class BatteryControllerTest extends SysuiTestCase { TestableLooper.get(this).processAllMessages(); // Should not throw an exception } + + @Test + public void batteryStateChanged_withChargingSourceDock_isChargingSourceDockTrue() { + Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); + intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING); + intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_DOCK); + + mBatteryController.onReceive(getContext(), intent); + + Assert.assertTrue(mBatteryController.isChargingSourceDock()); + } + + @Test + public void batteryStateChanged_withChargingSourceNotDock_isChargingSourceDockFalse() { + Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); + intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING); + intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_WIRELESS); + + mBatteryController.onReceive(getContext(), intent); + + Assert.assertFalse(mBatteryController.isChargingSourceDock()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt new file mode 100644 index 000000000000..db0029af4ee2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import android.content.pm.PackageManager +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.hardware.camera2.impl.CameraMetadataNative +import android.test.suitebuilder.annotation.SmallTest +import android.testing.AndroidTestingRunner +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.time.FakeSystemClock +import java.util.concurrent.Executor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.eq +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class FlashlightControllerImplTest : SysuiTestCase() { + + @Mock + private lateinit var dumpManager: DumpManager + + @Mock + private lateinit var cameraManager: CameraManager + + @Mock + private lateinit var broadcastSender: BroadcastSender + + @Mock + private lateinit var packageManager: PackageManager + + private lateinit var fakeSettings: FakeSettings + private lateinit var fakeSystemClock: FakeSystemClock + private lateinit var backgroundExecutor: FakeExecutor + private lateinit var controller: FlashlightControllerImpl + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + fakeSystemClock = FakeSystemClock() + backgroundExecutor = FakeExecutor(fakeSystemClock) + fakeSettings = FakeSettings() + + `when`(packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) + .thenReturn(true) + + controller = FlashlightControllerImpl( + dumpManager, + cameraManager, + backgroundExecutor, + fakeSettings, + broadcastSender, + packageManager + ) + } + + @Test + fun testNoCameraManagerInteractionDirectlyOnConstructor() { + verifyZeroInteractions(cameraManager) + } + + @Test + fun testCameraManagerInitAfterConstructionOnExecutor() { + injectCamera() + backgroundExecutor.runAllReady() + + verify(cameraManager).registerTorchCallback(eq(backgroundExecutor), any()) + } + + @Test + fun testNoCallbackIfNoFlashCamera() { + injectCamera(flash = false) + backgroundExecutor.runAllReady() + + verify(cameraManager, never()).registerTorchCallback(any<Executor>(), any()) + } + + @Test + fun testNoCallbackIfNoBackCamera() { + injectCamera(facing = CameraCharacteristics.LENS_FACING_FRONT) + backgroundExecutor.runAllReady() + + verify(cameraManager, never()).registerTorchCallback(any<Executor>(), any()) + } + + @Test + fun testSetFlashlightInBackgroundExecutor() { + val id = injectCamera() + backgroundExecutor.runAllReady() + + clearInvocations(cameraManager) + val enable = !controller.isEnabled + controller.setFlashlight(enable) + verifyNoMoreInteractions(cameraManager) + + backgroundExecutor.runAllReady() + verify(cameraManager).setTorchMode(id, enable) + } + + private fun injectCamera( + flash: Boolean = true, + facing: Int = CameraCharacteristics.LENS_FACING_BACK + ): String { + val cameraID = "ID" + val camera = CameraCharacteristics(CameraMetadataNative().apply { + set(CameraCharacteristics.FLASH_INFO_AVAILABLE, flash) + set(CameraCharacteristics.LENS_FACING, facing) + }) + `when`(cameraManager.cameraIdList).thenReturn(arrayOf(cameraID)) + `when`(cameraManager.getCameraCharacteristics(cameraID)).thenReturn(camera) + return cameraID + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java index 424a40058997..b8e25ab43691 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java @@ -106,7 +106,7 @@ public class HeadsUpManagerTest extends AlertingNotificationManagerTest { public void testHunRemovedLogging() { mAlertEntry.mEntry = mEntry; mHeadsUpManager.onAlertEntryRemoved(mAlertEntry); - verify(mLogger, times(1)).logNotificationActuallyRemoved(eq(mEntry.getKey())); + verify(mLogger, times(1)).logNotificationActuallyRemoved(eq(mEntry)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java index 1e35b0f0e68a..125b3627b342 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java @@ -159,6 +159,24 @@ public class ConditionMonitorTest extends SysuiTestCase { Mockito.clearInvocations(callback); } + // Ensure that updating a callback that is removed doesn't result in an exception due to the + // absence of the condition. + @Test + public void testUpdateRemovedCallback() { + final Monitor.Callback callback1 = + mock(Monitor.Callback.class); + final Monitor.Subscription.Token subscription1 = + mConditionMonitor.addSubscription(getDefaultBuilder(callback1).build()); + ArgumentCaptor<Condition.Callback> monitorCallback = + ArgumentCaptor.forClass(Condition.Callback.class); + mExecutor.runAllReady(); + verify(mCondition1).addCallback(monitorCallback.capture()); + // This will execute first before the handler for onConditionChanged. + mConditionMonitor.removeSubscription(subscription1); + monitorCallback.getValue().onConditionChanged(mCondition1); + mExecutor.runAllReady(); + } + @Test public void addCallback_addFirstCallback_addCallbackToAllConditions() { final Monitor.Callback callback1 = diff --git a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java index 229799a8457d..d5991d3930a8 100644 --- a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java +++ b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java @@ -73,6 +73,9 @@ class AssociationStoreImpl implements AssociationStore { private final Set<OnChangeListener> mListeners = new LinkedHashSet<>(); void addAssociation(@NonNull AssociationInfo association) { + // Validity check first. + checkNotRevoked(association); + final int id = association.getId(); if (DEBUG) { @@ -99,6 +102,9 @@ class AssociationStoreImpl implements AssociationStore { } void updateAssociation(@NonNull AssociationInfo updated) { + // Validity check first. + checkNotRevoked(updated); + final int id = updated.getId(); if (DEBUG) { @@ -292,6 +298,9 @@ class AssociationStoreImpl implements AssociationStore { } void setAssociations(Collection<AssociationInfo> allAssociations) { + // Validity check first. + allAssociations.forEach(AssociationStoreImpl::checkNotRevoked); + if (DEBUG) { Log.i(TAG, "setAssociations() n=" + allAssociations.size()); final StringJoiner stringJoiner = new StringJoiner(", "); @@ -324,4 +333,11 @@ class AssociationStoreImpl implements AssociationStore { mAddressMap.clear(); mCachedPerUser.clear(); } + + private static void checkNotRevoked(@NonNull AssociationInfo association) { + if (association.isRevoked()) { + throw new IllegalArgumentException( + "Revoked (removed) associations MUST NOT appear in the AssociationStore"); + } + } } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 3f7cba6a4d09..abc49372053e 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -18,6 +18,7 @@ package com.android.server.companion; import static android.Manifest.permission.MANAGE_COMPANION_DEVICES; +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; import static android.content.pm.PackageManager.CERT_INPUT_SHA256; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.SYSTEM_UID; @@ -48,6 +49,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.app.ActivityManager.RunningAppProcessInfo; import android.app.ActivityManagerInternal; import android.app.AppOpsManager; import android.app.NotificationManager; @@ -91,6 +94,7 @@ import android.util.SparseBooleanArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsService; import com.android.internal.content.PackageMonitor; +import com.android.internal.infra.PerUser; import com.android.internal.notification.NotificationAccessConfirmationActivityContract; import com.android.internal.os.BackgroundThread; import com.android.internal.util.ArrayUtils; @@ -100,10 +104,12 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.companion.presence.CompanionDevicePresenceMonitor; import com.android.server.pm.UserManagerInternal; +import com.android.server.wm.ActivityTaskManagerInternal; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -127,6 +133,9 @@ public class CompanionDeviceManagerService extends SystemService { private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90); + private final ActivityManager mActivityManager; + private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener; + private PersistentDataStore mPersistentStore; private final PersistUserStateHandler mUserPersistenceHandler; @@ -135,6 +144,7 @@ public class CompanionDeviceManagerService extends SystemService { private CompanionDevicePresenceMonitor mDevicePresenceMonitor; private CompanionApplicationController mCompanionAppController; + private final ActivityTaskManagerInternal mAtmInternal; private final ActivityManagerInternal mAmInternal; private final IAppOpsService mAppOpsManager; private final PowerWhitelistManager mPowerWhitelistManager; @@ -150,21 +160,53 @@ public class CompanionDeviceManagerService extends SystemService { @GuardedBy("mPreviouslyUsedIds") private final SparseArray<Map<String, Set<Integer>>> mPreviouslyUsedIds = new SparseArray<>(); + /** + * A structure that consists of a set of revoked associations that pending for role holder + * removal per each user. + * + * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo) + * @see #addToPendingRoleHolderRemoval(AssociationInfo) + * @see #removeFromPendingRoleHolderRemoval(AssociationInfo) + * @see #getPendingRoleHolderRemovalAssociationsForUser(int) + */ + @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval") + private final PerUserAssociationSet mRevokedAssociationsPendingRoleHolderRemoval = + new PerUserAssociationSet(); + /** + * Contains uid-s of packages pending to be removed from the role holder list (after + * revocation of an association), which will happen one the package is no longer visible to the + * user. + * For quicker uid -> (userId, packageName) look-up this is not a {@code Set<Integer>} but + * a {@code Map<Integer, String>} which maps uid-s to packageName-s (userId-s can be derived + * from uid-s using {@link UserHandle#getUserId(int)}). + * + * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo) + * @see #addToPendingRoleHolderRemoval(AssociationInfo) + * @see #removeFromPendingRoleHolderRemoval(AssociationInfo) + */ + @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval") + private final Map<Integer, String> mUidsPendingRoleHolderRemoval = new HashMap<>(); + private final RemoteCallbackList<IOnAssociationsChangedListener> mListeners = new RemoteCallbackList<>(); public CompanionDeviceManagerService(Context context) { super(context); + mActivityManager = context.getSystemService(ActivityManager.class); mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class); mAppOpsManager = IAppOpsService.Stub.asInterface( ServiceManager.getService(Context.APP_OPS_SERVICE)); + mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class); mAmInternal = LocalServices.getService(ActivityManagerInternal.class); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mUserManager = context.getSystemService(UserManager.class); mUserPersistenceHandler = new PersistUserStateHandler(); mAssociationStore = new AssociationStoreImpl(); + + mOnPackageVisibilityChangeListener = + new OnPackageVisibilityChangeListener(mActivityManager); } @Override @@ -201,7 +243,33 @@ public class CompanionDeviceManagerService extends SystemService { mUserManager.getAliveUsers(), allAssociations, mPreviouslyUsedIds); } - mAssociationStore.setAssociations(allAssociations); + final Set<AssociationInfo> activeAssociations = + new ArraySet<>(/* capacity */ allAssociations.size()); + // A set contains the userIds that need to persist state after remove the app + // from the list of role holders. + final Set<Integer> usersToPersistStateFor = new ArraySet<>(); + + for (AssociationInfo association : allAssociations) { + if (!association.isRevoked()) { + activeAssociations.add(association); + } else if (maybeRemoveRoleHolderForAssociation(association)) { + // Nothing more to do here, but we'll need to persist all the associations to the + // disk afterwards. + usersToPersistStateFor.add(association.getUserId()); + } else { + addToPendingRoleHolderRemoval(association); + } + } + + mAssociationStore.setAssociations(activeAssociations); + + // IMPORTANT: only do this AFTER mAssociationStore.setAssociations(), because + // persistStateForUser() queries AssociationStore. + // (If persistStateForUser() is invoked before mAssociationStore.setAssociations() it + // would effectively just clear-out all the persisted associations). + for (int userId : usersToPersistStateFor) { + persistStateForUser(userId); + } } @Override @@ -351,10 +419,18 @@ public class CompanionDeviceManagerService extends SystemService { } private void persistStateForUser(@UserIdInt int userId) { - final List<AssociationInfo> updatedAssociations = - mAssociationStore.getAssociationsForUser(userId); + // We want to store both active associations and the revoked (removed) association that we + // are keeping around for the final clean-up (delayed role holder removal). + final List<AssociationInfo> allAssociations; + // Start with the active associations - these we can get from the AssociationStore. + allAssociations = new ArrayList<>( + mAssociationStore.getAssociationsForUser(userId)); + // ... and add the revoked (removed) association, that are yet to be permanently removed. + allAssociations.addAll(getPendingRoleHolderRemovalAssociationsForUser(userId)); + final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId); - mPersistentStore.persistStateForUser(userId, updatedAssociations, usedIdsForUser); + + mPersistentStore.persistStateForUser(userId, allAssociations, usedIdsForUser); } private void notifyListeners( @@ -422,13 +498,17 @@ public class CompanionDeviceManagerService extends SystemService { removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT; } - for (AssociationInfo ai : mAssociationStore.getAssociations()) { - if (!ai.isSelfManaged()) continue; - final boolean isInactive = currentTime - ai.getLastTimeConnectedMs() >= removalWindow; - if (isInactive) { - Slog.i(TAG, "Removing inactive self-managed association: " + ai.getId()); - disassociateInternal(ai.getId()); - } + for (AssociationInfo association : mAssociationStore.getAssociations()) { + if (!association.isSelfManaged()) continue; + + final boolean isInactive = + currentTime - association.getLastTimeConnectedMs() >= removalWindow; + if (!isInactive) continue; + + final int id = association.getId(); + + Slog.i(TAG, "Removing inactive self-managed association id=" + id); + disassociateInternal(id); } } @@ -668,7 +748,7 @@ public class CompanionDeviceManagerService extends SystemService { enforceCallerIsSystemOr(userId, packageName); AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress( - userId, packageName, deviceAddress); + userId, packageName, deviceAddress); if (association == null) { throw new RemoteException(new DeviceNotAssociatedException("App " + packageName @@ -728,7 +808,7 @@ public class CompanionDeviceManagerService extends SystemService { enforceUsesCompanionDeviceFeature(getContext(), userId, callingPackage); checkState(!ArrayUtils.isEmpty( - mAssociationStore.getAssociationsForPackage(userId, callingPackage)), + mAssociationStore.getAssociationsForPackage(userId, callingPackage)), "App must have an association before calling this API"); } @@ -788,8 +868,8 @@ public class CompanionDeviceManagerService extends SystemService { final long timestamp = System.currentTimeMillis(); final AssociationInfo association = new AssociationInfo(id, userId, packageName, - macAddress, displayName, deviceProfile, selfManaged, false, timestamp, - Long.MAX_VALUE); + macAddress, displayName, deviceProfile, selfManaged, + /* notifyOnDeviceNearby */ false, /* revoked */ false, timestamp, Long.MAX_VALUE); Slog.i(TAG, "New CDM association created=" + association); mAssociationStore.addAssociation(association); @@ -801,6 +881,11 @@ public class CompanionDeviceManagerService extends SystemService { updateSpecialAccessPermissionForAssociatedPackage(association); logCreateAssociation(deviceProfile); + + // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since + // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case + // that there are other devices with the same profile, so the role holder won't be removed. + return association; } @@ -881,36 +966,184 @@ public class CompanionDeviceManagerService extends SystemService { final String packageName = association.getPackageName(); final String deviceProfile = association.getDeviceProfile(); + if (!maybeRemoveRoleHolderForAssociation(association)) { + // Need to remove the app from list of the role holders, but will have to do it later + // (the app is in foreground at the moment). + addToPendingRoleHolderRemoval(association); + } + + // Need to check if device still present now because CompanionDevicePresenceMonitor will + // remove current connected device after mAssociationStore.removeAssociation final boolean wasPresent = mDevicePresenceMonitor.isDevicePresent(associationId); // Removing the association. mAssociationStore.removeAssociation(associationId); + // Do not need to persistUserState since CompanionDeviceManagerService will get callback + // from #onAssociationChanged, and it will handle the persistUserState which including + // active and revoked association. logRemoveAssociation(deviceProfile); - final List<AssociationInfo> otherAssociations = - mAssociationStore.getAssociationsForPackage(userId, packageName); - - // Check if the package is associated with other devices with the same profile. - // If not: take away the role. - if (deviceProfile != null) { - final boolean shouldKeepTheRole = any(otherAssociations, - it -> deviceProfile.equals(it.getDeviceProfile())); - if (!shouldKeepTheRole) { - Binder.withCleanCallingIdentity(() -> - removeRoleHolderForAssociation(getContext(), association)); - } - } - if (!wasPresent || !association.isNotifyOnDeviceNearby()) return; // The device was connected and the app was notified: check if we need to unbind the app // now. - final boolean shouldStayBound = any(otherAssociations, + final boolean shouldStayBound = any( + mAssociationStore.getAssociationsForPackage(userId, packageName), it -> it.isNotifyOnDeviceNearby() && mDevicePresenceMonitor.isDevicePresent(it.getId())); if (shouldStayBound) return; mCompanionAppController.unbindCompanionApplication(userId, packageName); } + /** + * First, checks if the companion application should be removed from the list role holders when + * upon association's removal, i.e.: association's profile (matches the role) is not null, + * the application does not have other associations with the same profile, etc. + * + * <p> + * Then, if establishes that the application indeed has to be removed from the list of the role + * holders, checks if it could be done right now - + * {@link android.app.role.RoleManager#removeRoleHolderAsUser(String, String, int, UserHandle, java.util.concurrent.Executor, java.util.function.Consumer) RoleManager#removeRoleHolderAsUser()} + * will kill the application's process, which leads poor user experience if the application was + * in foreground when this happened, to avoid this CDMS delays invoking + * {@code RoleManager.removeRoleHolderAsUser()} until the app is no longer in foreground. + * + * @return {@code true} if the application does NOT need be removed from the list of the role + * holders OR if the application was successfully removed from the list of role holders. + * I.e.: from the role-management perspective the association is done with. + * {@code false} if the application needs to be removed from the list of role the role + * holders, BUT it CDMS would prefer to do it later. + * I.e.: application is in the foreground at the moment, but invoking + * {@code RoleManager.removeRoleHolderAsUser()} will kill the application's process, + * which would lead to the poor UX, hence need to try later. + */ + + private boolean maybeRemoveRoleHolderForAssociation(@NonNull AssociationInfo association) { + if (DEBUG) Log.d(TAG, "maybeRemoveRoleHolderForAssociation() association=" + association); + + final String deviceProfile = association.getDeviceProfile(); + if (deviceProfile == null) { + // No role was granted to for this association, there is nothing else we need to here. + return true; + } + + // Check if the applications is associated with another devices with the profile. If so, + // it should remain the role holder. + final int id = association.getId(); + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + final boolean roleStillInUse = any( + mAssociationStore.getAssociationsForPackage(userId, packageName), + it -> deviceProfile.equals(it.getDeviceProfile()) && id != it.getId()); + if (roleStillInUse) { + // Application should remain a role holder, there is nothing else we need to here. + return true; + } + + final int packageProcessImportance = getPackageProcessImportance(userId, packageName); + if (packageProcessImportance <= IMPORTANCE_VISIBLE) { + // Need to remove the app from the list of role holders, but the process is visible to + // the user at the moment, so we'll need to it later: log and return false. + Slog.i(TAG, "Cannot remove role holder for the removed association id=" + id + + " now - process is visible."); + return false; + } + + removeRoleHolderForAssociation(getContext(), association); + return true; + } + + private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) { + return Binder.withCleanCallingIdentity(() -> { + final int uid = + mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); + return mActivityManager.getUidImportance(uid); + }); + } + + /** + * Set revoked flag for active association and add the revoked association and the uid into + * the caches. + * + * @see #mRevokedAssociationsPendingRoleHolderRemoval + * @see #mUidsPendingRoleHolderRemoval + * @see OnPackageVisibilityChangeListener + */ + private void addToPendingRoleHolderRemoval(@NonNull AssociationInfo association) { + // First: set revoked flag. + association = AssociationInfo.builder(association) + .setRevoked(true) + .build(); + + final String packageName = association.getPackageName(); + final int userId = association.getUserId(); + final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); + + // Second: add to the set. + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + mRevokedAssociationsPendingRoleHolderRemoval.forUser(association.getUserId()) + .add(association); + if (!mUidsPendingRoleHolderRemoval.containsKey(uid)) { + mUidsPendingRoleHolderRemoval.put(uid, packageName); + + if (mUidsPendingRoleHolderRemoval.size() == 1) { + // Just added first uid: start the listener + mOnPackageVisibilityChangeListener.startListening(); + } + } + } + } + + /** + * Remove the revoked association form the cache and also remove the uid form the map if + * there are other associations with the same package still pending for role holder removal. + * + * @see #mRevokedAssociationsPendingRoleHolderRemoval + * @see #mUidsPendingRoleHolderRemoval + * @see OnPackageVisibilityChangeListener + */ + private void removeFromPendingRoleHolderRemoval(@NonNull AssociationInfo association) { + final String packageName = association.getPackageName(); + final int userId = association.getUserId(); + final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); + + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId) + .remove(association); + + final boolean shouldKeepUidForRemoval = any( + getPendingRoleHolderRemovalAssociationsForUser(userId), + ai -> packageName.equals(ai.getPackageName())); + // Do not remove the uid form the map since other associations with + // the same packageName still pending for role holder removal. + if (!shouldKeepUidForRemoval) { + mUidsPendingRoleHolderRemoval.remove(uid); + } + + if (mUidsPendingRoleHolderRemoval.isEmpty()) { + // The set is empty now - can "turn off" the listener. + mOnPackageVisibilityChangeListener.stopListening(); + } + } + } + + /** + * @return a copy of the revoked associations set (safeguarding against + * {@code ConcurrentModificationException}-s). + */ + private @NonNull Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser( + @UserIdInt int userId) { + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + // Return a copy. + return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId)); + } + } + + private String getPackageNameByUid(int uid) { + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + return mUidsPendingRoleHolderRemoval.get(uid); + } + } + private void updateSpecialAccessPermissionForAssociatedPackage(AssociationInfo association) { final PackageInfo packageInfo = getPackageInfo(getContext(), association.getUserId(), association.getPackageName()); @@ -969,6 +1202,9 @@ public class CompanionDeviceManagerService extends SystemService { companionAppUids.add(uid); } } + if (mAtmInternal != null) { + mAtmInternal.setCompanionAppUids(userId, companionAppUids); + } if (mAmInternal != null) { // Make a copy of the set and send it to ActivityManager. mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids)); @@ -1128,4 +1364,80 @@ public class CompanionDeviceManagerService extends SystemService { persistStateForUser(userId); } } + + /** + * An OnUidImportanceListener class which watches the importance of the packages. + * In this class, we ONLY interested in the importance of the running process is greater than + * {@link RunningAppProcessInfo.IMPORTANCE_VISIBLE} for the uids have been added into the + * {@link mUidsPendingRoleHolderRemoval}. Lastly remove the role holder for the revoked + * associations for the same packages. + * + * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo) + * @see #removeFromPendingRoleHolderRemoval(AssociationInfo) + * @see #getPendingRoleHolderRemovalAssociationsForUser(int) + */ + private class OnPackageVisibilityChangeListener implements + ActivityManager.OnUidImportanceListener { + final @NonNull ActivityManager mAm; + + OnPackageVisibilityChangeListener(@NonNull ActivityManager am) { + this.mAm = am; + } + + void startListening() { + Binder.withCleanCallingIdentity( + () -> mAm.addOnUidImportanceListener( + /* listener */ OnPackageVisibilityChangeListener.this, + RunningAppProcessInfo.IMPORTANCE_VISIBLE)); + } + + void stopListening() { + Binder.withCleanCallingIdentity( + () -> mAm.removeOnUidImportanceListener( + /* listener */ OnPackageVisibilityChangeListener.this)); + } + + @Override + public void onUidImportance(int uid, int importance) { + if (importance <= RunningAppProcessInfo.IMPORTANCE_VISIBLE) { + // The lower the importance value the more "important" the process is. + // We are only interested when the process ceases to be visible. + return; + } + + final String packageName = getPackageNameByUid(uid); + if (packageName == null) { + // Not interested in this uid. + return; + } + + final int userId = UserHandle.getUserId(uid); + + boolean needToPersistStateForUser = false; + + for (AssociationInfo association : + getPendingRoleHolderRemovalAssociationsForUser(userId)) { + if (!packageName.equals(association.getPackageName())) continue; + + if (!maybeRemoveRoleHolderForAssociation(association)) { + // Did not remove the role holder, will have to try again later. + continue; + } + + removeFromPendingRoleHolderRemoval(association); + needToPersistStateForUser = true; + } + + if (needToPersistStateForUser) { + mUserPersistenceHandler.postPersistUserState(userId); + } + } + } + + private static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> { + @Override + protected @NonNull Set<AssociationInfo> create(int userId) { + return new ArraySet<>(); + } + } } diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java index 36393894f727..4b56c1b28036 100644 --- a/services/companion/java/com/android/server/companion/PersistentDataStore.java +++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java @@ -103,7 +103,7 @@ import java.util.concurrent.ConcurrentMap; * Since Android T the data is stored to "companion_device_manager.xml" file in * {@link Environment#getDataSystemDeDirectory(int) /data/system_de/}. * - * See {@link #getBaseStorageFileForUser(int) getBaseStorageFileForUser()} + * See {@link DataStoreUtils#getBaseStorageFileForUser(int, String)} * * <p> * Since Android T the data is stored using the v1 schema. @@ -120,7 +120,7 @@ import java.util.concurrent.ConcurrentMap; * <li> {@link #readPreviouslyUsedIdsV1(TypedXmlPullParser, Map) readPreviouslyUsedIdsV1()} * </ul> * - * The following snippet is a sample of a file that is using v0 schema. + * The following snippet is a sample of a file that is using v1 schema. * <pre>{@code * <state persistence-version="1"> * <associations> @@ -130,6 +130,8 @@ import java.util.concurrent.ConcurrentMap; * mac_address="AA:BB:CC:DD:EE:00" * self_managed="false" * notify_device_nearby="false" + * revoked="false" + * last_time_connected="1634641160229" * time_approved="1634389553216"/> * * <association @@ -139,6 +141,8 @@ import java.util.concurrent.ConcurrentMap; * display_name="Jhon's Chromebook" * self_managed="true" * notify_device_nearby="false" + * revoked="false" + * last_time_connected="1634641160229" * time_approved="1634641160229"/> * </associations> * @@ -178,6 +182,7 @@ final class PersistentDataStore { private static final String XML_ATTR_PROFILE = "profile"; private static final String XML_ATTR_SELF_MANAGED = "self_managed"; private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby"; + private static final String XML_ATTR_REVOKED = "revoked"; private static final String XML_ATTR_TIME_APPROVED = "time_approved"; private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected"; @@ -415,7 +420,8 @@ final class PersistentDataStore { out.add(new AssociationInfo(associationId, userId, appPackage, MacAddress.fromString(deviceAddress), null, profile, - /* managedByCompanionApp */false, notify, timeApproved, Long.MAX_VALUE)); + /* managedByCompanionApp */ false, notify, /* revoked */ false, timeApproved, + Long.MAX_VALUE)); } private static void readAssociationsV1(@NonNull TypedXmlPullParser parser, @@ -444,13 +450,14 @@ final class PersistentDataStore { final String displayName = readStringAttribute(parser, XML_ATTR_DISPLAY_NAME); final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED); final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); + final boolean revoked = readBooleanAttribute(parser, XML_ATTR_REVOKED, false); final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); final long lastTimeConnected = readLongAttribute( parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE); final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId, - appPackage, macAddress, displayName, profile, selfManaged, notify, timeApproved, - lastTimeConnected); + appPackage, macAddress, displayName, profile, selfManaged, notify, revoked, + timeApproved, lastTimeConnected); if (associationInfo != null) { out.add(associationInfo); } @@ -503,6 +510,8 @@ final class PersistentDataStore { writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged()); writeBooleanAttribute( serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby()); + writeBooleanAttribute( + serializer, XML_ATTR_REVOKED, a.isRevoked()); writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs()); writeLongAttribute( serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs()); @@ -544,11 +553,12 @@ final class PersistentDataStore { private static AssociationInfo createAssociationInfoNoThrow(int associationId, @UserIdInt int userId, @NonNull String appPackage, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String profile, boolean selfManaged, - boolean notify, long timeApproved, long lastTimeConnected) { + boolean notify, boolean revoked, long timeApproved, long lastTimeConnected) { AssociationInfo associationInfo = null; try { associationInfo = new AssociationInfo(associationId, userId, appPackage, macAddress, - displayName, profile, selfManaged, notify, timeApproved, lastTimeConnected); + displayName, profile, selfManaged, notify, revoked, timeApproved, + lastTimeConnected); } catch (Exception e) { if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e); } diff --git a/services/companion/java/com/android/server/companion/RolesUtils.java b/services/companion/java/com/android/server/companion/RolesUtils.java index 35488a80b78b..0fff3f488562 100644 --- a/services/companion/java/com/android/server/companion/RolesUtils.java +++ b/services/companion/java/com/android/server/companion/RolesUtils.java @@ -85,6 +85,8 @@ final class RolesUtils { final int userId = associationInfo.getUserId(); final UserHandle userHandle = UserHandle.of(userId); + Slog.i(TAG, "Removing CDM role holder, role=" + deviceProfile + + ", package=u" + userId + "\\" + packageName); roleManager.removeRoleHolderAsUser(deviceProfile, packageName, MANAGE_HOLDERS_FLAG_DONT_KILL_APP, userHandle, context.getMainExecutor(), success -> { diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java index 297d28dadde3..56990eda3e78 100644 --- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java +++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java @@ -19,7 +19,7 @@ package com.android.server.adb; import static com.android.internal.util.dump.DumpUtils.writeStringIfNotNull; import android.annotation.NonNull; -import android.annotation.TestApi; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.Notification; import android.app.NotificationChannel; @@ -102,11 +102,26 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** - * Provides communication to the Android Debug Bridge daemon to allow, deny, or clear public keysi + * Provides communication to the Android Debug Bridge daemon to allow, deny, or clear public keys * that are authorized to connect to the ADB service itself. + * + * <p>The AdbDebuggingManager controls two files: + * <ol> + * <li>adb_keys + * <li>adb_temp_keys.xml + * </ol> + * + * <p>The ADB Daemon (adbd) reads <em>only</em> the adb_keys file for authorization. Public keys + * from registered hosts are stored in adb_keys, one entry per line. + * + * <p>AdbDebuggingManager also keeps adb_temp_keys.xml, which is used for two things + * <ol> + * <li>Removing unused keys from the adb_keys file + * <li>Managing authorized WiFi access points for ADB over WiFi + * </ol> */ public class AdbDebuggingManager { - private static final String TAG = "AdbDebuggingManager"; + private static final String TAG = AdbDebuggingManager.class.getSimpleName(); private static final boolean DEBUG = false; private static final boolean MDNS_DEBUG = false; @@ -118,18 +133,20 @@ public class AdbDebuggingManager { // as a subsequent connection occurs within the allowed duration. private static final String ADB_TEMP_KEYS_FILE = "adb_temp_keys.xml"; private static final int BUFFER_SIZE = 65536; + private static final Ticker SYSTEM_TICKER = () -> System.currentTimeMillis(); private final Context mContext; private final ContentResolver mContentResolver; - private final Handler mHandler; - private AdbDebuggingThread mThread; + @VisibleForTesting final AdbDebuggingHandler mHandler; + @Nullable private AdbDebuggingThread mThread; private boolean mAdbUsbEnabled = false; private boolean mAdbWifiEnabled = false; private String mFingerprints; // A key can be used more than once (e.g. USB, wifi), so need to keep a refcount - private final Map<String, Integer> mConnectedKeys; - private String mConfirmComponent; - private final File mTestUserKeyFile; + private final Map<String, Integer> mConnectedKeys = new HashMap<>(); + private final String mConfirmComponent; + @Nullable private final File mUserKeyFile; + @Nullable private final File mTempKeysFile; private static final String WIFI_PERSISTENT_CONFIG_PROPERTY = "persist.adb.tls_server.enable"; @@ -138,37 +155,44 @@ public class AdbDebuggingManager { private static final int PAIRING_CODE_LENGTH = 6; private PairingThread mPairingThread = null; // A list of keys connected via wifi - private final Set<String> mWifiConnectedKeys; + private final Set<String> mWifiConnectedKeys = new HashSet<>(); // The current info of the adbwifi connection. - private AdbConnectionInfo mAdbConnectionInfo; + private AdbConnectionInfo mAdbConnectionInfo = new AdbConnectionInfo(); // Polls for a tls port property when adb wifi is enabled private AdbConnectionPortPoller mConnectionPortPoller; private final PortListenerImpl mPortListener = new PortListenerImpl(); + private final Ticker mTicker; public AdbDebuggingManager(Context context) { - mHandler = new AdbDebuggingHandler(FgThread.get().getLooper()); - mContext = context; - mContentResolver = mContext.getContentResolver(); - mTestUserKeyFile = null; - mConnectedKeys = new HashMap<String, Integer>(); - mWifiConnectedKeys = new HashSet<String>(); - mAdbConnectionInfo = new AdbConnectionInfo(); + this( + context, + /* confirmComponent= */ null, + getAdbFile(ADB_KEYS_FILE), + getAdbFile(ADB_TEMP_KEYS_FILE), + /* adbDebuggingThread= */ null, + SYSTEM_TICKER); } /** * Constructor that accepts the component to be invoked to confirm if the user wants to allow * an adb connection from the key. */ - @TestApi - protected AdbDebuggingManager(Context context, String confirmComponent, File testUserKeyFile) { - mHandler = new AdbDebuggingHandler(FgThread.get().getLooper()); + @VisibleForTesting + AdbDebuggingManager( + Context context, + String confirmComponent, + File testUserKeyFile, + File tempKeysFile, + AdbDebuggingThread adbDebuggingThread, + Ticker ticker) { mContext = context; mContentResolver = mContext.getContentResolver(); mConfirmComponent = confirmComponent; - mTestUserKeyFile = testUserKeyFile; - mConnectedKeys = new HashMap<String, Integer>(); - mWifiConnectedKeys = new HashSet<String>(); - mAdbConnectionInfo = new AdbConnectionInfo(); + mUserKeyFile = testUserKeyFile; + mTempKeysFile = tempKeysFile; + mThread = adbDebuggingThread; + mTicker = ticker; + mHandler = new AdbDebuggingHandler(FgThread.get().getLooper(), mThread); } static void sendBroadcastWithDebugPermission(@NonNull Context context, @NonNull Intent intent, @@ -189,8 +213,7 @@ public class AdbDebuggingManager { // consisting of only letters, digits, and hyphens, must begin and end // with a letter or digit, must not contain consecutive hyphens, and // must contain at least one letter. - @VisibleForTesting - static final String SERVICE_PROTOCOL = "adb-tls-pairing"; + @VisibleForTesting static final String SERVICE_PROTOCOL = "adb-tls-pairing"; private final String mServiceType = String.format("_%s._tcp.", SERVICE_PROTOCOL); private int mPort; @@ -352,16 +375,24 @@ public class AdbDebuggingManager { } } - class AdbDebuggingThread extends Thread { + @VisibleForTesting + static class AdbDebuggingThread extends Thread { private boolean mStopped; private LocalSocket mSocket; private OutputStream mOutputStream; private InputStream mInputStream; + private Handler mHandler; + @VisibleForTesting AdbDebuggingThread() { super(TAG); } + @VisibleForTesting + void setHandler(Handler handler) { + mHandler = handler; + } + @Override public void run() { if (DEBUG) Slog.d(TAG, "Entering thread"); @@ -536,7 +567,7 @@ public class AdbDebuggingManager { } } - class AdbConnectionInfo { + private static class AdbConnectionInfo { private String mBssid; private String mSsid; private int mPort; @@ -743,11 +774,14 @@ public class AdbDebuggingManager { // Notification when adbd socket is disconnected. static final int MSG_ADBD_SOCKET_DISCONNECTED = 27; + // === Messages from other parts of the system + private static final int MESSAGE_KEY_FILES_UPDATED = 28; + // === Messages we can send to adbd =========== static final String MSG_DISCONNECT_DEVICE = "DD"; static final String MSG_DISABLE_ADBDWIFI = "DA"; - private AdbKeyStore mAdbKeyStore; + @Nullable @VisibleForTesting AdbKeyStore mAdbKeyStore; // Usb, Wi-Fi transports can be enabled together or separately, so don't break the framework // connection unless all transport types are disconnected. @@ -762,19 +796,19 @@ public class AdbDebuggingManager { } }; - AdbDebuggingHandler(Looper looper) { + /** Constructor that accepts the AdbDebuggingThread to which responses should be sent. */ + @VisibleForTesting + AdbDebuggingHandler(Looper looper, AdbDebuggingThread thread) { super(looper); + mThread = thread; } - /** - * Constructor that accepts the AdbDebuggingThread to which responses should be sent - * and the AdbKeyStore to be used to store the temporary grants. - */ - @TestApi - AdbDebuggingHandler(Looper looper, AdbDebuggingThread thread, AdbKeyStore adbKeyStore) { - super(looper); - mThread = thread; - mAdbKeyStore = adbKeyStore; + /** Initialize the AdbKeyStore so tests can grab mAdbKeyStore immediately. */ + @VisibleForTesting + void initKeyStore() { + if (mAdbKeyStore == null) { + mAdbKeyStore = new AdbKeyStore(); + } } // Show when at least one device is connected. @@ -805,6 +839,7 @@ public class AdbDebuggingManager { registerForAuthTimeChanges(); mThread = new AdbDebuggingThread(); + mThread.setHandler(mHandler); mThread.start(); mAdbKeyStore.updateKeyStore(); @@ -825,8 +860,7 @@ public class AdbDebuggingManager { if (!mConnectedKeys.isEmpty()) { for (Map.Entry<String, Integer> entry : mConnectedKeys.entrySet()) { - mAdbKeyStore.setLastConnectionTime(entry.getKey(), - System.currentTimeMillis()); + mAdbKeyStore.setLastConnectionTime(entry.getKey(), mTicker.currentTimeMillis()); } sendPersistKeyStoreMessage(); mConnectedKeys.clear(); @@ -836,9 +870,7 @@ public class AdbDebuggingManager { } public void handleMessage(Message msg) { - if (mAdbKeyStore == null) { - mAdbKeyStore = new AdbKeyStore(); - } + initKeyStore(); switch (msg.what) { case MESSAGE_ADB_ENABLED: @@ -873,7 +905,7 @@ public class AdbDebuggingManager { if (!mConnectedKeys.containsKey(key)) { mConnectedKeys.put(key, 1); } - mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis()); + mAdbKeyStore.setLastConnectionTime(key, mTicker.currentTimeMillis()); sendPersistKeyStoreMessage(); scheduleJobToUpdateAdbKeyStore(); } @@ -920,9 +952,7 @@ public class AdbDebuggingManager { mConnectedKeys.clear(); // If the key store has not yet been instantiated then do so now; this avoids // the unnecessary creation of the key store when adb is not enabled. - if (mAdbKeyStore == null) { - mAdbKeyStore = new AdbKeyStore(); - } + initKeyStore(); mWifiConnectedKeys.clear(); mAdbKeyStore.deleteKeyStore(); cancelJobToUpdateAdbKeyStore(); @@ -937,7 +967,8 @@ public class AdbDebuggingManager { alwaysAllow = true; int refcount = mConnectedKeys.get(key) - 1; if (refcount == 0) { - mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis()); + mAdbKeyStore.setLastConnectionTime( + key, mTicker.currentTimeMillis()); sendPersistKeyStoreMessage(); scheduleJobToUpdateAdbKeyStore(); mConnectedKeys.remove(key); @@ -963,7 +994,7 @@ public class AdbDebuggingManager { if (!mConnectedKeys.isEmpty()) { for (Map.Entry<String, Integer> entry : mConnectedKeys.entrySet()) { mAdbKeyStore.setLastConnectionTime(entry.getKey(), - System.currentTimeMillis()); + mTicker.currentTimeMillis()); } sendPersistKeyStoreMessage(); scheduleJobToUpdateAdbKeyStore(); @@ -984,7 +1015,7 @@ public class AdbDebuggingManager { } else { mConnectedKeys.put(key, mConnectedKeys.get(key) + 1); } - mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis()); + mAdbKeyStore.setLastConnectionTime(key, mTicker.currentTimeMillis()); sendPersistKeyStoreMessage(); scheduleJobToUpdateAdbKeyStore(); logAdbConnectionChanged(key, AdbProtoEnums.AUTOMATICALLY_ALLOWED, true); @@ -1206,6 +1237,10 @@ public class AdbDebuggingManager { } break; } + case MESSAGE_KEY_FILES_UPDATED: { + mAdbKeyStore.reloadKeyMap(); + break; + } } } @@ -1377,8 +1412,7 @@ public class AdbDebuggingManager { AdbDebuggingManager.sendBroadcastWithDebugPermission(mContext, intent, UserHandle.ALL); // Add the key into the keystore - mAdbKeyStore.setLastConnectionTime(publicKey, - System.currentTimeMillis()); + mAdbKeyStore.setLastConnectionTime(publicKey, mTicker.currentTimeMillis()); sendPersistKeyStoreMessage(); scheduleJobToUpdateAdbKeyStore(); } @@ -1449,19 +1483,13 @@ public class AdbDebuggingManager { extras.add(new AbstractMap.SimpleEntry<String, String>("ssid", ssid)); extras.add(new AbstractMap.SimpleEntry<String, String>("bssid", bssid)); int currentUserId = ActivityManager.getCurrentUser(); - UserInfo userInfo = UserManager.get(mContext).getUserInfo(currentUserId); - String componentString; - if (userInfo.isAdmin()) { - componentString = Resources.getSystem().getString( - com.android.internal.R.string.config_customAdbWifiNetworkConfirmationComponent); - } else { - componentString = Resources.getSystem().getString( - com.android.internal.R.string.config_customAdbWifiNetworkConfirmationComponent); - } + String componentString = + Resources.getSystem().getString( + R.string.config_customAdbWifiNetworkConfirmationComponent); ComponentName componentName = ComponentName.unflattenFromString(componentString); + UserInfo userInfo = UserManager.get(mContext).getUserInfo(currentUserId); if (startConfirmationActivity(componentName, userInfo.getUserHandle(), extras) - || startConfirmationService(componentName, userInfo.getUserHandle(), - extras)) { + || startConfirmationService(componentName, userInfo.getUserHandle(), extras)) { return; } Slog.e(TAG, "Unable to start customAdbWifiNetworkConfirmation[SecondaryUser]Component " @@ -1543,7 +1571,7 @@ public class AdbDebuggingManager { /** * Returns a new File with the specified name in the adb directory. */ - private File getAdbFile(String fileName) { + private static File getAdbFile(String fileName) { File dataDir = Environment.getDataDirectory(); File adbDir = new File(dataDir, ADB_DIRECTORY); @@ -1556,66 +1584,38 @@ public class AdbDebuggingManager { } File getAdbTempKeysFile() { - return getAdbFile(ADB_TEMP_KEYS_FILE); + return mTempKeysFile; } File getUserKeyFile() { - return mTestUserKeyFile == null ? getAdbFile(ADB_KEYS_FILE) : mTestUserKeyFile; + return mUserKeyFile; } - private void writeKey(String key) { - try { - File keyFile = getUserKeyFile(); - - if (keyFile == null) { - return; - } - - FileOutputStream fo = new FileOutputStream(keyFile, true); - fo.write(key.getBytes()); - fo.write('\n'); - fo.close(); - - FileUtils.setPermissions(keyFile.toString(), - FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1); - } catch (IOException ex) { - Slog.e(TAG, "Error writing key:" + ex); + private void writeKeys(Iterable<String> keys) { + if (mUserKeyFile == null) { + return; } - } - private void writeKeys(Iterable<String> keys) { - AtomicFile atomicKeyFile = null; + AtomicFile atomicKeyFile = new AtomicFile(mUserKeyFile); + // Note: Do not use a try-with-resources with the FileOutputStream, because AtomicFile + // requires that it's cleaned up with AtomicFile.failWrite(); FileOutputStream fo = null; try { - File keyFile = getUserKeyFile(); - - if (keyFile == null) { - return; - } - - atomicKeyFile = new AtomicFile(keyFile); fo = atomicKeyFile.startWrite(); for (String key : keys) { fo.write(key.getBytes()); fo.write('\n'); } atomicKeyFile.finishWrite(fo); - - FileUtils.setPermissions(keyFile.toString(), - FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1); } catch (IOException ex) { Slog.e(TAG, "Error writing keys: " + ex); - if (atomicKeyFile != null) { - atomicKeyFile.failWrite(fo); - } + atomicKeyFile.failWrite(fo); + return; } - } - private void deleteKeyFile() { - File keyFile = getUserKeyFile(); - if (keyFile != null) { - keyFile.delete(); - } + FileUtils.setPermissions( + mUserKeyFile.toString(), + FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1); } /** @@ -1745,6 +1745,13 @@ public class AdbDebuggingManager { } /** + * Notify that they key files were updated so the AdbKeyManager reloads the keys. + */ + public void notifyKeyFilesUpdated() { + mHandler.sendEmptyMessage(AdbDebuggingHandler.MESSAGE_KEY_FILES_UPDATED); + } + + /** * Sends a message to the handler to persist the keystore. */ private void sendPersistKeyStoreMessage() { @@ -1778,7 +1785,7 @@ public class AdbDebuggingManager { try { dump.write("keystore", AdbDebuggingManagerProto.KEYSTORE, - FileUtils.readTextFile(getAdbTempKeysFile(), 0, null)); + FileUtils.readTextFile(mTempKeysFile, 0, null)); } catch (IOException e) { Slog.i(TAG, "Cannot read keystore: ", e); } @@ -1792,12 +1799,12 @@ public class AdbDebuggingManager { * ADB_ALLOWED_CONNECTION_TIME setting. */ class AdbKeyStore { - private Map<String, Long> mKeyMap; - private Set<String> mSystemKeys; - private File mKeyFile; private AtomicFile mAtomicKeyFile; - private List<String> mTrustedNetworks; + private final Set<String> mSystemKeys; + private final Map<String, Long> mKeyMap = new HashMap<>(); + private final List<String> mTrustedNetworks = new ArrayList<>(); + private static final int KEYSTORE_VERSION = 1; private static final int MAX_SUPPORTED_KEYSTORE_VERSION = 1; private static final String XML_KEYSTORE_START_TAG = "keyStore"; @@ -1819,26 +1826,22 @@ public class AdbDebuggingManager { public static final long NO_PREVIOUS_CONNECTION = 0; /** - * Constructor that uses the default location for the persistent adb keystore. + * Create an AdbKeyStore instance. + * + * <p>Upon creation, we parse {@link #mTempKeysFile} to determine authorized WiFi APs and + * retrieve the map of stored ADB keys and their last connected times. After that, we read + * the {@link #mUserKeyFile}, and any keys that exist in that file that do not exist in the + * map are added to the map (for backwards compatibility). */ AdbKeyStore() { - init(); - } - - /** - * Constructor that uses the specified file as the location for the persistent adb keystore. - */ - AdbKeyStore(File keyFile) { - mKeyFile = keyFile; - init(); - } - - private void init() { initKeyFile(); - mKeyMap = getKeyMap(); - mTrustedNetworks = getTrustedNetworks(); + readTempKeysFile(); mSystemKeys = getSystemKeysFromFile(SYSTEM_KEY_FILE); - addUserKeysToKeyStore(); + addExistingUserKeysToKeyStore(); + } + + public void reloadKeyMap() { + readTempKeysFile(); } public void addTrustedNetwork(String bssid) { @@ -1877,7 +1880,6 @@ public class AdbDebuggingManager { public void removeKey(String key) { if (mKeyMap.containsKey(key)) { mKeyMap.remove(key); - writeKeys(mKeyMap.keySet()); sendPersistKeyStoreMessage(); } } @@ -1886,12 +1888,9 @@ public class AdbDebuggingManager { * Initializes the key file that will be used to persist the adb grants. */ private void initKeyFile() { - if (mKeyFile == null) { - mKeyFile = getAdbTempKeysFile(); - } - // getAdbTempKeysFile can return null if the adb file cannot be obtained - if (mKeyFile != null) { - mAtomicKeyFile = new AtomicFile(mKeyFile); + // mTempKeysFile can be null if the adb file cannot be obtained + if (mTempKeysFile != null) { + mAtomicKeyFile = new AtomicFile(mTempKeysFile); } } @@ -1932,201 +1931,108 @@ public class AdbDebuggingManager { } /** - * Returns the key map with the keys and last connection times from the key file. + * Update the key map and the trusted networks list with values parsed from the temp keys + * file. */ - private Map<String, Long> getKeyMap() { - Map<String, Long> keyMap = new HashMap<String, Long>(); - // if the AtomicFile could not be instantiated before attempt again; if it still fails - // return an empty key map. + private void readTempKeysFile() { + mKeyMap.clear(); + mTrustedNetworks.clear(); if (mAtomicKeyFile == null) { initKeyFile(); if (mAtomicKeyFile == null) { - Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading"); - return keyMap; + Slog.e( + TAG, + "Unable to obtain the key file, " + mTempKeysFile + ", for reading"); + return; } } if (!mAtomicKeyFile.exists()) { - return keyMap; + return; } try (FileInputStream keyStream = mAtomicKeyFile.openRead()) { - TypedXmlPullParser parser = Xml.resolvePullParser(keyStream); - // Check for supported keystore version. - XmlUtils.beginDocument(parser, XML_KEYSTORE_START_TAG); - if (parser.next() != XmlPullParser.END_DOCUMENT) { - String tagName = parser.getName(); - if (tagName == null || !XML_KEYSTORE_START_TAG.equals(tagName)) { - Slog.e(TAG, "Expected " + XML_KEYSTORE_START_TAG + ", but got tag=" - + tagName); - return keyMap; - } + TypedXmlPullParser parser; + try { + parser = Xml.resolvePullParser(keyStream); + XmlUtils.beginDocument(parser, XML_KEYSTORE_START_TAG); + int keystoreVersion = parser.getAttributeInt(null, XML_ATTRIBUTE_VERSION); if (keystoreVersion > MAX_SUPPORTED_KEYSTORE_VERSION) { Slog.e(TAG, "Keystore version=" + keystoreVersion + " not supported (max_supported=" + MAX_SUPPORTED_KEYSTORE_VERSION + ")"); - return keyMap; - } - } - while (parser.next() != XmlPullParser.END_DOCUMENT) { - String tagName = parser.getName(); - if (tagName == null) { - break; - } else if (!tagName.equals(XML_TAG_ADB_KEY)) { - XmlUtils.skipCurrentTag(parser); - continue; - } - String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY); - long connectionTime; - try { - connectionTime = parser.getAttributeLong(null, - XML_ATTRIBUTE_LAST_CONNECTION); - } catch (XmlPullParserException e) { - Slog.e(TAG, - "Caught a NumberFormatException parsing the last connection time: " - + e); - XmlUtils.skipCurrentTag(parser); - continue; + return; } - keyMap.put(key, connectionTime); + } catch (XmlPullParserException e) { + // This could be because the XML document doesn't start with + // XML_KEYSTORE_START_TAG. Try again, instead just starting the document with + // the adbKey tag (the old format). + parser = Xml.resolvePullParser(keyStream); } + readKeyStoreContents(parser); } catch (IOException e) { Slog.e(TAG, "Caught an IOException parsing the XML key file: ", e); } catch (XmlPullParserException e) { - Slog.w(TAG, "Caught XmlPullParserException parsing the XML key file: ", e); - // The file could be written in a format prior to introducing keystore tag. - return getKeyMapBeforeKeystoreVersion(); + Slog.e(TAG, "Caught XmlPullParserException parsing the XML key file: ", e); + } + } + + private void readKeyStoreContents(TypedXmlPullParser parser) + throws XmlPullParserException, IOException { + // This parser is very forgiving. For backwards-compatibility, we simply iterate through + // all the tags in the file, skipping over anything that's not an <adbKey> tag or a + // <wifiAP> tag. Invalid tags (such as ones that don't have a valid "lastConnection" + // attribute) are simply ignored. + while ((parser.next()) != XmlPullParser.END_DOCUMENT) { + String tagName = parser.getName(); + if (XML_TAG_ADB_KEY.equals(tagName)) { + addAdbKeyToKeyMap(parser); + } else if (XML_TAG_WIFI_ACCESS_POINT.equals(tagName)) { + addTrustedNetworkToTrustedNetworks(parser); + } else { + Slog.w(TAG, "Ignoring tag '" + tagName + "'. Not recognized."); + } + XmlUtils.skipCurrentTag(parser); } - return keyMap; } - - /** - * Returns the key map with the keys and last connection times from the key file. - * This implementation was prior to adding the XML_KEYSTORE_START_TAG. - */ - private Map<String, Long> getKeyMapBeforeKeystoreVersion() { - Map<String, Long> keyMap = new HashMap<String, Long>(); - // if the AtomicFile could not be instantiated before attempt again; if it still fails - // return an empty key map. - if (mAtomicKeyFile == null) { - initKeyFile(); - if (mAtomicKeyFile == null) { - Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading"); - return keyMap; - } - } - if (!mAtomicKeyFile.exists()) { - return keyMap; - } - try (FileInputStream keyStream = mAtomicKeyFile.openRead()) { - TypedXmlPullParser parser = Xml.resolvePullParser(keyStream); - XmlUtils.beginDocument(parser, XML_TAG_ADB_KEY); - while (parser.next() != XmlPullParser.END_DOCUMENT) { - String tagName = parser.getName(); - if (tagName == null) { - break; - } else if (!tagName.equals(XML_TAG_ADB_KEY)) { - XmlUtils.skipCurrentTag(parser); - continue; - } - String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY); - long connectionTime; - try { - connectionTime = parser.getAttributeLong(null, - XML_ATTRIBUTE_LAST_CONNECTION); - } catch (XmlPullParserException e) { - Slog.e(TAG, - "Caught a NumberFormatException parsing the last connection time: " - + e); - XmlUtils.skipCurrentTag(parser); - continue; - } - keyMap.put(key, connectionTime); - } - } catch (IOException | XmlPullParserException e) { - Slog.e(TAG, "Caught an exception parsing the XML key file: ", e); + private void addAdbKeyToKeyMap(TypedXmlPullParser parser) { + String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY); + try { + long connectionTime = + parser.getAttributeLong(null, XML_ATTRIBUTE_LAST_CONNECTION); + mKeyMap.put(key, connectionTime); + } catch (XmlPullParserException e) { + Slog.e(TAG, "Error reading adbKey attributes", e); } - return keyMap; } - /** - * Returns the map of trusted networks from the keystore file. - * - * This was implemented in keystore version 1. - */ - private List<String> getTrustedNetworks() { - List<String> trustedNetworks = new ArrayList<String>(); - // if the AtomicFile could not be instantiated before attempt again; if it still fails - // return an empty key map. - if (mAtomicKeyFile == null) { - initKeyFile(); - if (mAtomicKeyFile == null) { - Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading"); - return trustedNetworks; - } - } - if (!mAtomicKeyFile.exists()) { - return trustedNetworks; - } - try (FileInputStream keyStream = mAtomicKeyFile.openRead()) { - TypedXmlPullParser parser = Xml.resolvePullParser(keyStream); - // Check for supported keystore version. - XmlUtils.beginDocument(parser, XML_KEYSTORE_START_TAG); - if (parser.next() != XmlPullParser.END_DOCUMENT) { - String tagName = parser.getName(); - if (tagName == null || !XML_KEYSTORE_START_TAG.equals(tagName)) { - Slog.e(TAG, "Expected " + XML_KEYSTORE_START_TAG + ", but got tag=" - + tagName); - return trustedNetworks; - } - int keystoreVersion = parser.getAttributeInt(null, XML_ATTRIBUTE_VERSION); - if (keystoreVersion > MAX_SUPPORTED_KEYSTORE_VERSION) { - Slog.e(TAG, "Keystore version=" + keystoreVersion - + " not supported (max_supported=" - + MAX_SUPPORTED_KEYSTORE_VERSION); - return trustedNetworks; - } - } - while (parser.next() != XmlPullParser.END_DOCUMENT) { - String tagName = parser.getName(); - if (tagName == null) { - break; - } else if (!tagName.equals(XML_TAG_WIFI_ACCESS_POINT)) { - XmlUtils.skipCurrentTag(parser); - continue; - } - String bssid = parser.getAttributeValue(null, XML_ATTRIBUTE_WIFI_BSSID); - trustedNetworks.add(bssid); - } - } catch (IOException | XmlPullParserException | NumberFormatException e) { - Slog.e(TAG, "Caught an exception parsing the XML key file: ", e); - } - return trustedNetworks; + private void addTrustedNetworkToTrustedNetworks(TypedXmlPullParser parser) { + String bssid = parser.getAttributeValue(null, XML_ATTRIBUTE_WIFI_BSSID); + mTrustedNetworks.add(bssid); } /** * Updates the keystore with keys that were previously set to be always allowed before the * connection time of keys was tracked. */ - private void addUserKeysToKeyStore() { - File userKeyFile = getUserKeyFile(); + private void addExistingUserKeysToKeyStore() { + if (mUserKeyFile == null || !mUserKeyFile.exists()) { + return; + } boolean mapUpdated = false; - if (userKeyFile != null && userKeyFile.exists()) { - try (BufferedReader in = new BufferedReader(new FileReader(userKeyFile))) { - long time = System.currentTimeMillis(); - String key; - while ((key = in.readLine()) != null) { - // if the keystore does not contain the key from the user key file then add - // it to the Map with the current system time to prevent it from expiring - // immediately if the user is actively using this key. - if (!mKeyMap.containsKey(key)) { - mKeyMap.put(key, time); - mapUpdated = true; - } + try (BufferedReader in = new BufferedReader(new FileReader(mUserKeyFile))) { + String key; + while ((key = in.readLine()) != null) { + // if the keystore does not contain the key from the user key file then add + // it to the Map with the current system time to prevent it from expiring + // immediately if the user is actively using this key. + if (!mKeyMap.containsKey(key)) { + mKeyMap.put(key, mTicker.currentTimeMillis()); + mapUpdated = true; } - } catch (IOException e) { - Slog.e(TAG, "Caught an exception reading " + userKeyFile + ": " + e); } + } catch (IOException e) { + Slog.e(TAG, "Caught an exception reading " + mUserKeyFile + ": " + e); } if (mapUpdated) { sendPersistKeyStoreMessage(); @@ -2147,7 +2053,9 @@ public class AdbDebuggingManager { if (mAtomicKeyFile == null) { initKeyFile(); if (mAtomicKeyFile == null) { - Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for writing"); + Slog.e( + TAG, + "Unable to obtain the key file, " + mTempKeysFile + ", for writing"); return; } } @@ -2178,17 +2086,21 @@ public class AdbDebuggingManager { Slog.e(TAG, "Caught an exception writing the key map: ", e); mAtomicKeyFile.failWrite(keyStream); } + writeKeys(mKeyMap.keySet()); } private boolean filterOutOldKeys() { - boolean keysDeleted = false; long allowedTime = getAllowedConnectionTime(); - long systemTime = System.currentTimeMillis(); + if (allowedTime == 0) { + return false; + } + boolean keysDeleted = false; + long systemTime = mTicker.currentTimeMillis(); Iterator<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator(); while (keyMapIterator.hasNext()) { Map.Entry<String, Long> keyEntry = keyMapIterator.next(); long connectionTime = keyEntry.getValue(); - if (allowedTime != 0 && systemTime > (connectionTime + allowedTime)) { + if (systemTime > (connectionTime + allowedTime)) { keyMapIterator.remove(); keysDeleted = true; } @@ -2212,7 +2124,7 @@ public class AdbDebuggingManager { if (allowedTime == 0) { return minExpiration; } - long systemTime = System.currentTimeMillis(); + long systemTime = mTicker.currentTimeMillis(); Iterator<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator(); while (keyMapIterator.hasNext()) { Map.Entry<String, Long> keyEntry = keyMapIterator.next(); @@ -2233,7 +2145,9 @@ public class AdbDebuggingManager { public void deleteKeyStore() { mKeyMap.clear(); mTrustedNetworks.clear(); - deleteKeyFile(); + if (mUserKeyFile != null) { + mUserKeyFile.delete(); + } if (mAtomicKeyFile == null) { return; } @@ -2260,7 +2174,8 @@ public class AdbDebuggingManager { * is set to true the time will be set even if it is older than the previously written * connection time. */ - public void setLastConnectionTime(String key, long connectionTime, boolean force) { + @VisibleForTesting + void setLastConnectionTime(String key, long connectionTime, boolean force) { // Do not set the connection time to a value that is earlier than what was previously // stored as the last connection time unless force is set. if (mKeyMap.containsKey(key) && mKeyMap.get(key) >= connectionTime && !force) { @@ -2271,11 +2186,6 @@ public class AdbDebuggingManager { if (mSystemKeys.contains(key)) { return; } - // if this is the first time the key is being added then write it to the key file as - // well. - if (!mKeyMap.containsKey(key)) { - writeKey(key); - } mKeyMap.put(key, connectionTime); } @@ -2307,12 +2217,8 @@ public class AdbDebuggingManager { long allowedConnectionTime = getAllowedConnectionTime(); // if the allowed connection time is 0 then revert to the previous behavior of always // allowing previously granted adb grants. - if (allowedConnectionTime == 0 || (System.currentTimeMillis() < (lastConnectionTime - + allowedConnectionTime))) { - return true; - } else { - return false; - } + return allowedConnectionTime == 0 + || (mTicker.currentTimeMillis() < (lastConnectionTime + allowedConnectionTime)); } /** @@ -2324,4 +2230,15 @@ public class AdbDebuggingManager { return mTrustedNetworks.contains(bssid); } } + + /** + * A Guava-like interface for getting the current system time. + * + * This allows us to swap a fake ticker in for testing to reduce "Thread.sleep()" calls and test + * for exact expected times instead of random ones. + */ + @VisibleForTesting + interface Ticker { + long currentTimeMillis(); + } } diff --git a/services/core/java/com/android/server/adb/AdbService.java b/services/core/java/com/android/server/adb/AdbService.java index 5d0c732d5f48..55d8dba69626 100644 --- a/services/core/java/com/android/server/adb/AdbService.java +++ b/services/core/java/com/android/server/adb/AdbService.java @@ -152,6 +152,14 @@ public class AdbService extends IAdbManager.Stub { } @Override + public void notifyKeyFilesUpdated() { + if (mDebuggingManager == null) { + return; + } + mDebuggingManager.notifyKeyFilesUpdated(); + } + + @Override public void startAdbdForTransport(byte transportType) { FgThread.getHandler().sendMessage(obtainMessage( AdbService::setAdbdEnabledForTransport, AdbService.this, true, transportType)); diff --git a/services/core/java/com/android/server/am/TraceErrorLogger.java b/services/core/java/com/android/server/am/TraceErrorLogger.java index 29a9b5c501c4..ec0587f721d4 100644 --- a/services/core/java/com/android/server/am/TraceErrorLogger.java +++ b/services/core/java/com/android/server/am/TraceErrorLogger.java @@ -16,7 +16,6 @@ package com.android.server.am; -import android.os.Build; import android.os.Trace; import java.util.UUID; @@ -31,7 +30,7 @@ public class TraceErrorLogger { private static final int PLACEHOLDER_VALUE = 1; public boolean isAddErrorIdEnabled() { - return Build.IS_DEBUGGABLE; + return true; } /** diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 43d77ab32371..0040ea9215b3 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -365,6 +365,8 @@ public class AudioService extends IAudioService.Stub private static final int MSG_REMOVE_ASSISTANT_SERVICE_UID = 45; private static final int MSG_UPDATE_ACTIVE_ASSISTANT_SERVICE_UID = 46; private static final int MSG_DISPATCH_DEVICE_VOLUME_BEHAVIOR = 47; + private static final int MSG_ROTATION_UPDATE = 48; + private static final int MSG_FOLD_UPDATE = 49; // start of messages handled under wakelock // these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(), @@ -1251,7 +1253,9 @@ public class AudioService extends IAudioService.Stub intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); if (mMonitorRotation) { - RotationHelper.init(mContext, mAudioHandler); + RotationHelper.init(mContext, mAudioHandler, + rotationParam -> onRotationUpdate(rotationParam), + foldParam -> onFoldUpdate(foldParam)); } intentFilter.addAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); @@ -1398,6 +1402,20 @@ public class AudioService extends IAudioService.Stub } //----------------------------------------------------------------- + // rotation/fold updates coming from RotationHelper + void onRotationUpdate(String rotationParameter) { + // use REPLACE as only the last rotation matters + sendMsg(mAudioHandler, MSG_ROTATION_UPDATE, SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0, + /*obj*/ rotationParameter, /*delay*/ 0); + } + + void onFoldUpdate(String foldParameter) { + // use REPLACE as only the last fold state matters + sendMsg(mAudioHandler, MSG_FOLD_UPDATE, SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0, + /*obj*/ foldParameter, /*delay*/ 0); + } + + //----------------------------------------------------------------- // monitoring requests for volume range initialization @Override // AudioSystemAdapter.OnVolRangeInitRequestListener public void onVolumeRangeInitRequestFromNative() { @@ -8327,6 +8345,16 @@ public class AudioService extends IAudioService.Stub case MSG_DISPATCH_DEVICE_VOLUME_BEHAVIOR: dispatchDeviceVolumeBehavior((AudioDeviceAttributes) msg.obj, msg.arg1); break; + + case MSG_ROTATION_UPDATE: + // rotation parameter format: "rotation=x" where x is one of 0, 90, 180, 270 + mAudioSystem.setParameters((String) msg.obj); + break; + + case MSG_FOLD_UPDATE: + // fold parameter format: "device_folded=x" where x is one of on, off + mAudioSystem.setParameters((String) msg.obj); + break; } } } diff --git a/services/core/java/com/android/server/audio/RotationHelper.java b/services/core/java/com/android/server/audio/RotationHelper.java index eb8387fe85e5..5cdf58bdd62f 100644 --- a/services/core/java/com/android/server/audio/RotationHelper.java +++ b/services/core/java/com/android/server/audio/RotationHelper.java @@ -21,13 +21,14 @@ import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerGlobal; -import android.media.AudioSystem; import android.os.Handler; import android.os.HandlerExecutor; import android.util.Log; import android.view.Display; import android.view.Surface; +import java.util.function.Consumer; + /** * Class to handle device rotation events for AudioService, and forward device rotation * and folded state to the audio HALs through AudioSystem. @@ -53,6 +54,10 @@ class RotationHelper { private static AudioDisplayListener sDisplayListener; private static FoldStateListener sFoldStateListener; + /** callback to send rotation updates to AudioSystem */ + private static Consumer<String> sRotationUpdateCb; + /** callback to send folded state updates to AudioSystem */ + private static Consumer<String> sFoldUpdateCb; private static final Object sRotationLock = new Object(); private static final Object sFoldStateLock = new Object(); @@ -67,13 +72,16 @@ class RotationHelper { * - sDisplayListener != null * - sContext != null */ - static void init(Context context, Handler handler) { + static void init(Context context, Handler handler, + Consumer<String> rotationUpdateCb, Consumer<String> foldUpdateCb) { if (context == null) { throw new IllegalArgumentException("Invalid null context"); } sContext = context; sHandler = handler; sDisplayListener = new AudioDisplayListener(); + sRotationUpdateCb = rotationUpdateCb; + sFoldUpdateCb = foldUpdateCb; enable(); } @@ -115,21 +123,26 @@ class RotationHelper { if (DEBUG_ROTATION) { Log.i(TAG, "publishing device rotation =" + rotation + " (x90deg)"); } + String rotationParam; switch (rotation) { case Surface.ROTATION_0: - AudioSystem.setParameters("rotation=0"); + rotationParam = "rotation=0"; break; case Surface.ROTATION_90: - AudioSystem.setParameters("rotation=90"); + rotationParam = "rotation=90"; break; case Surface.ROTATION_180: - AudioSystem.setParameters("rotation=180"); + rotationParam = "rotation=180"; break; case Surface.ROTATION_270: - AudioSystem.setParameters("rotation=270"); + rotationParam = "rotation=270"; break; default: Log.e(TAG, "Unknown device rotation"); + rotationParam = null; + } + if (rotationParam != null) { + sRotationUpdateCb.accept(rotationParam); } } @@ -140,11 +153,13 @@ class RotationHelper { synchronized (sFoldStateLock) { if (sDeviceFold != newFolded) { sDeviceFold = newFolded; + String foldParam; if (newFolded) { - AudioSystem.setParameters("device_folded=on"); + foldParam = "device_folded=on"; } else { - AudioSystem.setParameters("device_folded=off"); + foldParam = "device_folded=off"; } + sFoldUpdateCb.accept(foldParam); } } } diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 5b26672c7de2..dd44af1b68ee 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -280,18 +280,13 @@ public class SpatializerHelper { } // for both transaural / binaural, we are not forcing enablement as the init() method // could have been called another time after boot in case of audioserver restart - if (mTransauralSupported) { - // not force-enabling as this device might already be in the device list - addCompatibleAudioDevice( - new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""), - false /*forceEnable*/); - } - if (mBinauralSupported) { - // not force-enabling as this device might already be in the device list - addCompatibleAudioDevice( - new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""), - false /*forceEnable*/); - } + addCompatibleAudioDevice( + new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""), + false /*forceEnable*/); + // not force-enabling as this device might already be in the device list + addCompatibleAudioDevice( + new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""), + false /*forceEnable*/); } catch (RemoteException e) { resetCapabilities(); } finally { @@ -497,10 +492,9 @@ public class SpatializerHelper { synchronized @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() { // build unionOf(mCompatibleAudioDevices, mEnabledDevice) - mDisabledAudioDevices ArrayList<AudioDeviceAttributes> compatList = new ArrayList<>(); - for (SADeviceState dev : mSADevices) { - if (dev.mEnabled) { - compatList.add(new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, - dev.mDeviceType, dev.mDeviceAddress == null ? "" : dev.mDeviceAddress)); + for (SADeviceState deviceState : mSADevices) { + if (deviceState.mEnabled) { + compatList.add(deviceState.getAudioDeviceAttributes()); } } return compatList; @@ -521,15 +515,15 @@ public class SpatializerHelper { */ private void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada, boolean forceEnable) { + if (!isDeviceCompatibleWithSpatializationModes(ada)) { + return; + } loglogi("addCompatibleAudioDevice: dev=" + ada); - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); boolean isInList = false; SADeviceState deviceUpdated = null; // non-null on update. for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (!wireless || ada.getAddress().equals(deviceState.mDeviceAddress))) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { isInList = true; if (forceEnable) { deviceState.mEnabled = true; @@ -539,11 +533,10 @@ public class SpatializerHelper { } } if (!isInList) { - final SADeviceState dev = new SADeviceState(deviceType, - wireless ? ada.getAddress() : ""); - dev.mEnabled = true; - mSADevices.add(dev); - deviceUpdated = dev; + final SADeviceState deviceState = new SADeviceState(ada.getType(), ada.getAddress()); + deviceState.mEnabled = true; + mSADevices.add(deviceState); + deviceUpdated = deviceState; } if (deviceUpdated != null) { onRoutingUpdated(); @@ -574,13 +567,10 @@ public class SpatializerHelper { synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { loglogi("removeCompatibleAudioDevice: dev=" + ada); - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); SADeviceState deviceUpdated = null; // non-null on update. for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (!wireless || ada.getAddress().equals(deviceState.mDeviceAddress))) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { deviceState.mEnabled = false; deviceUpdated = deviceState; break; @@ -602,10 +592,9 @@ public class SpatializerHelper { // if not a wireless device, this value will be overwritten to map the type // to TYPE_BUILTIN_SPEAKER or TYPE_WIRED_HEADPHONES @AudioDeviceInfo.AudioDeviceType int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); // if not a wireless device: find if media device is in the speaker, wired headphones - if (!wireless) { + if (!isWireless(deviceType)) { // is the device type capable of doing SA? if (!mSACapableDeviceTypes.contains(deviceType)) { Log.i(TAG, "Device incompatible with Spatial Audio dev:" + ada); @@ -640,9 +629,7 @@ public class SpatializerHelper { boolean enabled = false; boolean available = false; for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { available = true; enabled = deviceState.mEnabled; break; @@ -652,11 +639,12 @@ public class SpatializerHelper { } private synchronized void addWirelessDeviceIfNew(@NonNull AudioDeviceAttributes ada) { + if (!isDeviceCompatibleWithSpatializationModes(ada)) { + return; + } boolean knownDevice = false; for (SADeviceState deviceState : mSADevices) { - // wireless device so always check address - if (ada.getType() == deviceState.mDeviceType - && ada.getAddress().equals(deviceState.mDeviceAddress)) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { knownDevice = true; break; } @@ -704,13 +692,8 @@ public class SpatializerHelper { if (ada.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) { return false; } - - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { return true; } } @@ -719,12 +702,19 @@ public class SpatializerHelper { private synchronized boolean canBeSpatializedOnDevice(@NonNull AudioAttributes attributes, @NonNull AudioFormat format, @NonNull AudioDeviceAttributes[] devices) { - final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(devices[0].getType(), + if (isDeviceCompatibleWithSpatializationModes(devices[0])) { + return AudioSystem.canBeSpatialized(attributes, format, devices); + } + return false; + } + + private boolean isDeviceCompatibleWithSpatializationModes(@NonNull AudioDeviceAttributes ada) { + final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(ada.getType(), /*default when type not found*/ SpatializationMode.SPATIALIZER_BINAURAL); if ((modeForDevice == SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported) || (modeForDevice == SpatializationMode.SPATIALIZER_TRANSAURAL && mTransauralSupported)) { - return AudioSystem.canBeSpatialized(attributes, format, devices); + return true; } return false; } @@ -1089,13 +1079,8 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, ignoring setHeadTrackerEnabled to " + enabled + " for " + ada); } - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); - for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { if (!deviceState.mHasHeadTracker) { Log.e(TAG, "Called setHeadTrackerEnabled enabled:" + enabled + " device:" + ada + " on a device without headtracker"); @@ -1109,7 +1094,7 @@ public class SpatializerHelper { } } // check current routing to see if it affects the headtracking mode - if (ROUTING_DEVICES[0].getType() == deviceType + if (ROUTING_DEVICES[0].getType() == ada.getType() && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) { setDesiredHeadTrackingMode(enabled ? mDesiredHeadTrackingModeWhenEnabled : Spatializer.HEAD_TRACKING_MODE_DISABLED); @@ -1121,13 +1106,8 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, hasHeadTracker always false for " + ada); return false; } - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); - for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { return deviceState.mHasHeadTracker; } } @@ -1144,13 +1124,8 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, setHasHeadTracker always false for " + ada); return false; } - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); - for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { if (!deviceState.mHasHeadTracker) { deviceState.mHasHeadTracker = true; mAudioService.persistSpatialAudioDeviceSettings(); @@ -1168,13 +1143,8 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, isHeadTrackerEnabled always false for " + ada); return false; } - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); - for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { if (!deviceState.mHasHeadTracker) { return false; } @@ -1531,7 +1501,7 @@ public class SpatializerHelper { SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @NonNull String address) { mDeviceType = deviceType; - mDeviceAddress = Objects.requireNonNull(address); + mDeviceAddress = isWireless(deviceType) ? Objects.requireNonNull(address) : ""; } @Override @@ -1599,6 +1569,18 @@ public class SpatializerHelper { return null; } } + + public AudioDeviceAttributes getAudioDeviceAttributes() { + return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, + mDeviceType, mDeviceAddress == null ? "" : mDeviceAddress); + } + + public boolean matchesAudioDeviceAttributes(AudioDeviceAttributes ada) { + final int deviceType = ada.getType(); + final boolean wireless = isWireless(deviceType); + return (deviceType == mDeviceType) + && (!wireless || ada.getAddress().equals(mDeviceAddress)); + } } /*package*/ synchronized String getSADeviceSettings() { @@ -1619,7 +1601,9 @@ public class SpatializerHelper { // small list, not worth overhead of Arrays.stream(devSettings) for (String setting : devSettings) { SADeviceState devState = SADeviceState.fromPersistedString(setting); - if (devState != null) { + if (devState != null + && isDeviceCompatibleWithSpatializationModes( + devState.getAudioDeviceAttributes())) { mSADevices.add(devState); logDeviceState(devState, "setSADeviceSettings"); } diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index cc49f07dd0e5..41ca13f5d5f5 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -538,13 +538,12 @@ public final class AuthSession implements IBinder.DeathRecipient { void onDialogAnimatedIn() { if (mState != STATE_AUTH_STARTED) { - Slog.w(TAG, "onDialogAnimatedIn, unexpected state: " + mState); + Slog.e(TAG, "onDialogAnimatedIn, unexpected state: " + mState); + return; } mState = STATE_AUTH_STARTED_UI_SHOWING; - startAllPreparedFingerprintSensors(); - mState = STATE_AUTH_STARTED_UI_SHOWING; } void onTryAgainPressed() { diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java index 968146a166ed..ef2931ff5850 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java @@ -20,14 +20,18 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.biometrics.BiometricConstants; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.function.BooleanSupplier; /** * Contains all the necessary information for a HAL operation. @@ -84,6 +88,8 @@ public class BiometricSchedulerOperation { private final BaseClientMonitor mClientMonitor; @Nullable private final ClientMonitorCallback mClientCallback; + @NonNull + private final BooleanSupplier mIsDebuggable; @Nullable private ClientMonitorCallback mOnStartCallback; @OperationState @@ -99,14 +105,33 @@ public class BiometricSchedulerOperation { this(clientMonitor, callback, STATE_WAITING_IN_QUEUE); } + @VisibleForTesting + BiometricSchedulerOperation( + @NonNull BaseClientMonitor clientMonitor, + @Nullable ClientMonitorCallback callback, + @NonNull BooleanSupplier isDebuggable + ) { + this(clientMonitor, callback, STATE_WAITING_IN_QUEUE, isDebuggable); + } + protected BiometricSchedulerOperation( @NonNull BaseClientMonitor clientMonitor, @Nullable ClientMonitorCallback callback, @OperationState int state ) { + this(clientMonitor, callback, state, Build::isDebuggable); + } + + private BiometricSchedulerOperation( + @NonNull BaseClientMonitor clientMonitor, + @Nullable ClientMonitorCallback callback, + @OperationState int state, + @NonNull BooleanSupplier isDebuggable + ) { mClientMonitor = clientMonitor; mClientCallback = callback; mState = state; + mIsDebuggable = isDebuggable; mCancelWatchdog = () -> { if (!isFinished()) { Slog.e(TAG, "[Watchdog Triggered]: " + this); @@ -144,13 +169,19 @@ public class BiometricSchedulerOperation { * @return if this operation started */ public boolean start(@NonNull ClientMonitorCallback callback) { - checkInState("start", + if (errorWhenNoneOf("start", STATE_WAITING_IN_QUEUE, STATE_WAITING_FOR_COOKIE, - STATE_WAITING_IN_QUEUE_CANCELING); + STATE_WAITING_IN_QUEUE_CANCELING)) { + return false; + } if (mClientMonitor.getCookie() != 0) { - throw new IllegalStateException("operation requires cookie"); + String err = "operation requires cookie"; + if (mIsDebuggable.getAsBoolean()) { + throw new IllegalStateException(err); + } + Slog.e(TAG, err); } return doStart(callback); @@ -164,16 +195,18 @@ public class BiometricSchedulerOperation { * @return if this operation started */ public boolean startWithCookie(@NonNull ClientMonitorCallback callback, int cookie) { - checkInState("start", - STATE_WAITING_IN_QUEUE, - STATE_WAITING_FOR_COOKIE, - STATE_WAITING_IN_QUEUE_CANCELING); - if (mClientMonitor.getCookie() != cookie) { Slog.e(TAG, "Mismatched cookie for operation: " + this + ", received: " + cookie); return false; } + if (errorWhenNoneOf("start", + STATE_WAITING_IN_QUEUE, + STATE_WAITING_FOR_COOKIE, + STATE_WAITING_IN_QUEUE_CANCELING)) { + return false; + } + return doStart(callback); } @@ -217,10 +250,12 @@ public class BiometricSchedulerOperation { * immediately abort the operation and notify the client that it has finished unsuccessfully. */ public void abort() { - checkInState("cannot abort a non-pending operation", + if (errorWhenNoneOf("abort", STATE_WAITING_IN_QUEUE, STATE_WAITING_FOR_COOKIE, - STATE_WAITING_IN_QUEUE_CANCELING); + STATE_WAITING_IN_QUEUE_CANCELING)) { + return; + } if (isHalOperation()) { ((HalClientMonitor<?>) mClientMonitor).unableToStart(); @@ -247,7 +282,9 @@ public class BiometricSchedulerOperation { * the callback used from {@link #start(ClientMonitorCallback)} is used) */ public void cancel(@NonNull Handler handler, @NonNull ClientMonitorCallback callback) { - checkNotInState("cancel", STATE_FINISHED); + if (errorWhenOneOf("cancel", STATE_FINISHED)) { + return; + } final int currentState = mState; if (!isInterruptable()) { @@ -402,21 +439,28 @@ public class BiometricSchedulerOperation { return mClientMonitor; } - private void checkNotInState(String message, @OperationState int... states) { - for (int state : states) { - if (mState == state) { - throw new IllegalStateException(message + ": illegal state= " + state); + private boolean errorWhenOneOf(String op, @OperationState int... states) { + final boolean isError = ArrayUtils.contains(states, mState); + if (isError) { + String err = op + ": mState must not be " + mState; + if (mIsDebuggable.getAsBoolean()) { + throw new IllegalStateException(err); } + Slog.e(TAG, err); } + return isError; } - private void checkInState(String message, @OperationState int... states) { - for (int state : states) { - if (mState == state) { - return; + private boolean errorWhenNoneOf(String op, @OperationState int... states) { + final boolean isError = !ArrayUtils.contains(states, mState); + if (isError) { + String err = op + ": mState=" + mState + " must be one of " + Arrays.toString(states); + if (mIsDebuggable.getAsBoolean()) { + throw new IllegalStateException(err); } + Slog.e(TAG, err); } - throw new IllegalStateException(message + ": illegal state= " + mState); + return isError; } @Override diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 3551ff91f542..f526960d0ef3 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -86,6 +86,8 @@ import android.net.ipsec.ike.ChildSessionConfiguration; import android.net.ipsec.ike.ChildSessionParams; import android.net.ipsec.ike.IkeSession; import android.net.ipsec.ike.IkeSessionCallback; +import android.net.ipsec.ike.IkeSessionConfiguration; +import android.net.ipsec.ike.IkeSessionConnectionInfo; import android.net.ipsec.ike.IkeSessionParams; import android.net.ipsec.ike.IkeTunnelConnectionParams; import android.net.ipsec.ike.exceptions.IkeNetworkLostException; @@ -168,9 +170,10 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** @@ -190,11 +193,19 @@ public class Vpn { private static final long VPN_LAUNCH_IDLE_ALLOWLIST_DURATION_MS = 60 * 1000; // Length of time (in milliseconds) that an app registered for VpnManager events is placed on - // the device idle allowlist each time the a VpnManager event is fired. + // the device idle allowlist each time the VpnManager event is fired. private static final long VPN_MANAGER_EVENT_ALLOWLIST_DURATION_MS = 30 * 1000; private static final String LOCKDOWN_ALLOWLIST_SETTING_NAME = Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST; + + /** + * The retries for consecutive failures. + * + * <p>If retries have exceeded the length of this array, the last entry in the array will be + * used as a repeating interval. + */ + private static final long[] IKEV2_VPN_RETRY_DELAYS_SEC = {1L, 2L, 5L, 30L, 60L, 300L, 900L}; /** * Largest profile size allowable for Platform VPNs. * @@ -473,6 +484,20 @@ public class Vpn { "Cannot set tunnel's fd as blocking=" + blocking, e); } } + + /** + * Retrieves the next retry delay + * + * <p>If retries have exceeded the IKEV2_VPN_RETRY_DELAYS_SEC, the last entry in + * the array will be used as a repeating interval. + */ + public long getNextRetryDelaySeconds(int retryCount) { + if (retryCount >= IKEV2_VPN_RETRY_DELAYS_SEC.length) { + return IKEV2_VPN_RETRY_DELAYS_SEC[IKEV2_VPN_RETRY_DELAYS_SEC.length - 1]; + } else { + return IKEV2_VPN_RETRY_DELAYS_SEC[retryCount]; + } + } } public Vpn(Looper looper, Context context, INetworkManagementService netService, INetd netd, @@ -1646,7 +1671,9 @@ public class Vpn { for (String app : packageNames) { int uid = getAppUid(app, userId); if (uid != -1) uids.add(uid); - if (Process.isApplicationUid(uid)) { + // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from + // ConnectivityServiceTest. + if (Process.isApplicationUid(uid) && SdkLevel.isAtLeastT()) { uids.add(Process.toSdkSandboxUid(uid)); } } @@ -2603,13 +2630,23 @@ public class Vpn { void onDefaultNetworkLinkPropertiesChanged(@NonNull LinkProperties lp); - void onChildOpened( - @NonNull Network network, @NonNull ChildSessionConfiguration childConfig); + void onDefaultNetworkLost(@NonNull Network network); + + void onIkeOpened(int token, @NonNull IkeSessionConfiguration ikeConfiguration); + + void onIkeConnectionInfoChanged( + int token, @NonNull IkeSessionConnectionInfo ikeConnectionInfo); - void onChildTransformCreated( - @NonNull Network network, @NonNull IpSecTransform transform, int direction); + void onChildOpened(int token, @NonNull ChildSessionConfiguration childConfig); - void onSessionLost(@NonNull Network network, @Nullable Exception exception); + void onChildTransformCreated(int token, @NonNull IpSecTransform transform, int direction); + + void onChildMigrated( + int token, + @NonNull IpSecTransform inTransform, + @NonNull IpSecTransform outTransform); + + void onSessionLost(int token, @Nullable Exception exception); } /** @@ -2640,6 +2677,10 @@ public class Vpn { class IkeV2VpnRunner extends VpnRunner implements IkeV2VpnRunnerCallback { @NonNull private static final String TAG = "IkeV2VpnRunner"; + // 5 seconds grace period before tearing down the IKE Session in case new default network + // will come up + private static final long NETWORK_LOST_TIMEOUT_MS = 5000L; + @NonNull private final IpSecManager mIpSecManager; @NonNull private final Ikev2VpnProfile mProfile; @NonNull private final ConnectivityManager.NetworkCallback mNetworkCallback; @@ -2651,24 +2692,60 @@ public class Vpn { * of the mutable Ikev2VpnRunner fields. The Ikev2VpnRunner is built mostly lock-free by * virtue of everything being serialized on this executor. */ - @NonNull private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + @NonNull + private final ScheduledThreadPoolExecutor mExecutor = new ScheduledThreadPoolExecutor(1); + + @Nullable private ScheduledFuture<?> mScheduledHandleNetworkLostTimeout; + @Nullable private ScheduledFuture<?> mScheduledHandleRetryIkeSessionTimeout; /** Signal to ensure shutdown is honored even if a new Network is connected. */ private boolean mIsRunning = true; + /** + * The token used by the primary/current/active IKE session. + * + * <p>This token MUST be updated when the VPN switches to use a new IKE session. + */ + private int mCurrentToken = -1; + @Nullable private IpSecTunnelInterface mTunnelIface; - @Nullable private IkeSession mSession; @Nullable private Network mActiveNetwork; @Nullable private NetworkCapabilities mUnderlyingNetworkCapabilities; @Nullable private LinkProperties mUnderlyingLinkProperties; private final String mSessionKey; + @Nullable private IkeSession mSession; + @Nullable private IkeSessionConnectionInfo mIkeConnectionInfo; + + // mMobikeEnabled can only be updated after IKE AUTH is finished. + private boolean mMobikeEnabled = false; + + /** + * The number of attempts since the last successful connection. + * + * <p>This variable controls the retry delay, and is reset when a new IKE session is + * opened or when there is a new default network. + */ + private int mRetryCount = 0; + IkeV2VpnRunner(@NonNull Ikev2VpnProfile profile) { super(TAG); mProfile = profile; mIpSecManager = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); mNetworkCallback = new VpnIkev2Utils.Ikev2VpnNetworkCallback(TAG, this, mExecutor); mSessionKey = UUID.randomUUID().toString(); + + // Set the policy so that cancelled tasks will be removed from the work queue + mExecutor.setRemoveOnCancelPolicy(true); + + // Set the policy so that all delayed tasks will not be executed + mExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + + // To avoid hitting RejectedExecutionException upon shutdown of the mExecutor */ + mExecutor.setRejectedExecutionHandler( + (r, executor) -> { + Log.d(TAG, "Runnable " + r + " rejected by the mExecutor"); + }); } @Override @@ -2707,22 +2784,64 @@ public class Vpn { return Objects.equals(mActiveNetwork, network) && mIsRunning; } + private boolean isActiveToken(int token) { + return (mCurrentToken == token) && mIsRunning; + } + + /** + * Called when an IKE session has been opened + * + * <p>This method is only ever called once per IkeSession, and MUST run on the mExecutor + * thread in order to ensure consistency of the Ikev2VpnRunner fields. + */ + public void onIkeOpened(int token, @NonNull IkeSessionConfiguration ikeConfiguration) { + if (!isActiveToken(token)) { + Log.d(TAG, "onIkeOpened called for obsolete token " + token); + return; + } + + mMobikeEnabled = + ikeConfiguration.isIkeExtensionEnabled( + IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE); + onIkeConnectionInfoChanged(token, ikeConfiguration.getIkeSessionConnectionInfo()); + mRetryCount = 0; + } + + /** + * Called when an IKE session's {@link IkeSessionConnectionInfo} is available or updated + * + * <p>This callback is usually fired when an IKE session has been opened or migrated. + * + * <p>This method is called multiple times over the lifetime of an IkeSession, and MUST run + * on the mExecutor thread in order to ensure consistency of the Ikev2VpnRunner fields. + */ + public void onIkeConnectionInfoChanged( + int token, @NonNull IkeSessionConnectionInfo ikeConnectionInfo) { + if (!isActiveToken(token)) { + Log.d(TAG, "onIkeConnectionInfoChanged called for obsolete token " + token); + return; + } + + // The update on VPN and the IPsec tunnel will be done when migration is fully complete + // in onChildMigrated + mIkeConnectionInfo = ikeConnectionInfo; + } + /** * Called when an IKE Child session has been opened, signalling completion of the startup. * * <p>This method is only ever called once per IkeSession, and MUST run on the mExecutor * thread in order to ensure consistency of the Ikev2VpnRunner fields. */ - public void onChildOpened( - @NonNull Network network, @NonNull ChildSessionConfiguration childConfig) { - if (!isActiveNetwork(network)) { - Log.d(TAG, "onOpened called for obsolete network " + network); + public void onChildOpened(int token, @NonNull ChildSessionConfiguration childConfig) { + if (!isActiveToken(token)) { + Log.d(TAG, "onChildOpened called for obsolete token " + token); // Do nothing; this signals that either: (1) a new/better Network was found, - // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this - // IKE session was already shut down (exited, or an error was encountered somewhere - // else). In both cases, all resources and sessions are torn down via - // resetIkeState(). + // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in + // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited, + // or an error was encountered somewhere else). In both cases, all resources and + // sessions are torn down via resetIkeState(). return; } @@ -2741,6 +2860,11 @@ public class Vpn { dnsAddrStrings.add(addr.getHostAddress()); } + // The actual network of this IKE session has been set up with is + // mIkeConnectionInfo.getNetwork() instead of mActiveNetwork because + // mActiveNetwork might have been updated after the setup was triggered. + final Network network = mIkeConnectionInfo.getNetwork(); + final NetworkAgent networkAgent; final LinkProperties lp; @@ -2783,8 +2907,8 @@ public class Vpn { networkAgent.sendLinkProperties(lp); } catch (Exception e) { - Log.d(TAG, "Error in ChildOpened for network " + network, e); - onSessionLost(network, e); + Log.d(TAG, "Error in ChildOpened for token " + token, e); + onSessionLost(token, e); } } @@ -2792,19 +2916,19 @@ public class Vpn { * Called when an IPsec transform has been created, and should be applied. * * <p>This method is called multiple times over the lifetime of an IkeSession (or default - * network), and is MUST always be called on the mExecutor thread in order to ensure + * network), and MUST always be called on the mExecutor thread in order to ensure * consistency of the Ikev2VpnRunner fields. */ public void onChildTransformCreated( - @NonNull Network network, @NonNull IpSecTransform transform, int direction) { - if (!isActiveNetwork(network)) { - Log.d(TAG, "ChildTransformCreated for obsolete network " + network); + int token, @NonNull IpSecTransform transform, int direction) { + if (!isActiveToken(token)) { + Log.d(TAG, "ChildTransformCreated for obsolete token " + token); // Do nothing; this signals that either: (1) a new/better Network was found, - // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this - // IKE session was already shut down (exited, or an error was encountered somewhere - // else). In both cases, all resources and sessions are torn down via - // resetIkeState(). + // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in + // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited, + // or an error was encountered somewhere else). In both cases, all resources and + // sessions are torn down via resetIkeState(). return; } @@ -2813,36 +2937,127 @@ public class Vpn { // them alive for us mIpSecManager.applyTunnelModeTransform(mTunnelIface, direction, transform); } catch (IOException e) { - Log.d(TAG, "Transform application failed for network " + network, e); - onSessionLost(network, e); + Log.d(TAG, "Transform application failed for token " + token, e); + onSessionLost(token, e); + } + } + + /** + * Called when an IPsec transform has been created, and should be re-applied. + * + * <p>This method is called multiple times over the lifetime of an IkeSession (or default + * network), and MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + public void onChildMigrated( + int token, + @NonNull IpSecTransform inTransform, + @NonNull IpSecTransform outTransform) { + if (!isActiveToken(token)) { + Log.d(TAG, "onChildMigrated for obsolete token " + token); + return; + } + + // The actual network of this IKE session has migrated to is + // mIkeConnectionInfo.getNetwork() instead of mActiveNetwork because mActiveNetwork + // might have been updated after the migration was triggered. + final Network network = mIkeConnectionInfo.getNetwork(); + + try { + synchronized (Vpn.this) { + mConfig.underlyingNetworks = new Network[] {network}; + mNetworkCapabilities = + new NetworkCapabilities.Builder(mNetworkCapabilities) + .setUnderlyingNetworks(Collections.singletonList(network)) + .build(); + mNetworkAgent.setUnderlyingNetworks(Collections.singletonList(network)); + } + + mTunnelIface.setUnderlyingNetwork(network); + + // Transforms do not need to be persisted; the IkeSession will keep them alive for + // us + mIpSecManager.applyTunnelModeTransform( + mTunnelIface, IpSecManager.DIRECTION_IN, inTransform); + mIpSecManager.applyTunnelModeTransform( + mTunnelIface, IpSecManager.DIRECTION_OUT, outTransform); + } catch (IOException e) { + Log.d(TAG, "Transform application failed for token " + token, e); + onSessionLost(token, e); } } /** * Called when a new default network is connected. * - * <p>The Ikev2VpnRunner will unconditionally switch to the new network, killing the old IKE - * state in the process, and starting a new IkeSession instance. + * <p>The Ikev2VpnRunner will unconditionally switch to the new network. If the IKE session + * has mobility, Ikev2VpnRunner will migrate the existing IkeSession to the new network. + * Otherwise, Ikev2VpnRunner will kill the old IKE state, and start a new IkeSession + * instance. * * <p>This method MUST always be called on the mExecutor thread in order to ensure * consistency of the Ikev2VpnRunner fields. */ public void onDefaultNetworkChanged(@NonNull Network network) { - Log.d(TAG, "Starting IKEv2/IPsec session on new network: " + network); + Log.d(TAG, "onDefaultNetworkChanged: " + network); + + // If there is a new default network brought up, cancel the retry task to prevent + // establishing an unnecessary IKE session. + cancelRetryNewIkeSessionFuture(); + + // If there is a new default network brought up, cancel the obsolete reset and retry + // task. + cancelHandleNetworkLostTimeout(); + + if (!mIsRunning) { + Log.d(TAG, "onDefaultNetworkChanged after exit"); + return; // VPN has been shut down. + } + + mActiveNetwork = network; + mRetryCount = 0; + + startOrMigrateIkeSession(network); + } + + /** + * Start a new IKE session. + * + * <p>This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + * + * @param underlyingNetwork if the value is {@code null}, which means there is no active + * network can be used, do nothing and return immediately. Otherwise, use the + * given network to start a new IKE session. + */ + private void startOrMigrateIkeSession(@Nullable Network underlyingNetwork) { + if (underlyingNetwork == null) { + Log.d(TAG, "There is no active network for starting an IKE session"); + return; + } try { - if (!mIsRunning) { - Log.d(TAG, "onDefaultNetworkChanged after exit"); - return; // VPN has been shut down. + if (mSession != null && mMobikeEnabled) { + // IKE session can schedule a migration event only when IKE AUTH is finished + // and mMobikeEnabled is true. + Log.d( + TAG, + "Migrate IKE Session with token " + + mCurrentToken + + " to network " + + underlyingNetwork); + mSession.setNetwork(underlyingNetwork); + return; } + Log.d(TAG, "Start new IKE session on network " + underlyingNetwork); + // Clear mInterface to prevent Ikev2VpnRunner being cleared when // interfaceRemoved() is called. mInterface = null; // Without MOBIKE, we have no way to seamlessly migrate. Close on old // (non-default) network, and start the new one. resetIkeState(); - mActiveNetwork = network; // Get Ike options from IkeTunnelConnectionParams if it's available in the // profile. @@ -2852,12 +3067,12 @@ public class Vpn { final ChildSessionParams childSessionParams; if (ikeTunConnParams != null) { final IkeSessionParams.Builder builder = new IkeSessionParams.Builder( - ikeTunConnParams.getIkeSessionParams()).setNetwork(network); + ikeTunConnParams.getIkeSessionParams()).setNetwork(underlyingNetwork); ikeSessionParams = builder.build(); childSessionParams = ikeTunConnParams.getTunnelModeChildSessionParams(); } else { ikeSessionParams = VpnIkev2Utils.buildIkeSessionParams( - mContext, mProfile, network); + mContext, mProfile, underlyingNetwork); childSessionParams = VpnIkev2Utils.buildChildSessionParams( mProfile.getAllowedAlgorithms()); } @@ -2865,29 +3080,50 @@ public class Vpn { // TODO: Remove the need for adding two unused addresses with // IPsec tunnels. final InetAddress address = InetAddress.getLocalHost(); + + // When onChildOpened is called and transforms are applied, it is + // guaranteed that the underlying network is still "network", because the + // all the network switch events will be deferred before onChildOpened is + // called. Thus it is safe to build a mTunnelIface before IKE setup. mTunnelIface = mIpSecManager.createIpSecTunnelInterface( - address /* unused */, - address /* unused */, - network); + address /* unused */, address /* unused */, underlyingNetwork); NetdUtils.setInterfaceUp(mNetd, mTunnelIface.getInterfaceName()); - mSession = mIkev2SessionCreator.createIkeSession( - mContext, - ikeSessionParams, - childSessionParams, - mExecutor, - new VpnIkev2Utils.IkeSessionCallbackImpl( - TAG, IkeV2VpnRunner.this, network), - new VpnIkev2Utils.ChildSessionCallbackImpl( - TAG, IkeV2VpnRunner.this, network)); - Log.d(TAG, "Ike Session started for network " + network); + final int token = ++mCurrentToken; + mSession = + mIkev2SessionCreator.createIkeSession( + mContext, + ikeSessionParams, + childSessionParams, + mExecutor, + new VpnIkev2Utils.IkeSessionCallbackImpl( + TAG, IkeV2VpnRunner.this, token), + new VpnIkev2Utils.ChildSessionCallbackImpl( + TAG, IkeV2VpnRunner.this, token)); + Log.d(TAG, "IKE session started for token " + token); } catch (Exception e) { - Log.i(TAG, "Setup failed for network " + network + ". Aborting", e); - onSessionLost(network, e); + Log.i(TAG, "Setup failed for token " + mCurrentToken + ". Aborting", e); + onSessionLost(mCurrentToken, e); } } + private void scheduleRetryNewIkeSession() { + final long retryDelay = mDeps.getNextRetryDelaySeconds(mRetryCount++); + Log.d(TAG, "Retry new IKE session after " + retryDelay + " seconds."); + // If the default network is lost during the retry delay, the mActiveNetwork will be + // null, and the new IKE session won't be established until there is a new default + // network bringing up. + mScheduledHandleRetryIkeSessionTimeout = + mExecutor.schedule(() -> { + startOrMigrateIkeSession(mActiveNetwork); + + // Reset mScheduledHandleRetryIkeSessionTimeout since it's already run on + // executor thread. + mScheduledHandleRetryIkeSessionTimeout = null; + }, retryDelay, TimeUnit.SECONDS); + } + /** Called when the NetworkCapabilities of underlying network is changed */ public void onDefaultNetworkCapabilitiesChanged(@NonNull NetworkCapabilities nc) { mUnderlyingNetworkCapabilities = nc; @@ -2898,6 +3134,99 @@ public class Vpn { mUnderlyingLinkProperties = lp; } + /** + * Handles loss of the default underlying network + * + * <p>If the IKE Session has mobility, Ikev2VpnRunner will schedule a teardown event with a + * delay so that the IKE Session can migrate if a new network is available soon. Otherwise, + * Ikev2VpnRunner will kill the IKE session and reset the VPN. + * + * <p>This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + public void onDefaultNetworkLost(@NonNull Network network) { + // If the default network is torn down, there is no need to call + // startOrMigrateIkeSession() since it will always check if there is an active network + // can be used or not. + cancelRetryNewIkeSessionFuture(); + + if (!isActiveNetwork(network)) { + Log.d(TAG, "onDefaultNetworkLost called for obsolete network " + network); + + // Do nothing; this signals that either: (1) a new/better Network was found, + // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in + // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited, + // or an error was encountered somewhere else). In both cases, all resources and + // sessions are torn down via resetIkeState(). + return; + } else { + mActiveNetwork = null; + } + + if (mScheduledHandleNetworkLostTimeout != null + && !mScheduledHandleNetworkLostTimeout.isCancelled() + && !mScheduledHandleNetworkLostTimeout.isDone()) { + final IllegalStateException exception = + new IllegalStateException( + "Found a pending mScheduledHandleNetworkLostTimeout"); + Log.i( + TAG, + "Unexpected error in onDefaultNetworkLost. Tear down session", + exception); + handleSessionLost(exception, network); + return; + } + + if (mSession != null && mMobikeEnabled) { + Log.d( + TAG, + "IKE Session has mobility. Delay handleSessionLost for losing network " + + network + + " on session with token " + + mCurrentToken); + + // Delay the teardown in case a new network will be available soon. For example, + // during handover between two WiFi networks, Android will disconnect from the + // first WiFi and then connects to the second WiFi. + mScheduledHandleNetworkLostTimeout = + mExecutor.schedule( + () -> { + handleSessionLost(null, network); + }, + NETWORK_LOST_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + } else { + Log.d(TAG, "Call handleSessionLost for losing network " + network); + handleSessionLost(null, network); + } + } + + private void cancelHandleNetworkLostTimeout() { + if (mScheduledHandleNetworkLostTimeout != null + && !mScheduledHandleNetworkLostTimeout.isDone()) { + // It does not matter what to put in #cancel(boolean), because it is impossible + // that the task tracked by mScheduledHandleNetworkLostTimeout is + // in-progress since both that task and onDefaultNetworkChanged are submitted to + // mExecutor who has only one thread. + Log.d(TAG, "Cancel the task for handling network lost timeout"); + mScheduledHandleNetworkLostTimeout.cancel(false /* mayInterruptIfRunning */); + mScheduledHandleNetworkLostTimeout = null; + } + } + + private void cancelRetryNewIkeSessionFuture() { + if (mScheduledHandleRetryIkeSessionTimeout != null + && !mScheduledHandleRetryIkeSessionTimeout.isDone()) { + // It does not matter what to put in #cancel(boolean), because it is impossible + // that the task tracked by mScheduledHandleRetryIkeSessionTimeout is + // in-progress since both that task and onDefaultNetworkChanged are submitted to + // mExecutor who has only one thread. + Log.d(TAG, "Cancel the task for handling new ike session timeout"); + mScheduledHandleRetryIkeSessionTimeout.cancel(false /* mayInterruptIfRunning */); + mScheduledHandleRetryIkeSessionTimeout = null; + } + } + /** Marks the state as FAILED, and disconnects. */ private void markFailedAndDisconnect(Exception exception) { synchronized (Vpn.this) { @@ -2916,18 +3245,28 @@ public class Vpn { * <p>This method MUST always be called on the mExecutor thread in order to ensure * consistency of the Ikev2VpnRunner fields. */ - public void onSessionLost(@NonNull Network network, @Nullable Exception exception) { - if (!isActiveNetwork(network)) { - Log.d(TAG, "onSessionLost() called for obsolete network " + network); + public void onSessionLost(int token, @Nullable Exception exception) { + Log.d(TAG, "onSessionLost() called for token " + token); + + if (!isActiveToken(token)) { + Log.d(TAG, "onSessionLost() called for obsolete token " + token); // Do nothing; this signals that either: (1) a new/better Network was found, - // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this - // IKE session was already shut down (exited, or an error was encountered somewhere - // else). In both cases, all resources and sessions are torn down via - // onSessionLost() and resetIkeState(). + // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in + // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited, + // or an error was encountered somewhere else). In both cases, all resources and + // sessions are torn down via resetIkeState(). return; } + handleSessionLost(exception, mActiveNetwork); + } + + private void handleSessionLost(@Nullable Exception exception, @Nullable Network network) { + // Cancel mScheduledHandleNetworkLostTimeout if the session it is going to terminate is + // already terminated due to other failures. + cancelHandleNetworkLostTimeout(); + synchronized (Vpn.this) { if (exception instanceof IkeProtocolException) { final IkeProtocolException ikeException = (IkeProtocolException) exception; @@ -2947,7 +3286,7 @@ public class Vpn { VpnManager.ERROR_CLASS_NOT_RECOVERABLE, ikeException.getErrorType(), getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -2965,7 +3304,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, ikeException.getErrorType(), getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -2984,7 +3323,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_LOST, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -2999,7 +3338,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3013,7 +3352,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3027,7 +3366,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_IO, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3037,15 +3376,16 @@ public class Vpn { } else if (exception != null) { Log.wtf(TAG, "onSessionLost: exception = " + exception); } + + scheduleRetryNewIkeSession(); } - mActiveNetwork = null; mUnderlyingNetworkCapabilities = null; mUnderlyingLinkProperties = null; // Close all obsolete state, but keep VPN alive incase a usable network comes up. // (Mirrors VpnService behavior) - Log.d(TAG, "Resetting state for network: " + network); + Log.d(TAG, "Resetting state for token: " + mCurrentToken); synchronized (Vpn.this) { // Since this method handles non-fatal errors only, set mInterface to null to @@ -3090,6 +3430,8 @@ public class Vpn { mSession.kill(); // Kill here to make sure all resources are released immediately mSession = null; } + mIkeConnectionInfo = null; + mMobikeEnabled = false; } /** diff --git a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java index 17058282d947..857c86de57ca 100644 --- a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java +++ b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java @@ -68,6 +68,7 @@ import android.net.ipsec.ike.IkeRfc822AddrIdentification; import android.net.ipsec.ike.IkeSaProposal; import android.net.ipsec.ike.IkeSessionCallback; import android.net.ipsec.ike.IkeSessionConfiguration; +import android.net.ipsec.ike.IkeSessionConnectionInfo; import android.net.ipsec.ike.IkeSessionParams; import android.net.ipsec.ike.IkeTrafficSelector; import android.net.ipsec.ike.TunnelModeChildSessionParams; @@ -107,6 +108,7 @@ public class VpnIkev2Utils { new IkeSessionParams.Builder(context) .setServerHostname(profile.getServerAddr()) .setNetwork(network) + .addIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE) .setLocalIdentification(localId) .setRemoteIdentification(remoteId); setIkeAuth(profile, ikeOptionsBuilder); @@ -298,72 +300,79 @@ public class VpnIkev2Utils { static class IkeSessionCallbackImpl implements IkeSessionCallback { private final String mTag; private final Vpn.IkeV2VpnRunnerCallback mCallback; - private final Network mNetwork; + private final int mToken; - IkeSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, Network network) { + IkeSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, int token) { mTag = tag; mCallback = callback; - mNetwork = network; + mToken = token; } @Override public void onOpened(@NonNull IkeSessionConfiguration ikeSessionConfig) { - Log.d(mTag, "IkeOpened for network " + mNetwork); - // Nothing to do here. + Log.d(mTag, "IkeOpened for token " + mToken); + mCallback.onIkeOpened(mToken, ikeSessionConfig); } @Override public void onClosed() { - Log.d(mTag, "IkeClosed for network " + mNetwork); - mCallback.onSessionLost(mNetwork, null); // Server requested session closure. Retry? + Log.d(mTag, "IkeClosed for token " + mToken); + mCallback.onSessionLost(mToken, null); // Server requested session closure. Retry? } @Override public void onClosedExceptionally(@NonNull IkeException exception) { - Log.d(mTag, "IkeClosedExceptionally for network " + mNetwork, exception); - mCallback.onSessionLost(mNetwork, exception); + Log.d(mTag, "IkeClosedExceptionally for token " + mToken, exception); + mCallback.onSessionLost(mToken, exception); } @Override public void onError(@NonNull IkeProtocolException exception) { - Log.d(mTag, "IkeError for network " + mNetwork, exception); + Log.d(mTag, "IkeError for token " + mToken, exception); // Non-fatal, log and continue. } + + @Override + public void onIkeSessionConnectionInfoChanged( + @NonNull IkeSessionConnectionInfo connectionInfo) { + Log.d(mTag, "onIkeSessionConnectionInfoChanged for token " + mToken); + mCallback.onIkeConnectionInfoChanged(mToken, connectionInfo); + } } static class ChildSessionCallbackImpl implements ChildSessionCallback { private final String mTag; private final Vpn.IkeV2VpnRunnerCallback mCallback; - private final Network mNetwork; + private final int mToken; - ChildSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, Network network) { + ChildSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, int token) { mTag = tag; mCallback = callback; - mNetwork = network; + mToken = token; } @Override public void onOpened(@NonNull ChildSessionConfiguration childConfig) { - Log.d(mTag, "ChildOpened for network " + mNetwork); - mCallback.onChildOpened(mNetwork, childConfig); + Log.d(mTag, "ChildOpened for token " + mToken); + mCallback.onChildOpened(mToken, childConfig); } @Override public void onClosed() { - Log.d(mTag, "ChildClosed for network " + mNetwork); - mCallback.onSessionLost(mNetwork, null); + Log.d(mTag, "ChildClosed for token " + mToken); + mCallback.onSessionLost(mToken, null); } @Override public void onClosedExceptionally(@NonNull IkeException exception) { - Log.d(mTag, "ChildClosedExceptionally for network " + mNetwork, exception); - mCallback.onSessionLost(mNetwork, exception); + Log.d(mTag, "ChildClosedExceptionally for token " + mToken, exception); + mCallback.onSessionLost(mToken, exception); } @Override public void onIpSecTransformCreated(@NonNull IpSecTransform transform, int direction) { - Log.d(mTag, "ChildTransformCreated; Direction: " + direction + "; network " + mNetwork); - mCallback.onChildTransformCreated(mNetwork, transform, direction); + Log.d(mTag, "ChildTransformCreated; Direction: " + direction + "; token " + mToken); + mCallback.onChildTransformCreated(mToken, transform, direction); } @Override @@ -371,8 +380,15 @@ public class VpnIkev2Utils { // Nothing to be done; no references to the IpSecTransform are held by the // Ikev2VpnRunner (or this callback class), and this transform will be closed by the // IKE library. - Log.d(mTag, - "ChildTransformDeleted; Direction: " + direction + "; for network " + mNetwork); + Log.d(mTag, "ChildTransformDeleted; Direction: " + direction + "; for token " + mToken); + } + + @Override + public void onIpSecTransformsMigrated( + @NonNull IpSecTransform inIpSecTransform, + @NonNull IpSecTransform outIpSecTransform) { + Log.d(mTag, "ChildTransformsMigrated; token " + mToken); + mCallback.onChildMigrated(mToken, inIpSecTransform, outIpSecTransform); } } @@ -390,7 +406,7 @@ public class VpnIkev2Utils { @Override public void onAvailable(@NonNull Network network) { - Log.d(mTag, "Starting IKEv2/IPsec session on new network: " + network); + Log.d(mTag, "onAvailable called for network: " + network); mExecutor.execute(() -> mCallback.onDefaultNetworkChanged(network)); } @@ -412,8 +428,8 @@ public class VpnIkev2Utils { @Override public void onLost(@NonNull Network network) { - Log.d(mTag, "Tearing down; lost network: " + network); - mExecutor.execute(() -> mCallback.onSessionLost(network, null)); + Log.d(mTag, "onLost called for network: " + network); + mExecutor.execute(() -> mCallback.onDefaultNetworkLost(network)); } } diff --git a/services/core/java/com/android/server/display/BrightnessThrottler.java b/services/core/java/com/android/server/display/BrightnessThrottler.java index 767b2d18a69a..eccee52f37aa 100644 --- a/services/core/java/com/android/server/display/BrightnessThrottler.java +++ b/services/core/java/com/android/server/display/BrightnessThrottler.java @@ -16,21 +16,31 @@ package com.android.server.display; +import android.annotation.NonNull; import android.content.Context; import android.hardware.display.BrightnessInfo; +import android.hardware.display.DisplayManager; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IThermalEventListener; import android.os.IThermalService; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.Temperature; +import android.provider.DeviceConfig; +import android.provider.DeviceConfigInterface; import android.util.Slog; -import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData; +import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Executor; /** * This class monitors various conditions, such as skin temperature throttling status, and limits @@ -44,28 +54,54 @@ class BrightnessThrottler { private final Injector mInjector; private final Handler mHandler; - private BrightnessThrottlingData mThrottlingData; + // We need a separate handler for unit testing. These two handlers are the same throughout the + // non-test code. + private final Handler mDeviceConfigHandler; private final Runnable mThrottlingChangeCallback; private final SkinThermalStatusObserver mSkinThermalStatusObserver; + private final DeviceConfigListener mDeviceConfigListener; + private final DeviceConfigInterface mDeviceConfig; + private int mThrottlingStatus; + private BrightnessThrottlingData mThrottlingData; + private BrightnessThrottlingData mDdcThrottlingData; private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; + private String mUniqueDisplayId; + + // The most recent string that has been set from DeviceConfig + private String mBrightnessThrottlingDataString; + + // This is a collection of brightness throttling data that has been written as overrides from + // the DeviceConfig. This will always take priority over the display device config data. + private HashMap<String, BrightnessThrottlingData> mBrightnessThrottlingDataOverride = + new HashMap<>(1); BrightnessThrottler(Handler handler, BrightnessThrottlingData throttlingData, - Runnable throttlingChangeCallback) { - this(new Injector(), handler, throttlingData, throttlingChangeCallback); + Runnable throttlingChangeCallback, String uniqueDisplayId) { + this(new Injector(), handler, handler, throttlingData, throttlingChangeCallback, + uniqueDisplayId); } - BrightnessThrottler(Injector injector, Handler handler, BrightnessThrottlingData throttlingData, - Runnable throttlingChangeCallback) { + @VisibleForTesting + BrightnessThrottler(Injector injector, Handler handler, Handler deviceConfigHandler, + BrightnessThrottlingData throttlingData, Runnable throttlingChangeCallback, + String uniqueDisplayId) { mInjector = injector; + mHandler = handler; + mDeviceConfigHandler = deviceConfigHandler; mThrottlingData = throttlingData; + mDdcThrottlingData = throttlingData; mThrottlingChangeCallback = throttlingChangeCallback; mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler); - resetThrottlingData(mThrottlingData); + mUniqueDisplayId = uniqueDisplayId; + mDeviceConfig = injector.getDeviceConfig(); + mDeviceConfigListener = new DeviceConfigListener(); + + resetThrottlingData(mThrottlingData, mUniqueDisplayId); } boolean deviceSupportsThrottling() { @@ -86,7 +122,7 @@ class BrightnessThrottler { void stop() { mSkinThermalStatusObserver.stopObserving(); - + mDeviceConfig.removeOnPropertiesChangedListener(mDeviceConfigListener); // We're asked to stop throttling, so reset brightness restrictions. mBrightnessCap = PowerManager.BRIGHTNESS_MAX; mBrightnessMaxReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; @@ -97,9 +133,19 @@ class BrightnessThrottler { mThrottlingStatus = THROTTLING_INVALID; } - void resetThrottlingData(BrightnessThrottlingData throttlingData) { + private void resetThrottlingData() { + resetThrottlingData(mDdcThrottlingData, mUniqueDisplayId); + } + + void resetThrottlingData(BrightnessThrottlingData throttlingData, String displayId) { stop(); - mThrottlingData = throttlingData; + + mUniqueDisplayId = displayId; + mDdcThrottlingData = throttlingData; + mDeviceConfigListener.startListening(); + reloadBrightnessThrottlingDataOverride(); + mThrottlingData = mBrightnessThrottlingDataOverride.getOrDefault(mUniqueDisplayId, + throttlingData); if (deviceSupportsThrottling()) { mSkinThermalStatusObserver.startObserving(); @@ -173,14 +219,148 @@ class BrightnessThrottler { private void dumpLocal(PrintWriter pw) { pw.println("BrightnessThrottler:"); pw.println(" mThrottlingData=" + mThrottlingData); + pw.println(" mDdcThrottlingData=" + mDdcThrottlingData); + pw.println(" mUniqueDisplayId=" + mUniqueDisplayId); pw.println(" mThrottlingStatus=" + mThrottlingStatus); pw.println(" mBrightnessCap=" + mBrightnessCap); pw.println(" mBrightnessMaxReason=" + BrightnessInfo.briMaxReasonToString(mBrightnessMaxReason)); + pw.println(" mBrightnessThrottlingDataOverride=" + mBrightnessThrottlingDataOverride); + pw.println(" mBrightnessThrottlingDataString=" + mBrightnessThrottlingDataString); mSkinThermalStatusObserver.dump(pw); } + private String getBrightnessThrottlingDataString() { + return mDeviceConfig.getString(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA, + /* defaultValue= */ null); + } + + private boolean parseAndSaveData(@NonNull String strArray, + @NonNull HashMap<String, BrightnessThrottlingData> tempBrightnessThrottlingData) { + boolean validConfig = true; + String[] items = strArray.split(","); + int i = 0; + + try { + String uniqueDisplayId = items[i++]; + + // number of throttling points + int noOfThrottlingPoints = Integer.parseInt(items[i++]); + List<ThrottlingLevel> throttlingLevels = new ArrayList<>(noOfThrottlingPoints); + + // throttling level and point + for (int j = 0; j < noOfThrottlingPoints; j++) { + String severity = items[i++]; + int status = parseThermalStatus(severity); + + float brightnessPoint = parseBrightness(items[i++]); + + throttlingLevels.add(new ThrottlingLevel(status, brightnessPoint)); + } + BrightnessThrottlingData toSave = + DisplayDeviceConfig.BrightnessThrottlingData.create(throttlingLevels); + tempBrightnessThrottlingData.put(uniqueDisplayId, toSave); + } catch (NumberFormatException | IndexOutOfBoundsException + | UnknownThermalStatusException e) { + validConfig = false; + Slog.e(TAG, "Throttling data is invalid array: '" + strArray + "'", e); + } + + if (i != items.length) { + validConfig = false; + } + + return validConfig; + } + + public void reloadBrightnessThrottlingDataOverride() { + HashMap<String, BrightnessThrottlingData> tempBrightnessThrottlingData = + new HashMap<>(1); + mBrightnessThrottlingDataString = getBrightnessThrottlingDataString(); + boolean validConfig = true; + mBrightnessThrottlingDataOverride.clear(); + if (mBrightnessThrottlingDataString != null) { + String[] throttlingDataSplits = mBrightnessThrottlingDataString.split(";"); + for (String s : throttlingDataSplits) { + if (!parseAndSaveData(s, tempBrightnessThrottlingData)) { + validConfig = false; + break; + } + } + + if (validConfig) { + mBrightnessThrottlingDataOverride.putAll(tempBrightnessThrottlingData); + tempBrightnessThrottlingData.clear(); + } + + } else { + Slog.w(TAG, "DeviceConfig BrightnessThrottlingData is null"); + } + } + + /** + * Listens to config data change and updates the brightness throttling data using + * DisplayManager#KEY_BRIGHTNESS_THROTTLING_DATA. + * The format should be a string similar to: "local:4619827677550801152,2,moderate,0.5,severe, + * 0.379518072;local:4619827677550801151,1,moderate,0.75" + * In this order: + * <displayId>,<no of throttling levels>,[<severity as string>,<brightness cap>] + * Where the latter part is repeated for each throttling level, and the entirety is repeated + * for each display, separated by a semicolon. + */ + public class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener { + public Executor mExecutor = new HandlerExecutor(mDeviceConfigHandler); + + public void startListening() { + mDeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + mExecutor, this); + } + + @Override + public void onPropertiesChanged(DeviceConfig.Properties properties) { + reloadBrightnessThrottlingDataOverride(); + resetThrottlingData(); + } + } + + private float parseBrightness(String intVal) throws NumberFormatException { + float value = Float.parseFloat(intVal); + if (value < PowerManager.BRIGHTNESS_MIN || value > PowerManager.BRIGHTNESS_MAX) { + throw new NumberFormatException("Brightness constraint value out of bounds."); + } + return value; + } + + @PowerManager.ThermalStatus private int parseThermalStatus(@NonNull String value) + throws UnknownThermalStatusException { + switch (value) { + case "none": + return PowerManager.THERMAL_STATUS_NONE; + case "light": + return PowerManager.THERMAL_STATUS_LIGHT; + case "moderate": + return PowerManager.THERMAL_STATUS_MODERATE; + case "severe": + return PowerManager.THERMAL_STATUS_SEVERE; + case "critical": + return PowerManager.THERMAL_STATUS_CRITICAL; + case "emergency": + return PowerManager.THERMAL_STATUS_EMERGENCY; + case "shutdown": + return PowerManager.THERMAL_STATUS_SHUTDOWN; + default: + throw new UnknownThermalStatusException("Invalid Thermal Status: " + value); + } + } + + private static class UnknownThermalStatusException extends Exception { + UnknownThermalStatusException(String message) { + super(message); + } + } + private final class SkinThermalStatusObserver extends IThermalEventListener.Stub { private final Injector mInjector; private final Handler mHandler; @@ -258,5 +438,10 @@ class BrightnessThrottler { return IThermalService.Stub.asInterface( ServiceManager.getService(Context.THERMAL_SERVICE)); } + + @NonNull + public DeviceConfigInterface getDeviceConfig() { + return DeviceConfigInterface.REAL; + } } } diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index a25ac210f9c8..2322280d8a9b 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -279,9 +279,13 @@ public class DisplayDeviceConfig { private HighBrightnessModeData mHbmData; private DensityMapping mDensityMapping; private String mLoadedFrom = null; + private Spline mSdrToHdrRatioSpline; + // Brightness Throttling data may be updated via the DeviceConfig. Here we store the original + // data, which comes from the ddc, and the current one, which may be the DeviceConfig + // overwritten value. private BrightnessThrottlingData mBrightnessThrottlingData; - private Spline mSdrToHdrRatioSpline; + private BrightnessThrottlingData mOriginalBrightnessThrottlingData; private DisplayDeviceConfig(Context context) { mContext = context; @@ -422,6 +426,10 @@ public class DisplayDeviceConfig { return config; } + void setBrightnessThrottlingData(BrightnessThrottlingData brightnessThrottlingData) { + mBrightnessThrottlingData = brightnessThrottlingData; + } + /** * Return the brightness mapping nits array. * @@ -637,6 +645,7 @@ public class DisplayDeviceConfig { + ", mHbmData=" + mHbmData + ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline + ", mBrightnessThrottlingData=" + mBrightnessThrottlingData + + ", mOriginalBrightnessThrottlingData=" + mOriginalBrightnessThrottlingData + ", mBrightnessRampFastDecrease=" + mBrightnessRampFastDecrease + ", mBrightnessRampFastIncrease=" + mBrightnessRampFastIncrease + ", mBrightnessRampSlowDecrease=" + mBrightnessRampSlowDecrease @@ -932,6 +941,7 @@ public class DisplayDeviceConfig { if (!badConfig) { mBrightnessThrottlingData = BrightnessThrottlingData.create(throttlingLevels); + mOriginalBrightnessThrottlingData = mBrightnessThrottlingData; } } @@ -1407,7 +1417,9 @@ public class DisplayDeviceConfig { /** * Container for brightness throttling data. */ - static class BrightnessThrottlingData { + public static class BrightnessThrottlingData { + public List<ThrottlingLevel> throttlingLevels; + static class ThrottlingLevel { public @PowerManager.ThermalStatus int thermalStatus; public float brightness; @@ -1421,9 +1433,25 @@ public class DisplayDeviceConfig { public String toString() { return "[" + thermalStatus + "," + brightness + "]"; } - } - public List<ThrottlingLevel> throttlingLevels; + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ThrottlingLevel)) { + return false; + } + ThrottlingLevel otherThrottlingLevel = (ThrottlingLevel) obj; + + return otherThrottlingLevel.thermalStatus == this.thermalStatus + && otherThrottlingLevel.brightness == this.brightness; + } + @Override + public int hashCode() { + int result = 1; + result = 31 * result + thermalStatus; + result = 31 * result + Float.hashCode(brightness); + return result; + } + } static public BrightnessThrottlingData create(List<ThrottlingLevel> throttlingLevels) { @@ -1482,12 +1510,30 @@ public class DisplayDeviceConfig { + "} "; } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof BrightnessThrottlingData)) { + return false; + } + + BrightnessThrottlingData otherBrightnessThrottlingData = (BrightnessThrottlingData) obj; + return throttlingLevels.equals(otherBrightnessThrottlingData.throttlingLevels); + } + + @Override + public int hashCode() { + return throttlingLevels.hashCode(); + } + private BrightnessThrottlingData(List<ThrottlingLevel> inLevels) { throttlingLevels = new ArrayList<>(inLevels.size()); for (ThrottlingLevel level : inLevels) { throttlingLevels.add(new ThrottlingLevel(level.thermalStatus, level.brightness)); } } - } } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index d05a902c6593..95c8fef12976 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -461,6 +461,18 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private boolean mIsRbcActive; + // Whether there's a callback to tell listeners the display has changed scheduled to run. When + // true it implies a wakelock is being held to guarantee the update happens before we collapse + // into suspend and so needs to be cleaned up if the thread is exiting. + // Should only be accessed on the Handler thread. + private boolean mOnStateChangedPending; + + // Count of proximity messages currently on this DPC's Handler. Used to keep track of how many + // suspend blocker acquisitions are pending when shutting down this DPC. + // Should only be accessed on the Handler thread. + private int mOnProximityPositiveMessages; + private int mOnProximityNegativeMessages; + // Animators. private ObjectAnimator mColorFadeOnAnimator; private ObjectAnimator mColorFadeOffAnimator; @@ -861,7 +873,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } }); mBrightnessThrottler.resetThrottlingData( - mDisplayDeviceConfig.getBrightnessThrottlingData()); + mDisplayDeviceConfig.getBrightnessThrottlingData(), mUniqueDisplayId); } private void sendUpdatePowerState() { @@ -1091,10 +1103,24 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mHbmController.stop(); mBrightnessThrottler.stop(); mHandler.removeCallbacksAndMessages(null); + + // Release any outstanding wakelocks we're still holding because of pending messages. if (mUnfinishedBusiness) { mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness); mUnfinishedBusiness = false; } + if (mOnStateChangedPending) { + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged); + mOnStateChangedPending = false; + } + for (int i = 0; i < mOnProximityPositiveMessages; i++) { + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive); + } + mOnProximityPositiveMessages = 0; + for (int i = 0; i < mOnProximityNegativeMessages; i++) { + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative); + } + mOnProximityNegativeMessages = 0; final float brightness = mPowerState != null ? mPowerState.getScreenBrightness() @@ -1816,7 +1842,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call () -> { sendUpdatePowerStateLocked(); postBrightnessChangeRunnable(); - }); + }, mUniqueDisplayId); } private void blockScreenOn() { @@ -2248,8 +2274,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } private void sendOnStateChangedWithWakelock() { - mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged); - mHandler.post(mOnStateChangedRunnable); + if (!mOnStateChangedPending) { + mOnStateChangedPending = true; + mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged); + mHandler.post(mOnStateChangedRunnable); + } } private void logDisplayPolicyChanged(int newPolicy) { @@ -2408,6 +2437,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private final Runnable mOnStateChangedRunnable = new Runnable() { @Override public void run() { + mOnStateChangedPending = false; mCallbacks.onStateChanged(); mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged); } @@ -2416,17 +2446,20 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private void sendOnProximityPositiveWithWakelock() { mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxPositive); mHandler.post(mOnProximityPositiveRunnable); + mOnProximityPositiveMessages++; } private final Runnable mOnProximityPositiveRunnable = new Runnable() { @Override public void run() { + mOnProximityPositiveMessages--; mCallbacks.onProximityPositive(); mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive); } }; private void sendOnProximityNegativeWithWakelock() { + mOnProximityNegativeMessages++; mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxNegative); mHandler.post(mOnProximityNegativeRunnable); } @@ -2434,6 +2467,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private final Runnable mOnProximityNegativeRunnable = new Runnable() { @Override public void run() { + mOnProximityNegativeMessages--; mCallbacks.onProximityNegative(); mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative); } @@ -2533,6 +2567,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call pw.println(" mReportedToPolicy=" + reportedToPolicyToString(mReportedScreenStateToPolicy)); pw.println(" mIsRbcActive=" + mIsRbcActive); + pw.println(" mOnStateChangePending=" + mOnStateChangedPending); + pw.println(" mOnProximityPositiveMessages=" + mOnProximityPositiveMessages); + pw.println(" mOnProximityNegativeMessages=" + mOnProximityNegativeMessages); if (mScreenBrightnessRampAnimator != null) { pw.println(" mScreenBrightnessRampAnimator.isAnimating()=" diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java index 8de150ac4124..223b8c181fea 100644 --- a/services/core/java/com/android/server/display/color/ColorDisplayService.java +++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java @@ -956,6 +956,8 @@ public final class ColorDisplayService extends SystemService { R.array.config_availableColorModes); if (availableColorModes.length > 0) { colorMode = availableColorModes[0]; + } else { + colorMode = NOT_SET; } } } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 098e8f74749c..7d12ede754ef 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -46,7 +46,6 @@ import android.os.UserHandle; import android.util.ArrayMap; import android.util.Slog; import android.view.ContentRecordingSession; -import android.window.WindowContainerToken; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; @@ -433,7 +432,7 @@ public final class MediaProjectionManagerService extends SystemService private IBinder mToken; private IBinder.DeathRecipient mDeathEater; private boolean mRestoreSystemAlertWindow; - private WindowContainerToken mTaskRecordingWindowContainerToken = null; + private IBinder mLaunchCookie = null; MediaProjection(int type, int uid, String packageName, int targetSdkVersion, boolean isPrivileged) { @@ -609,14 +608,13 @@ public final class MediaProjectionManagerService extends SystemService } @Override // Binder call - public void setTaskRecordingWindowContainerToken(WindowContainerToken token) { - // TODO(b/221417940) set the task id to record from sysui, for the package chosen. - mTaskRecordingWindowContainerToken = token; + public void setLaunchCookie(IBinder launchCookie) { + mLaunchCookie = launchCookie; } @Override // Binder call - public WindowContainerToken getTaskRecordingWindowContainerToken() { - return mTaskRecordingWindowContainerToken; + public IBinder getLaunchCookie() { + return mLaunchCookie; } public MediaProjectionInfo getProjectionInfo() { diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index de9102a69a2e..6135fe8acbed 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -340,7 +340,8 @@ public class ZenModeHelper { int newRuleInstanceCount = getCurrentInstanceCount(automaticZenRule.getOwner()) + getCurrentInstanceCount(automaticZenRule.getConfigurationActivity()) + 1; - if (newRuleInstanceCount > RULE_LIMIT_PER_PACKAGE + int newPackageRuleCount = getPackageRuleCount(pkg) + 1; + if (newPackageRuleCount > RULE_LIMIT_PER_PACKAGE || (ruleInstanceLimit > 0 && ruleInstanceLimit < newRuleInstanceCount)) { throw new IllegalArgumentException("Rule instance limit exceeded"); } @@ -521,6 +522,23 @@ public class ZenModeHelper { return count; } + // Equivalent method to getCurrentInstanceCount, but for all rules associated with a specific + // package rather than a condition provider service or activity. + private int getPackageRuleCount(String pkg) { + if (pkg == null) { + return 0; + } + int count = 0; + synchronized (mConfig) { + for (ZenRule rule : mConfig.automaticRules.values()) { + if (pkg.equals(rule.getPkg())) { + count++; + } + } + } + return count; + } + public boolean canManageAutomaticZenRule(ZenRule rule) { final int callingUid = Binder.getCallingUid(); if (callingUid == 0 || callingUid == Process.SYSTEM_UID) { diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java index af0a20ddf337..6cfe093df6d0 100644 --- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java +++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java @@ -936,12 +936,15 @@ public class PackageDexOptimizer { String classLoaderContext, int profileAnalysisResult, boolean downgrade, int dexoptFlags, String oatDir) { final boolean shouldBePublic = (dexoptFlags & DEXOPT_PUBLIC) != 0; - // If the artifacts should be public while the current artifacts are not, we should - // re-compile anyway. - if (shouldBePublic && isOdexPrivate(packageName, path, isa, oatDir)) { - // Ensure compilation by pretending a compiler filter change on the apk/odex location - // (the reason for the '-'. A positive value means the 'oat' location). - return adjustDexoptNeeded(-DexFile.DEX2OAT_FOR_FILTER); + final boolean isProfileGuidedFilter = (dexoptFlags & DEXOPT_PROFILE_GUIDED) != 0; + boolean newProfile = profileAnalysisResult == PROFILE_ANALYSIS_OPTIMIZE; + + if (!newProfile && isProfileGuidedFilter && shouldBePublic + && isOdexPrivate(packageName, path, isa, oatDir)) { + // The profile that will be used is a cloud profile, while the profile used previously + // is a user profile. Typically, this happens after an app starts being used by other + // apps. + newProfile = true; } int dexoptNeeded; @@ -959,7 +962,6 @@ public class PackageDexOptimizer { && profileAnalysisResult == PROFILE_ANALYSIS_DONT_OPTIMIZE_EMPTY_PROFILES) { actualCompilerFilter = "verify"; } - boolean newProfile = profileAnalysisResult == PROFILE_ANALYSIS_OPTIMIZE; dexoptNeeded = DexFile.getDexOptNeeded(path, isa, actualCompilerFilter, classLoaderContext, newProfile, downgrade); } catch (IOException ioe) { diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index a01942d0dfa8..bb23d89d218f 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -1346,7 +1346,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements private String getDeviceOwnerDeletedPackageMsg() { DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); return dpm.getResources().getString(PACKAGE_DELETED_BY_DO, - () -> mContext.getString(R.string.package_updated_device_owner)); + () -> mContext.getString(R.string.package_deleted_device_owner)); } @Override diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java index 88b4a94f7027..03e568c4599e 100644 --- a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java @@ -413,8 +413,8 @@ public class LegacyPermissionManagerService extends ILegacyPermissionManager.Stu return result; } mContext.getSystemService(AppOpsManager.class).noteOpNoThrow( - AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, uid, packageName, - attributionTag, reason); + AppOpsManager.OP_RECORD_AUDIO_HOTWORD, uid, packageName, attributionTag, + reason); return result; } } diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index 06a54a461d5e..9bfb40fe11f7 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java @@ -1540,6 +1540,7 @@ public class ParsingPackageUtils { try { int minVers = ParsingUtils.DEFAULT_MIN_SDK_VERSION; String minCode = null; + boolean minAssigned = false; int targetVers = ParsingUtils.DEFAULT_TARGET_SDK_VERSION; String targetCode = null; int maxVers = Integer.MAX_VALUE; @@ -1548,9 +1549,11 @@ public class ParsingPackageUtils { if (val != null) { if (val.type == TypedValue.TYPE_STRING && val.string != null) { minCode = val.string.toString(); + minAssigned = !TextUtils.isEmpty(minCode); } else { // If it's not a string, it's an integer. minVers = val.data; + minAssigned = true; } } @@ -1558,7 +1561,7 @@ public class ParsingPackageUtils { if (val != null) { if (val.type == TypedValue.TYPE_STRING && val.string != null) { targetCode = val.string.toString(); - if (minCode == null) { + if (!minAssigned) { minCode = targetCode; } } else { diff --git a/services/core/java/com/android/server/testharness/TestHarnessModeService.java b/services/core/java/com/android/server/testharness/TestHarnessModeService.java index b6a413524c5c..452bdf409828 100644 --- a/services/core/java/com/android/server/testharness/TestHarnessModeService.java +++ b/services/core/java/com/android/server/testharness/TestHarnessModeService.java @@ -189,6 +189,7 @@ public class TestHarnessModeService extends SystemService { if (adbManager.getAdbTempKeysFile() != null) { writeBytesToFile(persistentData.mAdbTempKeys, adbManager.getAdbTempKeysFile().toPath()); } + adbManager.notifyKeyFilesUpdated(); } private void configureUser() { diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java index 78b1c20ac4b2..d79837be3583 100644 --- a/services/core/java/com/android/server/vibrator/Vibration.java +++ b/services/core/java/com/android/server/vibrator/Vibration.java @@ -61,12 +61,11 @@ final class Vibration { IGNORED_BACKGROUND, IGNORED_UNKNOWN_VIBRATION, IGNORED_UNSUPPORTED, - IGNORED_FOR_ALARM, IGNORED_FOR_EXTERNAL, + IGNORED_FOR_HIGHER_IMPORTANCE, IGNORED_FOR_ONGOING, IGNORED_FOR_POWER, IGNORED_FOR_RINGER_MODE, - IGNORED_FOR_RINGTONE, IGNORED_FOR_SETTINGS, IGNORED_SUPERSEDED, } diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index f0911ca62027..5ac2f4f27452 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -713,14 +713,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { case IGNORED_ERROR_APP_OPS: Slog.w(TAG, "Would be an error: vibrate from uid " + uid); break; - case IGNORED_FOR_ALARM: + case IGNORED_FOR_EXTERNAL: if (DEBUG) { - Slog.d(TAG, "Ignoring incoming vibration in favor of alarm vibration"); + Slog.d(TAG, "Ignoring incoming vibration for current external vibration"); } break; - case IGNORED_FOR_EXTERNAL: + case IGNORED_FOR_HIGHER_IMPORTANCE: if (DEBUG) { - Slog.d(TAG, "Ignoring incoming vibration for current external vibration"); + Slog.d(TAG, "Ignoring incoming vibration in favor of ongoing vibration" + + " with higher importance"); } break; case IGNORED_FOR_ONGOING: @@ -734,12 +735,6 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { + attrs); } break; - case IGNORED_FOR_RINGTONE: - if (DEBUG) { - Slog.d(TAG, "Ignoring incoming vibration in favor of ringtone vibration"); - } - break; - default: if (DEBUG) { Slog.d(TAG, "Vibration for uid=" + uid + " and with attrs=" + attrs @@ -812,20 +807,43 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return null; } - if (currentVibration.attrs.getUsage() == VibrationAttributes.USAGE_ALARM) { - return Vibration.Status.IGNORED_FOR_ALARM; - } - - if (currentVibration.attrs.getUsage() == VibrationAttributes.USAGE_RINGTONE) { - return Vibration.Status.IGNORED_FOR_RINGTONE; + int currentUsage = currentVibration.attrs.getUsage(); + int newUsage = vib.attrs.getUsage(); + if (getVibrationImportance(currentUsage) > getVibrationImportance(newUsage)) { + // Current vibration has higher importance than this one and should not be cancelled. + return Vibration.Status.IGNORED_FOR_HIGHER_IMPORTANCE; } if (currentVibration.isRepeating()) { + // Current vibration is repeating, assume it's more important. return Vibration.Status.IGNORED_FOR_ONGOING; } + return null; } + private static int getVibrationImportance(@VibrationAttributes.Usage int usage) { + switch (usage) { + case VibrationAttributes.USAGE_RINGTONE: + return 5; + case VibrationAttributes.USAGE_ALARM: + return 4; + case VibrationAttributes.USAGE_NOTIFICATION: + return 3; + case VibrationAttributes.USAGE_COMMUNICATION_REQUEST: + case VibrationAttributes.USAGE_ACCESSIBILITY: + return 2; + case VibrationAttributes.USAGE_HARDWARE_FEEDBACK: + case VibrationAttributes.USAGE_PHYSICAL_EMULATION: + return 1; + case VibrationAttributes.USAGE_MEDIA: + case VibrationAttributes.USAGE_TOUCH: + case VibrationAttributes.USAGE_UNKNOWN: + default: + return 0; + } + } + /** * Check if given vibration should be ignored by this service. * diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 622de57a1078..270891fcf421 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -56,6 +56,10 @@ import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_T import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_WARM_LAUNCH; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE; import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__APPEARED_APPLY_TREATMENT; @@ -1376,7 +1380,7 @@ class ActivityMetricsLogger { return; } - logAppCompatStateInternal(activity, state, packageUid, compatStateInfo); + logAppCompatStateInternal(activity, state, compatStateInfo); } /** @@ -1416,18 +1420,61 @@ class ActivityMetricsLogger { } } if (activityToLog != null && stateToLog != APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE) { - logAppCompatStateInternal(activityToLog, stateToLog, packageUid, compatStateInfo); + logAppCompatStateInternal(activityToLog, stateToLog, compatStateInfo); } } + private static boolean isAppCompateStateChangedToLetterboxed(int state) { + return state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO + || state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION + || state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE; + } + private void logAppCompatStateInternal(@NonNull ActivityRecord activity, int state, - int packageUid, PackageCompatStateInfo compatStateInfo) { + PackageCompatStateInfo compatStateInfo) { compatStateInfo.mLastLoggedState = state; compatStateInfo.mLastLoggedActivity = activity; - FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED, packageUid, state); + int packageUid = activity.info.applicationInfo.uid; + + int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION; + if (isAppCompateStateChangedToLetterboxed(state)) { + positionToLog = activity.mLetterboxUiController.getLetterboxPositionForLogging(); + } + FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED, + packageUid, state, positionToLog); + + if (DEBUG_METRICS) { + Slog.i(TAG, String.format("APP_COMPAT_STATE_CHANGED(%s, %s, %s)", + packageUid, state, positionToLog)); + } + } + + /** + * Logs the changing of the letterbox position along with its package UID + */ + void logLetterboxPositionChange(@NonNull ActivityRecord activity, int position) { + int packageUid = activity.info.applicationInfo.uid; + FrameworkStatsLog.write(FrameworkStatsLog.LETTERBOX_POSITION_CHANGED, packageUid, position); + + if (!mPackageUidToCompatStateInfo.contains(packageUid)) { + // There is no last logged activity for this packageUid so we should not log the + // position change as we can only log the position change for the current activity + return; + } + final PackageCompatStateInfo compatStateInfo = mPackageUidToCompatStateInfo.get(packageUid); + final ActivityRecord lastLoggedActivity = compatStateInfo.mLastLoggedActivity; + if (activity != lastLoggedActivity) { + // Only log the position change for the current activity to be consistent with + // findAppCompatStateToLog and ensure that metrics for the state changes are computed + // correctly + return; + } + int state = activity.getAppCompatState(); + logAppCompatStateInternal(activity, state, compatStateInfo); if (DEBUG_METRICS) { - Slog.i(TAG, String.format("APP_COMPAT_STATE_CHANGED(%s, %s)", packageUid, state)); + Slog.i(TAG, String.format("LETTERBOX_POSITION_CHANGED(%s, %s)", + packageUid, position)); } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b3b392c149a1..62427e1d8e52 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2467,8 +2467,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (!newTask && taskSwitch && processRunning && !activityCreated && task.intent != null && mActivityComponent.equals(task.intent.getComponent())) { final ActivityRecord topAttached = task.getActivity(ActivityRecord::attachedToProcess); - if (topAttached != null && topAttached.isSnapshotCompatible(snapshot)) { - return STARTING_WINDOW_TYPE_SNAPSHOT; + if (topAttached != null) { + if (topAttached.isSnapshotCompatible(snapshot) + // This trampoline must be the same rotation. + && mDisplayContent.getDisplayRotation().rotationForOrientation(mOrientation, + mDisplayContent.getRotation()) == snapshot.getRotation()) { + return STARTING_WINDOW_TYPE_SNAPSHOT; + } + // No usable snapshot. And a splash screen may also be weird because an existing + // activity may be shown right after the trampoline is finished. + return STARTING_WINDOW_TYPE_NONE; } } final boolean isActivityHome = isActivityTypeHome(); @@ -3207,12 +3215,29 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return false; } - if (mRootWindowContainer.getTopResumedActivity() == this - && getDisplayContent().mFocusedApp == this) { - ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: already on top, " - + "activity=%s", this); - return !isState(RESUMED); + // If this activity already positions on the top focused task, moving the task to front + // is not needed. But we still need to ensure this activity is focused because the + // current focused activity could be another activity in the same Task if activities are + // displayed on adjacent TaskFragments. + final ActivityRecord currentFocusedApp = mDisplayContent.mFocusedApp; + if (currentFocusedApp != null && currentFocusedApp.task == task) { + final Task topFocusableTask = mDisplayContent.getTask( + (t) -> t.isLeafTask() && t.isFocusable(), true /* traverseTopToBottom */); + if (task == topFocusableTask) { + if (currentFocusedApp == this) { + ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: already on top " + + "and focused, activity=%s", this); + } else { + ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: set focused, " + + "activity=%s", this); + mDisplayContent.setFocusedApp(this); + mAtmService.mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, + true /* updateInputWindows */); + } + return !isState(RESUMED); + } } + ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: activity=%s", this); rootTask.moveToFront(reason, task); @@ -7788,11 +7813,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A newParentConfiguration.windowConfiguration.getWindowingMode(); final boolean isFixedOrientationLetterboxAllowed = parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW - || parentWindowingMode == WINDOWING_MODE_FULLSCREEN; + || parentWindowingMode == WINDOWING_MODE_FULLSCREEN + // Switching from PiP to fullscreen. + || (parentWindowingMode == WINDOWING_MODE_PINNED + && resolvedConfig.windowConfiguration.getWindowingMode() + == WINDOWING_MODE_FULLSCREEN); // TODO(b/181207944): Consider removing the if condition and always run // resolveFixedOrientationConfiguration() since this should be applied for all cases. if (isFixedOrientationLetterboxAllowed) { - resolveFixedOrientationConfiguration(newParentConfiguration, parentWindowingMode); + resolveFixedOrientationConfiguration(newParentConfiguration); } if (mCompatDisplayInsets != null) { @@ -8084,8 +8113,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * <p>If letterboxed due to fixed orientation then aspect ratio restrictions are also applied * in this method. */ - private void resolveFixedOrientationConfiguration(@NonNull Configuration newParentConfig, - int windowingMode) { + private void resolveFixedOrientationConfiguration(@NonNull Configuration newParentConfig) { mLetterboxBoundsForFixedOrientationAndAspectRatio = null; mIsEligibleForFixedOrientationLetterbox = false; final Rect parentBounds = newParentConfig.windowConfiguration.getBounds(); @@ -8105,11 +8133,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (organizedTf != null && !organizedTf.fillsParent()) { return; } - if (windowingMode == WINDOWING_MODE_PINNED) { - // PiP bounds have higher priority than the requested orientation. Otherwise the - // activity may be squeezed into a small piece. - return; - } final Rect resolvedBounds = getResolvedOverrideConfiguration().windowConfiguration.getBounds(); @@ -8174,7 +8197,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A resolvedBounds.set(containingBounds); final float letterboxAspectRatioOverride = - mWmService.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(); + mLetterboxUiController.getFixedOrientationLetterboxAspectRatio(); final float desiredAspectRatio = letterboxAspectRatioOverride > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO ? letterboxAspectRatioOverride : computeAspectRatio(parentBounds); @@ -8727,18 +8750,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * Returns the min aspect ratio of this activity. */ private float getMinAspectRatio() { - float infoAspectRatio = info.getMinAspectRatio(getRequestedOrientation()); - // Complying with the CDD 7.1.1.2 requirement for unresizble apps: - // https://source.android.com/compatibility/12/android-12-cdd#7112_screen_aspect_ratio - return infoAspectRatio < 1f && info.resizeMode == RESIZE_MODE_UNRESIZEABLE - // TODO(233582832): Consider removing fixed-orientation condition. - // Some apps switching from tablet to phone layout at the certain size - // threshold. This may lead to flickering on tablets in landscape orientation - // if an app sets orientation to portrait dynamically because of aspect ratio - // restriction applied here. - && getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED - ? mLetterboxUiController.getDefaultMinAspectRatioForUnresizableApps() - : infoAspectRatio; + return info.getMinAspectRatio(getRequestedOrientation()); } /** diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index fc412cbdad72..a7c09a4f9739 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -79,6 +79,10 @@ import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY; import static com.android.server.wm.Task.REPARENT_MOVE_ROOT_TASK_TO_FRONT; +import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; +import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION; +import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_NEW_TASK; +import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_UNTRUSTED_HOST; import static com.android.server.wm.WindowContainer.POSITION_TOP; import android.annotation.NonNull; @@ -132,6 +136,7 @@ import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.uri.NeededUriGrants; import com.android.server.wm.ActivityMetricsLogger.LaunchingState; import com.android.server.wm.LaunchParamsController.LaunchParams; +import com.android.server.wm.TaskFragment.EmbeddingCheckResult; import java.io.PrintWriter; import java.text.DateFormat; @@ -2074,24 +2079,6 @@ class ActivityStarter { } } - if (mInTaskFragment != null && !canEmbedActivity(mInTaskFragment, r, newTask, targetTask)) { - final StringBuilder errorMsg = new StringBuilder("Permission denied: Cannot embed " + r - + " to " + mInTaskFragment.getTask() + ". newTask=" + newTask + ", targetTask= " - + targetTask); - if (newTask && isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, - LAUNCH_SINGLE_INSTANCE_PER_TASK, LAUNCH_SINGLE_TASK)) { - errorMsg.append("\nActivity tries to launch on a new task because the launch mode" - + " is " + launchModeToString(mLaunchMode)); - } else if (newTask && (mLaunchFlags & (FLAG_ACTIVITY_NEW_DOCUMENT - | FLAG_ACTIVITY_NEW_TASK)) != 0) { - errorMsg.append("\nActivity tries to launch on a new task because the launch flags" - + " contains FLAG_ACTIVITY_NEW_DOCUMENT or FLAG_ACTIVITY_NEW_TASK. " - + "mLaunchFlag=" + mLaunchFlags); - } - Slog.e(TAG, errorMsg.toString()); - return START_PERMISSION_DENIED; - } - // Do not start the activity if target display's DWPC does not allow it. // We can't return fatal error code here because it will crash the caller of // startActivity() if they don't catch the exception. We don't expect 3P apps to make @@ -2118,19 +2105,21 @@ class ActivityStarter { } /** - * Return {@code true} if an activity can be embedded to the TaskFragment. + * Returns whether embedding of {@code starting} is allowed. + * * @param taskFragment the TaskFragment for embedding. * @param starting the starting activity. - * @param newTask whether the starting activity is going to be launched on a new task. * @param targetTask the target task for launching activity, which could be different from * the one who hosting the embedding. */ - private boolean canEmbedActivity(@NonNull TaskFragment taskFragment, - @NonNull ActivityRecord starting, boolean newTask, Task targetTask) { + @VisibleForTesting + @EmbeddingCheckResult + static int canEmbedActivity(@NonNull TaskFragment taskFragment, + @NonNull ActivityRecord starting, @NonNull Task targetTask) { final Task hostTask = taskFragment.getTask(); // Not allowed embedding a separate task or without host task. - if (hostTask == null || newTask || targetTask != hostTask) { - return false; + if (hostTask == null || targetTask != hostTask) { + return EMBEDDING_DISALLOWED_NEW_TASK; } return taskFragment.isAllowedToEmbedActivity(starting); @@ -2576,6 +2565,7 @@ class ActivityStarter { mInTask = null; } mInTaskFragment = inTaskFragment; + sendNewTaskFragmentResultRequestIfNeeded(); mStartFlags = startFlags; // If the onlyIfNeeded flag is set, then we can do this if the activity being launched @@ -2618,6 +2608,18 @@ class ActivityStarter { } } + private void sendNewTaskFragmentResultRequestIfNeeded() { + if (mStartActivity.resultTo != null && mInTaskFragment != null + && mInTaskFragment != mStartActivity.resultTo.getTaskFragment()) { + Slog.w(TAG, + "Activity is launching as a new TaskFragment, so cancelling activity result."); + mStartActivity.resultTo.sendResult(INVALID_UID, mStartActivity.resultWho, + mStartActivity.requestCode, RESULT_CANCELED, + null /* data */, null /* dataGrants */); + mStartActivity.resultTo = null; + } + } + private void computeLaunchingTaskFlags() { // If the caller is not coming from another activity, but has given us an explicit task into // which they would like us to launch the new activity, then let's see about doing that. @@ -2957,19 +2959,16 @@ class ActivityStarter { mIntentDelivered = true; } + /** Places {@link #mStartActivity} in {@code task} or an embedded {@link TaskFragment}. */ private void addOrReparentStartingActivity(@NonNull Task task, String reason) { TaskFragment newParent = task; if (mInTaskFragment != null) { - // TODO(b/234351413): remove remaining embedded Task logic. - // mInTaskFragment is created and added to the leaf task by task fragment organizer's - // request. If the task was resolved and different than mInTaskFragment, reparent the - // task to mInTaskFragment for embedding. - if (mInTaskFragment.getTask() != task) { - if (shouldReparentInTaskFragment(task)) { - task.reparent(mInTaskFragment, POSITION_TOP); - } - } else { + int embeddingCheckResult = canEmbedActivity(mInTaskFragment, mStartActivity, task); + if (embeddingCheckResult == EMBEDDING_ALLOWED) { newParent = mInTaskFragment; + } else { + // Start mStartActivity to task instead if it can't be embedded to mInTaskFragment. + sendCanNotEmbedActivityError(mInTaskFragment, embeddingCheckResult); } } else { TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null; @@ -2981,20 +2980,12 @@ class ActivityStarter { } } if (candidateTf != null && candidateTf.isEmbedded() - && canEmbedActivity(candidateTf, mStartActivity, false /* newTask */, task)) { + && canEmbedActivity(candidateTf, mStartActivity, task) == EMBEDDING_ALLOWED) { // Use the embedded TaskFragment of the top activity as the new parent if the // activity can be embedded. newParent = candidateTf; } } - // Start Activity to the Task if mStartActivity's min dimensions are not satisfied. - if (newParent.isEmbedded() && newParent.smallerThanMinDimension(mStartActivity)) { - reason += " - MinimumDimensionViolation"; - mService.mWindowOrganizerController.sendMinimumDimensionViolation( - newParent, mStartActivity.getMinDimensions(), mRequest.errorCallbackToken, - reason); - newParent = task; - } if (mStartActivity.getTaskFragment() == null || mStartActivity.getTaskFragment() == newParent) { newParent.addChild(mStartActivity, POSITION_TOP); @@ -3003,16 +2994,41 @@ class ActivityStarter { } } - private boolean shouldReparentInTaskFragment(Task task) { - // The task has not been embedded. We should reparent the task to TaskFragment. - if (!task.isEmbedded()) { - return true; + /** + * Notifies the client side that {@link #mStartActivity} cannot be embedded to + * {@code taskFragment}. + */ + private void sendCanNotEmbedActivityError(TaskFragment taskFragment, + @EmbeddingCheckResult int result) { + final String errMsg; + switch(result) { + case EMBEDDING_DISALLOWED_NEW_TASK: { + errMsg = "Cannot embed " + mStartActivity + " that launched on another task" + + ",mLaunchMode=" + launchModeToString(mLaunchMode) + + ",mLaunchFlag=" + Integer.toHexString(mLaunchFlags); + break; + } + case EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION: { + errMsg = "Cannot embed " + mStartActivity + + ". TaskFragment's bounds:" + taskFragment.getBounds() + + ", minimum dimensions:" + mStartActivity.getMinDimensions(); + break; + } + case EMBEDDING_DISALLOWED_UNTRUSTED_HOST: { + errMsg = "The app:" + mCallingUid + "is not trusted to " + mStartActivity; + break; + } + default: + errMsg = "Unhandled embed result:" + result; + } + if (taskFragment.isOrganized()) { + mService.mWindowOrganizerController.sendTaskFragmentOperationFailure( + taskFragment.getTaskFragmentOrganizer(), mRequest.errorCallbackToken, + new SecurityException(errMsg)); + } else { + // If the taskFragment is not organized, just dump error message as warning logs. + Slog.w(TAG, errMsg); } - WindowContainer<?> parent = task.getParent(); - // If the Activity is going to launch on top of embedded Task in the same TaskFragment, - // we don't need to reparent the Task. Otherwise, the embedded Task should reparent to - // another TaskFragment. - return parent.asTaskFragment() != mInTaskFragment; } private int adjustLaunchFlagsToDocumentMode(ActivityRecord r, boolean launchSingleInstance, diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 3d661222d226..fb9d7e602210 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -116,7 +116,6 @@ public class AppTransitionController { private final DisplayContent mDisplayContent; private final WallpaperController mWallpaperControllerLocked; private RemoteAnimationDefinition mRemoteAnimationDefinition = null; - private static final int KEYGUARD_GOING_AWAY_ANIMATION_DURATION = 400; private static final int TYPE_NONE = 0; private static final int TYPE_ACTIVITY = 1; @@ -727,14 +726,17 @@ public class AppTransitionController { */ private void overrideWithRemoteAnimationIfSet(@Nullable ActivityRecord animLpActivity, @TransitionOldType int transit, ArraySet<Integer> activityTypes) { + RemoteAnimationAdapter adapter = null; if (transit == TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE) { // The crash transition has higher priority than any involved remote animations. - return; - } - final RemoteAnimationAdapter adapter = - getRemoteAnimationOverride(animLpActivity, transit, activityTypes); - if (adapter != null - && mDisplayContent.mAppTransition.getRemoteAnimationController() == null) { + } else if (AppTransition.isKeyguardGoingAwayTransitOld(transit)) { + adapter = mRemoteAnimationDefinition != null + ? mRemoteAnimationDefinition.getAdapter(transit, activityTypes) + : null; + } else if (mDisplayContent.mAppTransition.getRemoteAnimationController() == null) { + adapter = getRemoteAnimationOverride(animLpActivity, transit, activityTypes); + } + if (adapter != null) { mDisplayContent.mAppTransition.overridePendingAppTransitionRemote(adapter); } } diff --git a/services/core/java/com/android/server/wm/ContentRecordingController.java b/services/core/java/com/android/server/wm/ContentRecordingController.java index fca4942d4b79..fff7637acc7e 100644 --- a/services/core/java/com/android/server/wm/ContentRecordingController.java +++ b/services/core/java/com/android/server/wm/ContentRecordingController.java @@ -63,6 +63,7 @@ final class ContentRecordingController { */ void setContentRecordingSessionLocked(@Nullable ContentRecordingSession incomingSession, @NonNull WindowManagerService wmService) { + // TODO(b/219761722) handle a null session arriving due to task setup failing if (incomingSession != null && (!ContentRecordingSession.isValid(incomingSession) || ContentRecordingSession.isSameDisplay(mSession, incomingSession))) { // Ignore an invalid session, or a session for the same display as currently recording. diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index ad3b8ee119d3..288777bdb324 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1610,7 +1610,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (mTransitionController.useShellTransitionsRotation()) { return ROTATION_UNDEFINED; } - if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM) { + if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM + || getIgnoreOrientationRequest()) { return ROTATION_UNDEFINED; } if (r.mOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) { @@ -2941,9 +2942,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Set some sort of reasonable bounds on the size of the display that we will try // to emulate. final int minSize = 200; - final int maxScale = 2; - width = Math.min(Math.max(width, minSize), mInitialDisplayWidth * maxScale); - height = Math.min(Math.max(height, minSize), mInitialDisplayHeight * maxScale); + final int maxScale = 3; + final int maxSize = Math.max(mInitialDisplayWidth, mInitialDisplayHeight) * maxScale; + width = Math.min(Math.max(width, minSize), maxSize); + height = Math.min(Math.max(height, minSize), maxSize); } Slog.i(TAG_WM, "Using new display size: " + width + "x" + height); @@ -6193,6 +6195,14 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp .getKeyguardController().isAodShowing(mDisplayId); } + /** + * @return whether the keyguard is occluded on this display + */ + boolean isKeyguardOccluded() { + return mRootWindowContainer.mTaskSupervisor + .getKeyguardController().isDisplayOccluded(mDisplayId); + } + @VisibleForTesting void removeAllTasks() { forAllTasks((t) -> { t.getRootTask().removeChild(t, "removeAllTasks"); }); diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 5c1fc653586e..cff8b93ac947 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1212,7 +1212,8 @@ public class DisplayPolicy { break; default: if (attrs.providedInsets != null) { - for (InsetsFrameProvider provider : attrs.providedInsets) { + for (int i = attrs.providedInsets.length - 1; i >= 0; i--) { + final InsetsFrameProvider provider = attrs.providedInsets[i]; switch (provider.type) { case ITYPE_STATUS_BAR: mStatusBarAlt = win; @@ -1231,21 +1232,29 @@ public class DisplayPolicy { mExtraNavBarAltPosition = getAltBarPosition(attrs); break; } + // The index of the provider and corresponding insets types cannot change at + // runtime as ensured in WMS. Make use of the index in the provider directly + // to access the latest provided size at runtime. + final int index = i; final TriConsumer<DisplayFrames, WindowContainer, Rect> frameProvider = provider.insetsSize != null ? (displayFrames, windowContainer, inOutFrame) -> { inOutFrame.inset(win.mGivenContentInsets); + final InsetsFrameProvider ifp = + win.mAttrs.forRotation(displayFrames.mRotation) + .providedInsets[index]; calculateInsetsFrame(displayFrames, windowContainer, - inOutFrame, provider.source, - provider.insetsSize); + inOutFrame, ifp.source, ifp.insetsSize); } : null; final TriConsumer<DisplayFrames, WindowContainer, Rect> imeFrameProvider = provider.imeInsetsSize != null ? (displayFrames, windowContainer, inOutFrame) -> { inOutFrame.inset(win.mGivenContentInsets); + final InsetsFrameProvider ifp = + win.mAttrs.forRotation(displayFrames.mRotation) + .providedInsets[index]; calculateInsetsFrame(displayFrames, windowContainer, - inOutFrame, provider.source, - provider.imeInsetsSize); + inOutFrame, ifp.source, ifp.imeInsetsSize); } : null; mDisplayContent.setInsetProvider(provider.type, win, frameProvider, imeFrameProvider); @@ -1256,14 +1265,14 @@ public class DisplayPolicy { } } - private void calculateInsetsFrame(DisplayFrames df, WindowContainer coutainer, Rect inOutFrame, + private void calculateInsetsFrame(DisplayFrames df, WindowContainer container, Rect inOutFrame, int source, Insets insetsSize) { if (source == InsetsFrameProvider.SOURCE_DISPLAY) { inOutFrame.set(df.mUnrestricted); } else if (source == InsetsFrameProvider.SOURCE_CONTAINER_BOUNDS) { - inOutFrame.set(coutainer.getBounds()); + inOutFrame.set(container.getBounds()); } - if (insetsSize == null || insetsSize.equals(Insets.NONE)) { + if (insetsSize == null) { return; } // Only one side of the provider shall be applied. Check in the order of left - top - @@ -1276,6 +1285,8 @@ public class DisplayPolicy { inOutFrame.left = inOutFrame.right - insetsSize.right; } else if (insetsSize.bottom != 0) { inOutFrame.top = inOutFrame.bottom - insetsSize.bottom; + } else { + inOutFrame.setEmpty(); } } @@ -1523,13 +1534,14 @@ public class DisplayPolicy { */ void simulateLayoutDisplay(DisplayFrames displayFrames) { final InsetsStateController controller = mDisplayContent.getInsetsStateController(); + sTmpClientFrames.attachedFrame = null; for (int i = mInsetsSourceWindowsExceptIme.size() - 1; i >= 0; i--) { final WindowState win = mInsetsSourceWindowsExceptIme.valueAt(i); mWindowLayout.computeFrames(win.mAttrs.forRotation(displayFrames.mRotation), displayFrames.mInsetsState, displayFrames.mDisplayCutoutSafe, displayFrames.mUnrestricted, win.getWindowingMode(), UNSPECIFIED_LENGTH, - UNSPECIFIED_LENGTH, win.getRequestedVisibilities(), - null /* attachedWindowFrame */, win.mGlobalScale, sTmpClientFrames); + UNSPECIFIED_LENGTH, win.getRequestedVisibilities(), win.mGlobalScale, + sTmpClientFrames); final SparseArray<InsetsSource> sources = win.getProvidedInsetsSources(); final InsetsState state = displayFrames.mInsetsState; for (int index = sources.size() - 1; index >= 0; index--) { @@ -1541,13 +1553,14 @@ public class DisplayPolicy { } void updateInsetsSourceFramesExceptIme(DisplayFrames displayFrames) { + sTmpClientFrames.attachedFrame = null; for (int i = mInsetsSourceWindowsExceptIme.size() - 1; i >= 0; i--) { final WindowState win = mInsetsSourceWindowsExceptIme.valueAt(i); mWindowLayout.computeFrames(win.mAttrs.forRotation(displayFrames.mRotation), displayFrames.mInsetsState, displayFrames.mDisplayCutoutSafe, displayFrames.mUnrestricted, win.getWindowingMode(), UNSPECIFIED_LENGTH, - UNSPECIFIED_LENGTH, win.getRequestedVisibilities(), - null /* attachedWindowFrame */, win.mGlobalScale, sTmpClientFrames); + UNSPECIFIED_LENGTH, win.getRequestedVisibilities(), win.mGlobalScale, + sTmpClientFrames); win.updateSourceFrame(sTmpClientFrames.frame); } } @@ -1577,7 +1590,7 @@ public class DisplayPolicy { displayFrames = win.getDisplayFrames(displayFrames); final WindowManager.LayoutParams attrs = win.mAttrs.forRotation(displayFrames.mRotation); - final Rect attachedWindowFrame = attached != null ? attached.getFrame() : null; + sTmpClientFrames.attachedFrame = attached != null ? attached.getFrame() : null; // If this window has different LayoutParams for rotations, we cannot trust its requested // size. Because it might have not sent its requested size for the new rotation. @@ -1587,8 +1600,7 @@ public class DisplayPolicy { mWindowLayout.computeFrames(attrs, win.getInsetsState(), displayFrames.mDisplayCutoutSafe, win.getBounds(), win.getWindowingMode(), requestedWidth, requestedHeight, - win.getRequestedVisibilities(), attachedWindowFrame, win.mGlobalScale, - sTmpClientFrames); + win.getRequestedVisibilities(), win.mGlobalScale, sTmpClientFrames); win.setFrames(sTmpClientFrames, win.mRequestedWidth, win.mRequestedHeight); } diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index d2c71f57e701..b9d83198139d 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -620,7 +620,8 @@ public class DisplayRotation { // We only enable seamless rotation if the top window has requested it and is in the // fullscreen opaque state. Seamless rotation requires freezing various Surface states and // won't work well with animations, so we disable it in the animation case for now. - if (w.getAttrs().rotationAnimation != ROTATION_ANIMATION_SEAMLESS || w.isAnimatingLw()) { + if (w.getAttrs().rotationAnimation != ROTATION_ANIMATION_SEAMLESS || w.inMultiWindowMode() + || w.isAnimatingLw()) { return false; } diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index 2d227b66b3ce..91b2fb63a543 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -37,11 +37,6 @@ final class LetterboxConfiguration { */ static final float MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO = 1.0f; - // Min allowed aspect ratio for unresizable apps which is used when an app doesn't specify - // android:minAspectRatio in accordance with the CDD 7.1.1.2 requirement: - // https://source.android.com/compatibility/12/android-12-cdd#7112_screen_aspect_ratio - static final float MIN_UNRESIZABLE_ASPECT_RATIO = 4 / 3f; - /** Enum for Letterbox background type. */ @Retention(RetentionPolicy.SOURCE) @IntDef({LETTERBOX_BACKGROUND_SOLID_COLOR, LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND, @@ -109,9 +104,7 @@ final class LetterboxConfiguration { // MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO will be ignored. private float mFixedOrientationLetterboxAspectRatio; - // Default min aspect ratio for unresizable apps which is used when an app doesn't specify - // android:minAspectRatio in accordance with the CDD 7.1.1.2 requirement: - // https://source.android.com/compatibility/12/android-12-cdd#7112_screen_aspect_ratio + // Default min aspect ratio for unresizable apps that are eligible for the size compat mode. private float mDefaultMinAspectRatioForUnresizableApps; // Corners radius for activities presented in the letterbox mode, values < 0 will be ignored. @@ -250,13 +243,7 @@ final class LetterboxConfiguration { } /** - * Resets the min aspect ratio for unresizable apps which is used when an app doesn't specify - * {@code android:minAspectRatio} to {@link - * R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps}. - * - * @throws AssertionError if {@link - * R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps} is < {@link - * #MIN_UNRESIZABLE_ASPECT_RATIO}. + * Resets the min aspect ratio for unresizable apps that are eligible for size compat mode. */ void resetDefaultMinAspectRatioForUnresizableApps() { setDefaultMinAspectRatioForUnresizableApps(mContext.getResources().getFloat( @@ -264,25 +251,16 @@ final class LetterboxConfiguration { } /** - * Gets the min aspect ratio for unresizable apps which is used when an app doesn't specify - * {@code android:minAspectRatio}. + * Gets the min aspect ratio for unresizable apps that are eligible for size compat mode. */ float getDefaultMinAspectRatioForUnresizableApps() { return mDefaultMinAspectRatioForUnresizableApps; } /** - * Overrides the min aspect ratio for unresizable apps which is used when an app doesn't - * specify {@code android:minAspectRatio}. - * - * @throws AssertionError if given value is < {@link #MIN_UNRESIZABLE_ASPECT_RATIO}. + * Overrides the min aspect ratio for unresizable apps that are eligible for size compat mode. */ void setDefaultMinAspectRatioForUnresizableApps(float aspectRatio) { - if (aspectRatio < MIN_UNRESIZABLE_ASPECT_RATIO) { - throw new AssertionError( - "Unexpected min aspect ratio for unresizable apps, it should be <= " - + MIN_UNRESIZABLE_ASPECT_RATIO + " but was " + aspectRatio); - } mDefaultMinAspectRatioForUnresizableApps = aspectRatio; } @@ -709,6 +687,24 @@ final class LetterboxConfiguration { } } + /* + * Gets the horizontal position of the letterboxed app window when horizontal reachability is + * enabled. + */ + @LetterboxHorizontalReachabilityPosition + int getLetterboxPositionForHorizontalReachability() { + return mLetterboxPositionForHorizontalReachability; + } + + /* + * Gets the vertical position of the letterboxed app window when vertical reachability is + * enabled. + */ + @LetterboxVerticalReachabilityPosition + int getLetterboxPositionForVerticalReachability() { + return mLetterboxPositionForVerticalReachability; + } + /** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */ static String letterboxHorizontalReachabilityPositionToString( @LetterboxHorizontalReachabilityPosition int position) { diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index df9a87ea1ab0..d65276793700 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -21,6 +21,20 @@ import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER; import static com.android.server.wm.ActivityRecord.computeAspectRatio; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -28,6 +42,13 @@ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; +import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString; import android.annotation.Nullable; @@ -211,10 +232,19 @@ final class LetterboxUiController { : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(); } - float getDefaultMinAspectRatioForUnresizableApps() { + float getFixedOrientationLetterboxAspectRatio() { + return mActivityRecord.shouldCreateCompatDisplayInsets() + ? getDefaultMinAspectRatioForUnresizableApps() + : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(); + } + + private float getDefaultMinAspectRatioForUnresizableApps() { if (!mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled() || mActivityRecord.getDisplayContent() == null) { - return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps(); + return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps() + > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO + ? mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps() + : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(); } int dividerWindowWidth = @@ -226,10 +256,10 @@ final class LetterboxUiController { // Getting the same aspect ratio that apps get in split screen. Rect bounds = new Rect(mActivityRecord.getDisplayContent().getBounds()); if (bounds.width() >= bounds.height()) { - bounds.inset(/* dx */ dividerSize, /* dy */ 0); + bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0); bounds.right = bounds.centerX(); } else { - bounds.inset(/* dx */ 0, /* dy */ dividerSize); + bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2); bounds.bottom = bounds.centerY(); } return computeAspectRatio(bounds); @@ -249,12 +279,26 @@ final class LetterboxUiController { return; } + int letterboxPositionForHorizontalReachability = mLetterboxConfiguration + .getLetterboxPositionForHorizontalReachability(); if (mLetterbox.getInnerFrame().left > x) { // Moving to the next stop on the left side of the app window: right > center > left. mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(); + int changeToLog = + letterboxPositionForHorizontalReachability + == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER + ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT + : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER; + logLetterboxPositionChange(changeToLog); } else if (mLetterbox.getInnerFrame().right < x) { // Moving to the next stop on the right side of the app window: left > center > right. mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(); + int changeToLog = + letterboxPositionForHorizontalReachability + == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER + ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT + : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER; + logLetterboxPositionChange(changeToLog); } // TODO(197549949): Add animation for transition. @@ -270,13 +314,26 @@ final class LetterboxUiController { // Only react to clicks at the top and bottom of the letterboxed app window. return; } - + int letterboxPositionForVerticalReachability = mLetterboxConfiguration + .getLetterboxPositionForVerticalReachability(); if (mLetterbox.getInnerFrame().top > y) { // Moving to the next stop on the top side of the app window: bottom > center > top. mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop(); + int changeToLog = + letterboxPositionForVerticalReachability + == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER + ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP + : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER; + logLetterboxPositionChange(changeToLog); } else if (mLetterbox.getInnerFrame().bottom < y) { // Moving to the next stop on the bottom side of the app window: top > center > bottom. mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(); + int changeToLog = + letterboxPositionForVerticalReachability + == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER + ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM + : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER; + logLetterboxPositionChange(changeToLog); } // TODO(197549949): Add animation for transition. @@ -567,4 +624,63 @@ final class LetterboxUiController { return "UNKNOWN_REASON"; } + private int letterboxHorizontalReachabilityPositionToLetterboxPosition( + @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition int position) { + switch (position) { + case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT; + case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER; + case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT; + default: + throw new AssertionError( + "Unexpected letterbox horizontal reachability position type: " + + position); + } + } + + private int letterboxVerticalReachabilityPositionToLetterboxPosition( + @LetterboxConfiguration.LetterboxVerticalReachabilityPosition int position) { + switch (position) { + case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP; + case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER; + case LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM; + default: + throw new AssertionError( + "Unexpected letterbox vertical reachability position type: " + + position); + } + } + + int getLetterboxPositionForLogging() { + int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION; + if (isHorizontalReachabilityEnabled()) { + int letterboxPositionForHorizontalReachability = getLetterboxConfiguration() + .getLetterboxPositionForHorizontalReachability(); + positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition( + letterboxPositionForHorizontalReachability); + } else if (isVerticalReachabilityEnabled()) { + int letterboxPositionForVerticalReachability = getLetterboxConfiguration() + .getLetterboxPositionForVerticalReachability(); + positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition( + letterboxPositionForVerticalReachability); + } + return positionToLog; + } + + private LetterboxConfiguration getLetterboxConfiguration() { + return mLetterboxConfiguration; + } + + /** + * Logs letterbox position changes via {@link ActivityMetricsLogger#logLetterboxPositionChange}. + */ + private void logLetterboxPositionChange(int letterboxPositionChange) { + mActivityRecord.mTaskSupervisor.getActivityMetricsLogger() + .logLetterboxPositionChange(mActivityRecord, letterboxPositionChange); + } } diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java index ad158c7b45b9..ac1a2b17603a 100644 --- a/services/core/java/com/android/server/wm/RemoteAnimationController.java +++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java @@ -331,8 +331,10 @@ class RemoteAnimationController implements DeathRecipient { private void invokeAnimationCancelled(String reason) { ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "cancelAnimation(): reason=%s", reason); + final boolean isKeyguardOccluded = mDisplayContent.isKeyguardOccluded(); + try { - mRemoteAnimationAdapter.getRunner().onAnimationCancelled(); + mRemoteAnimationAdapter.getRunner().onAnimationCancelled(isKeyguardOccluded); } catch (RemoteException e) { Slog.e(TAG, "Failed to notify cancel", e); } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index e4337f238498..db730e0cb368 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -1239,11 +1239,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } @Override - void scheduleAnimation() { - mWmService.scheduleAnimationLocked(); - } - - @Override protected void removeChild(DisplayContent dc) { super.removeChild(dc); if (mTopFocusedDisplayId == dc.getDisplayId()) { diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 30b50839cd35..9b013dac6adf 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -115,8 +115,6 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { private float mLastReportedAnimatorScale; private String mPackageName; private String mRelayoutTag; - private String mUpdateViewVisibilityTag; - private String mUpdateWindowLayoutTag; private final InsetsVisibilities mDummyRequestedVisibilities = new InsetsVisibilities(); private final InsetsSourceControl[] mDummyControls = new InsetsSourceControl[0]; final boolean mSetsUnrestrictedKeepClearAreas; @@ -195,27 +193,28 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities, InputChannel outInputChannel, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { + InsetsSourceControl[] outActiveControls, Rect outAttachedFrame) { return mService.addWindow(this, window, attrs, viewVisibility, displayId, UserHandle.getUserId(mUid), requestedVisibilities, outInputChannel, outInsetsState, - outActiveControls); + outActiveControls, outAttachedFrame); } @Override public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities, InputChannel outInputChannel, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { + InsetsSourceControl[] outActiveControls, Rect outAttachedFrame) { return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId, - requestedVisibilities, outInputChannel, outInsetsState, outActiveControls); + requestedVisibilities, outInputChannel, outInsetsState, outActiveControls, + outAttachedFrame); } @Override public int addToDisplayWithoutInputChannel(IWindow window, WindowManager.LayoutParams attrs, - int viewVisibility, int displayId, InsetsState outInsetsState) { + int viewVisibility, int displayId, InsetsState outInsetsState, Rect outAttachedFrame) { return mService.addWindow(this, window, attrs, viewVisibility, displayId, UserHandle.getUserId(mUid), mDummyRequestedVisibilities, null /* outInputChannel */, - outInsetsState, mDummyControls); + outInsetsState, mDummyControls, outAttachedFrame); } @Override @@ -224,29 +223,13 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } @Override - public int updateVisibility(IWindow client, WindowManager.LayoutParams attrs, - int viewVisibility, MergedConfiguration outMergedConfiguration, - SurfaceControl outSurfaceControl, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mUpdateViewVisibilityTag); - int res = mService.updateViewVisibility(this, client, attrs, viewVisibility, - outMergedConfiguration, outSurfaceControl, outInsetsState, outActiveControls); - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - return res; - } - - @Override - public void updateLayout(IWindow window, WindowManager.LayoutParams attrs, int flags, - ClientWindowFrames clientFrames, int requestedWidth, int requestedHeight) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mUpdateWindowLayoutTag); - mService.updateWindowLayout(this, window, attrs, flags, clientFrames, requestedWidth, - requestedHeight); - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + public void prepareToReplaceWindows(IBinder appToken, boolean childrenOnly) { + mService.setWillReplaceWindows(appToken, childrenOnly); } @Override - public void prepareToReplaceWindows(IBinder appToken, boolean childrenOnly) { - mService.setWillReplaceWindows(appToken, childrenOnly); + public boolean cancelDraw(IWindow window) { + return mService.cancelDraw(this, window); } @Override @@ -711,8 +694,6 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { if (wpc != null) { mPackageName = wpc.mInfo.packageName; mRelayoutTag = "relayoutWindow: " + mPackageName; - mUpdateViewVisibilityTag = "updateVisibility: " + mPackageName; - mUpdateWindowLayoutTag = "updateLayout: " + mPackageName; } else { Slog.e(TAG_WM, "Unknown process pid=" + mPid); } diff --git a/services/core/java/com/android/server/wm/StartingSurfaceController.java b/services/core/java/com/android/server/wm/StartingSurfaceController.java index 68dbb0607ac1..0bb773ae5e41 100644 --- a/services/core/java/com/android/server/wm/StartingSurfaceController.java +++ b/services/core/java/com/android/server/wm/StartingSurfaceController.java @@ -158,14 +158,13 @@ public class StartingSurfaceController { + topFullscreenActivity); return null; } - if (topFullscreenActivity.getWindowConfiguration().getRotation() - != taskSnapshot.getRotation()) { + if (activity.mDisplayContent.getRotation() != taskSnapshot.getRotation()) { // The snapshot should have been checked by ActivityRecord#isSnapshotCompatible // that the activity will be updated to the same rotation as the snapshot. Since // the transition is not started yet, fixed rotation transform needs to be applied // earlier to make the snapshot show in a rotated container. activity.mDisplayContent.handleTopActivityLaunchingInDifferentOrientation( - topFullscreenActivity, false /* checkOpening */); + activity, false /* checkOpening */); } mService.mAtmService.mTaskOrganizerController.addStartingWindow(task, activity, 0 /* launchTheme */, taskSnapshot); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 64905c8fe2f3..3d127742d7bd 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3624,30 +3624,35 @@ class Task extends TaskFragment { } @Override + String toFullString() { + final StringBuilder sb = new StringBuilder(192); + sb.append(this); + sb.setLength(sb.length() - 1); // Remove tail '}'. + sb.append(" U="); + sb.append(mUserId); + final Task rootTask = getRootTask(); + if (rootTask != this) { + sb.append(" rootTaskId="); + sb.append(rootTask.mTaskId); + } + sb.append(" visible="); + sb.append(shouldBeVisible(null /* starting */)); + sb.append(" visibleRequested="); + sb.append(isVisibleRequested()); + sb.append(" mode="); + sb.append(windowingModeToString(getWindowingMode())); + sb.append(" translucent="); + sb.append(isTranslucent(null /* starting */)); + sb.append(" sz="); + sb.append(getChildCount()); + sb.append('}'); + return sb.toString(); + } + + @Override public String toString() { + if (stringName != null) return stringName; StringBuilder sb = new StringBuilder(128); - if (stringName != null) { - sb.append(stringName); - sb.append(" U="); - sb.append(mUserId); - final Task rootTask = getRootTask(); - if (rootTask != this) { - sb.append(" rootTaskId="); - sb.append(rootTask.mTaskId); - } - sb.append(" visible="); - sb.append(shouldBeVisible(null /* starting */)); - sb.append(" visibleRequested="); - sb.append(isVisibleRequested()); - sb.append(" mode="); - sb.append(windowingModeToString(getWindowingMode())); - sb.append(" translucent="); - sb.append(isTranslucent(null /* starting */)); - sb.append(" sz="); - sb.append(getChildCount()); - sb.append('}'); - return sb.toString(); - } sb.append("Task{"); sb.append(Integer.toHexString(System.identityHashCode(this))); sb.append(" #"); @@ -3662,11 +3667,9 @@ class Task extends TaskFragment { } else if (affinityIntent != null && affinityIntent.getComponent() != null) { sb.append(" aI="); sb.append(affinityIntent.getComponent().flattenToShortString()); - } else { - sb.append(" ??"); } - stringName = sb.toString(); - return toString(); + sb.append('}'); + return stringName = sb.toString(); } /** diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 02e796904b16..8220cae74dc8 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -1114,7 +1114,10 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { // If a task is launching from a created-by-organizer task, it should be launched into the // same created-by-organizer task as well. Unless, the candidate task is already positioned // in the another adjacent task. - if (sourceTask != null) { + if (sourceTask != null && (candidateTask == null + // A pinned task relaunching should be handled by its task organizer. Skip fallback + // launch target of a pinned task from source task. + || candidateTask.getWindowingMode() != WINDOWING_MODE_PINNED)) { Task launchTarget = sourceTask.getCreatedByOrganizerTask(); if (launchTarget != null && launchTarget.getAdjacentTaskFragment() != null) { if (candidateTask != null) { @@ -1937,7 +1940,7 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { continue; } final Task rootTask = child.asTask(); - pw.println(doublePrefix + "* " + rootTask); + pw.println(doublePrefix + "* " + rootTask.toFullString()); rootTask.dump(pw, triplePrefix, dumpAll); } } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 391077636654..f8a9d4665acc 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -140,6 +140,45 @@ class TaskFragment extends WindowContainer<WindowContainer> { static final boolean SHOW_APP_STARTING_PREVIEW = true; /** + * An embedding check result of {@link #isAllowedToEmbedActivity(ActivityRecord)} or + * {@link ActivityStarter#canEmbedActivity(TaskFragment, ActivityRecord, Task)}: + * indicate that an Activity can be embedded successfully. + */ + static final int EMBEDDING_ALLOWED = 0; + /** + * An embedding check result of {@link #isAllowedToEmbedActivity(ActivityRecord)} or + * {@link ActivityStarter#canEmbedActivity(TaskFragment, ActivityRecord, Task)}: + * indicate that an Activity can't be embedded because either the Activity does not allow + * untrusted embedding, and the embedding host app is not trusted. + */ + static final int EMBEDDING_DISALLOWED_UNTRUSTED_HOST = 1; + /** + * An embedding check result of {@link #isAllowedToEmbedActivity(ActivityRecord)} or + * {@link ActivityStarter#canEmbedActivity(TaskFragment, ActivityRecord, Task)}: + * indicate that an Activity can't be embedded because this taskFragment's bounds are + * {@link #smallerThanMinDimension(ActivityRecord)}. + */ + static final int EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION = 2; + /** + * An embedding check result of + * {@link ActivityStarter#canEmbedActivity(TaskFragment, ActivityRecord, Task)}: + * indicate that an Activity can't be embedded because the Activity is started on a new task. + */ + static final int EMBEDDING_DISALLOWED_NEW_TASK = 3; + + /** + * Embedding check results of {@link #isAllowedToEmbedActivity(ActivityRecord)} or + * {@link ActivityStarter#canEmbedActivity(TaskFragment, ActivityRecord, Task)}. + */ + @IntDef(prefix = {"EMBEDDING_"}, value = { + EMBEDDING_ALLOWED, + EMBEDDING_DISALLOWED_UNTRUSTED_HOST, + EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION, + EMBEDDING_DISALLOWED_NEW_TASK, + }) + @interface EmbeddingCheckResult {} + + /** * Indicate that the minimal width/height should use the default value. * * @see #mMinWidth @@ -509,20 +548,29 @@ class TaskFragment extends WindowContainer<WindowContainer> { return false; } - boolean isAllowedToEmbedActivity(@NonNull ActivityRecord a) { + @EmbeddingCheckResult + int isAllowedToEmbedActivity(@NonNull ActivityRecord a) { return isAllowedToEmbedActivity(a, mTaskFragmentOrganizerUid); } /** * Checks if the organized task fragment is allowed to have the specified activity, which is - * allowed if an activity allows embedding in untrusted mode, or if the trusted mode can be - * enabled. - * @see #isAllowedToEmbedActivityInTrustedMode(ActivityRecord) + * allowed if an activity allows embedding in untrusted mode, if the trusted mode can be + * enabled, or if the organized task fragment bounds are not + * {@link #smallerThanMinDimension(ActivityRecord)}. + * * @param uid uid of the TaskFragment organizer. + * @see #isAllowedToEmbedActivityInTrustedMode(ActivityRecord) */ - boolean isAllowedToEmbedActivity(@NonNull ActivityRecord a, int uid) { - return isAllowedToEmbedActivityInUntrustedMode(a) - || isAllowedToEmbedActivityInTrustedMode(a, uid); + @EmbeddingCheckResult + int isAllowedToEmbedActivity(@NonNull ActivityRecord a, int uid) { + if (!isAllowedToEmbedActivityInUntrustedMode(a) + && !isAllowedToEmbedActivityInTrustedMode(a, uid)) { + return EMBEDDING_DISALLOWED_UNTRUSTED_HOST; + } else if (smallerThanMinDimension(a)) { + return EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION; + } + return EMBEDDING_ALLOWED; } boolean smallerThanMinDimension(@NonNull ActivityRecord activity) { @@ -539,9 +587,8 @@ class TaskFragment extends WindowContainer<WindowContainer> { } final int minWidth = minDimensions.x; final int minHeight = minDimensions.y; - final boolean smaller = taskFragBounds.width() < minWidth + return taskFragBounds.width() < minWidth || taskFragBounds.height() < minHeight; - return smaller; } /** @@ -598,7 +645,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { // The system is trusted to embed other apps securely and for all users. return UserHandle.getAppId(uid) == SYSTEM_UID // Activities from the same UID can be embedded freely by the host. - || uid == a.getUid(); + || a.isUid(uid); } /** @@ -2581,6 +2628,32 @@ class TaskFragment extends WindowContainer<WindowContainer> { return getWindowingMode() == WINDOWING_MODE_FULLSCREEN || matchParentBounds(); } + String toFullString() { + final StringBuilder sb = new StringBuilder(128); + sb.append(this); + sb.setLength(sb.length() - 1); // Remove tail '}'. + if (mTaskFragmentOrganizerUid != INVALID_UID) { + sb.append(" organizerUid="); + sb.append(mTaskFragmentOrganizerUid); + } + if (mTaskFragmentOrganizerProcessName != null) { + sb.append(" organizerProc="); + sb.append(mTaskFragmentOrganizerProcessName); + } + if (mAdjacentTaskFragment != null) { + sb.append(" adjacent="); + sb.append(mAdjacentTaskFragment); + } + sb.append('}'); + return sb.toString(); + } + + @Override + public String toString() { + return "TaskFragment{" + Integer.toHexString(System.identityHashCode(this)) + + " mode=" + WindowConfiguration.windowingModeToString(getWindowingMode()) + "}"; + } + boolean dump(String prefix, FileDescriptor fd, PrintWriter pw, boolean dumpAll, boolean dumpClient, String dumpPackage, final boolean needSep, Runnable header) { boolean printed = false; @@ -2619,7 +2692,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { } void dumpInner(String prefix, PrintWriter pw, boolean dumpAll, String dumpPackage) { - pw.print(prefix); pw.print("* "); pw.println(this); + pw.print(prefix); pw.print("* "); pw.println(toFullString()); final Rect bounds = getRequestedOverrideBounds(); if (!bounds.isEmpty()) { pw.println(prefix + " mBounds=" + bounds); @@ -2640,10 +2713,11 @@ class TaskFragment extends WindowContainer<WindowContainer> { final String doublePrefix = prefix + " "; for (int i = mChildren.size() - 1; i >= 0; i--) { final WindowContainer<?> child = mChildren.get(i); - pw.println(prefix + "* " + child); + final TaskFragment tf = child.asTaskFragment(); + pw.println(prefix + "* " + (tf != null ? tf.toFullString() : child)); // Only dump non-activity because full activity info is already printed by // RootWindowContainer#dumpActivities. - if (child.asActivityRecord() == null) { + if (tf != null) { child.dump(pw, doublePrefix, dumpAll); } } diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 9aff23ddde14..392d4c2f772b 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.window.TaskFragmentOrganizer.putExceptionInBundle; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER; +import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; import static com.android.server.wm.WindowOrganizerController.configurationsAreEqualForOrganizer; import android.annotation.IntDef; @@ -235,7 +236,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr + " is not in a task belong to the organizer app."); return; } - if (!task.isAllowedToEmbedActivity(activity, mOrganizerUid)) { + if (task.isAllowedToEmbedActivity(activity, mOrganizerUid) != EMBEDDING_ALLOWED) { Slog.d(TAG, "Reparent activity=" + activity.token + " is not allowed to be embedded."); return; diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java index 814656db9fa0..534616fb7207 100644 --- a/services/core/java/com/android/server/wm/TaskSnapshotController.java +++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java @@ -47,6 +47,7 @@ import android.graphics.RenderNode; import android.hardware.HardwareBuffer; import android.os.Environment; import android.os.Handler; +import android.os.Trace; import android.util.ArraySet; import android.util.Pair; import android.util.Slog; @@ -391,8 +392,10 @@ class TaskSnapshotController { SurfaceControl.ScreenshotHardwareBuffer createTaskSnapshot(@NonNull Task task, TaskSnapshot.Builder builder) { Point taskSize = new Point(); + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "createTaskSnapshot"); final SurfaceControl.ScreenshotHardwareBuffer taskSnapshot = createTaskSnapshot(task, mHighResTaskSnapshotScale, builder.getPixelFormat(), taskSize, builder); + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); builder.setTaskSize(taskSize); return taskSnapshot; } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 3c0cac0079e8..ae61f24a7a54 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -149,9 +149,6 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe final @TransitionType int mType; private int mSyncId = -1; - // Used for tracking a Transition throughout a lifecycle (i.e. from STATE_COLLECTING to - // STATE_FINISHED or STATE_ABORT), and should only be used for testing and debugging. - private int mDebugId = -1; private @TransitionFlags int mFlags; private final TransitionController mController; private final BLASTSyncEngine mSyncEngine; @@ -295,11 +292,6 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return mSyncId; } - @VisibleForTesting - int getDebugId() { - return mDebugId; - } - @TransitionFlags int getFlags() { return mFlags; @@ -315,6 +307,10 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return mFinishTransaction; } + private boolean isCollecting() { + return mState == STATE_COLLECTING || mState == STATE_STARTED; + } + /** Starts collecting phase. Once this starts, all relevant surface operations are sync. */ void startCollecting(long timeoutMs) { if (mState != STATE_PENDING) { @@ -322,7 +318,6 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } mState = STATE_COLLECTING; mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG); - mDebugId = mSyncId; mController.mTransitionTracer.logState(this); } @@ -353,7 +348,10 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe if (mState < STATE_COLLECTING) { throw new IllegalStateException("Transition hasn't started collecting."); } - if (mSyncId < 0) return; + if (!isCollecting()) { + // Too late, transition already started playing, so don't collect. + return; + } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Collecting in transition %d: %s", mSyncId, wc); // "snapshot" all parents (as potential promotion targets). Do this before checking @@ -403,7 +401,10 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe * or waiting until after the animation to close). */ void collectExistenceChange(@NonNull WindowContainer wc) { - if (mSyncId < 0) return; + if (mState >= STATE_PLAYING) { + // Too late to collect. Don't check too-early here since `collect` will check that. + return; + } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Existence Changed in transition %d:" + " %s", mSyncId, wc); collect(wc); @@ -437,7 +438,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe */ void setOverrideAnimation(TransitionInfo.AnimationOptions options, @Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) { - if (mSyncId < 0) return; + if (!isCollecting()) return; mOverrideOptions = options; sendRemoteCallback(mClientAnimationStartCallback); mClientAnimationStartCallback = startCallback; @@ -455,7 +456,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe * The transition will wait for all groups to be ready. */ void setReady(WindowContainer wc, boolean ready) { - if (mSyncId < 0) return; + if (!isCollecting() || mSyncId < 0) return; mReadyTracker.setReadyFrom(wc, ready); applyReady(); } @@ -473,7 +474,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe * @see ReadyTracker#setAllReady. */ void setAllReady() { - if (mSyncId < 0) return; + if (!isCollecting() || mSyncId < 0) return; mReadyTracker.setAllReady(); applyReady(); } @@ -672,7 +673,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe SurfaceControl.Transaction inputSinkTransaction = null; for (int i = 0; i < mParticipants.size(); ++i) { final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord(); - if (ar == null || !ar.isVisible()) continue; + if (ar == null || !ar.isVisible() || ar.getParent() == null) continue; if (inputSinkTransaction == null) { inputSinkTransaction = new SurfaceControl.Transaction(); } @@ -889,7 +890,6 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe // No player registered, so just finish/apply immediately cleanUpOnFailure(); } - mSyncId = -1; mOverrideOptions = null; reportStartReasonsToLogger(); @@ -1614,7 +1614,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } boolean getLegacyIsReady() { - return (mState == STATE_STARTED || mState == STATE_COLLECTING) && mSyncId >= 0; + return isCollecting() && mSyncId >= 0; } static Transition fromBinder(IBinder binder) { diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java index b1951e038177..c1927d864320 100644 --- a/services/core/java/com/android/server/wm/TransitionTracer.java +++ b/services/core/java/com/android/server/wm/TransitionTracer.java @@ -79,7 +79,7 @@ public class TransitionTracer { final ProtoOutputStream outputStream = new ProtoOutputStream(); final long transitionEntryToken = outputStream.start(TRANSITION); - outputStream.write(ID, transition.getDebugId()); + outputStream.write(ID, transition.getSyncId()); outputStream.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos()); outputStream.write(TRANSITION_TYPE, transition.mType); outputStream.write(STATE, transition.getState()); diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 89c71c909641..33c0fe13af11 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -2694,9 +2694,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * will be applied. */ void scheduleAnimation() { - if (mParent != null) { - mParent.scheduleAnimation(); - } + mWmService.scheduleAnimationLocked(); } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index a48140eb0e04..b5abd3258ad8 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -48,6 +48,7 @@ import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES; import static android.provider.Settings.Global.DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR; import static android.provider.Settings.Global.DEVELOPMENT_WM_DISPLAY_SETTINGS_PATH; +import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; @@ -89,6 +90,7 @@ import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManagerGlobal.ADD_OKAY; +import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; import static android.view.WindowManagerPolicyConstants.NAV_BAR_INVALID; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER; @@ -287,6 +289,7 @@ import android.view.displayhash.VerifiedDisplayHash; import android.window.ClientWindowFrames; import android.window.ITaskFpsCallback; import android.window.TaskSnapshot; +import android.window.WindowContainerToken; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; @@ -1445,7 +1448,7 @@ public class WindowManagerService extends IWindowManager.Stub public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility, int displayId, int requestUserId, InsetsVisibilities requestedVisibilities, InputChannel outInputChannel, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { + InsetsSourceControl[] outActiveControls, Rect outAttachedFrame) { Arrays.fill(outActiveControls, null); int[] appOp = new int[1]; final boolean isRoundedCornerOverlay = (attrs.privateFlags @@ -1860,6 +1863,13 @@ public class WindowManagerService extends IWindowManager.Stub outInsetsState.set(win.getCompatInsetsState(), win.isClientLocal()); getInsetsSourceControls(win, outActiveControls); + + if (win.mLayoutAttached) { + outAttachedFrame.set(win.getParentWindow().getCompatFrame()); + } else { + // Make this invalid which indicates a null attached frame. + outAttachedFrame.set(0, 0, -1, -1); + } } Binder.restoreCallingIdentity(origId); @@ -2212,6 +2222,20 @@ public class WindowManagerService extends IWindowManager.Stub == PackageManager.PERMISSION_GRANTED; } + /** + * Returns whether this window can proceed with drawing or needs to retry later. + */ + public boolean cancelDraw(Session session, IWindow client) { + synchronized (mGlobalLock) { + final WindowState win = windowForClientLocked(session, client, false); + if (win == null) { + return false; + } + + return win.cancelAndRedraw(); + } + } + public int relayoutWindow(Session session, IWindow client, LayoutParams attrs, int requestedWidth, int requestedHeight, int viewVisibility, int flags, ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration, @@ -2228,6 +2252,11 @@ public class WindowManagerService extends IWindowManager.Stub if (win == null) { return 0; } + + if (win.cancelAndRedraw() && win.mPrepareSyncSeqId <= win.mLastSeqIdSentToRelayout) { + result |= RELAYOUT_RES_CANCEL_AND_REDRAW; + } + final DisplayContent displayContent = win.getDisplayContent(); final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy(); @@ -2642,29 +2671,6 @@ public class WindowManagerService extends IWindowManager.Stub return result; } - int updateViewVisibility(Session session, IWindow client, LayoutParams attrs, - int viewVisibility, MergedConfiguration outMergedConfiguration, - SurfaceControl outSurfaceControl, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { - // TODO(b/161810301): Finish the implementation. - return 0; - } - - void updateWindowLayout(Session session, IWindow client, LayoutParams attrs, int flags, - ClientWindowFrames clientWindowFrames, int requestedWidth, int requestedHeight) { - final long origId = Binder.clearCallingIdentity(); - synchronized (mGlobalLock) { - final WindowState win = windowForClientLocked(session, client, false); - if (win == null) { - return; - } - win.setFrames(clientWindowFrames, requestedWidth, requestedHeight); - - // TODO(b/161810301): Finish the implementation. - } - Binder.restoreCallingIdentity(origId); - } - public boolean outOfMemoryWindow(Session session, IWindow client) { final long origId = Binder.clearCallingIdentity(); @@ -6041,9 +6047,7 @@ public class WindowManagerService extends IWindowManager.Stub /** Note that Locked in this case is on mLayoutToAnim */ void scheduleAnimationLocked() { - if (mAnimator != null) { - mAnimator.scheduleAnimation(); - } + mAnimator.scheduleAnimation(); } boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) { @@ -8279,6 +8283,26 @@ public class WindowManagerService extends IWindowManager.Stub @Override public void setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) { synchronized (mGlobalLock) { + // Allow the controller to handle teardown or a non-task session. + if (incomingSession == null + || incomingSession.getContentToRecord() != RECORD_CONTENT_TASK) { + mContentRecordingController.setContentRecordingSessionLocked(incomingSession, + WindowManagerService.this); + return; + } + // For a task session, find the activity identified by the launch cookie. + final WindowContainerToken wct = getTaskWindowContainerTokenForLaunchCookie( + incomingSession.getTokenToRecord()); + if (wct == null) { + Slog.w(TAG, "Handling a new recording session; unable to find the " + + "WindowContainerToken"); + mContentRecordingController.setContentRecordingSessionLocked(null, + WindowManagerService.this); + return; + } + // Replace the launch cookie in the session details with the task's + // WindowContainerToken. + incomingSession.setTokenToRecord(wct.asBinder()); mContentRecordingController.setContentRecordingSessionLocked(incomingSession, WindowManagerService.this); } @@ -8537,6 +8561,38 @@ public class WindowManagerService extends IWindowManager.Stub } /** + * Retrieve the {@link WindowContainerToken} of the task that contains the activity started + * with the given launch cookie. + * + * @param launchCookie the launch cookie set on the {@link ActivityOptions} when starting an + * activity + * @return a token representing the task containing the activity started with the given launch + * cookie, or {@code null} if the token couldn't be found. + */ + @VisibleForTesting + @Nullable + WindowContainerToken getTaskWindowContainerTokenForLaunchCookie(@NonNull IBinder launchCookie) { + // Find the activity identified by the launch cookie. + final ActivityRecord targetActivity = mRoot.getActivity( + activity -> activity.mLaunchCookie == launchCookie); + if (targetActivity == null) { + Slog.w(TAG, "Unable to find the activity for this launch cookie"); + return null; + } + if (targetActivity.getTask() == null) { + Slog.w(TAG, "Unable to find the task for this launch cookie"); + return null; + } + WindowContainerToken taskWindowContainerToken = + targetActivity.getTask().mRemoteToken.toWindowContainerToken(); + if (taskWindowContainerToken == null) { + Slog.w(TAG, "Unable to find the WindowContainerToken for " + targetActivity.getName()); + return null; + } + return taskWindowContainerToken; + } + + /** * You need ALLOW_SLIPPERY_TOUCHES permission to be able to set FLAG_SLIPPERY. */ private int sanitizeFlagSlippery(int flags, String windowName, int callingUid, int callingPid) { diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java index 02f056cd33af..ff43a96d6afc 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -1370,9 +1370,10 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println(" be ignored and framework implementation will determine aspect ratio."); pw.println(" --minAspectRatioForUnresizable aspectRatio"); pw.println(" Default min aspect ratio for unresizable apps which is used when an"); - pw.println(" app doesn't specify android:minAspectRatio. An exception will be"); - pw.println(" thrown if aspectRatio < " - + LetterboxConfiguration.MIN_UNRESIZABLE_ASPECT_RATIO); + pw.println(" app is eligible for the size compat mode. If aspectRatio <= " + + LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); + pw.println(" both it and R.dimen.config_fixedOrientationLetterboxAspectRatio will"); + pw.println(" be ignored and framework implementation will determine aspect ratio."); pw.println(" --cornerRadius radius"); pw.println(" Corners radius for activities in the letterbox mode. If radius < 0,"); pw.println(" both it and R.integer.config_letterboxActivityCornersRadius will be"); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 1d93c8922d21..97dcb7574e3c 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -46,6 +46,7 @@ import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CON import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; +import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; import static com.android.server.wm.WindowContainer.POSITION_BOTTOM; import static com.android.server.wm.WindowContainer.POSITION_TOP; @@ -814,7 +815,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); break; } - if (!parent.isAllowedToEmbedActivity(activity)) { + if (parent.isAllowedToEmbedActivity(activity) != EMBEDDING_ALLOWED) { final Throwable exception = new SecurityException( "The task fragment is not trusted to embed the given activity."); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); @@ -1057,7 +1058,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } /** A helper method to send minimum dimension violation error to the client. */ - void sendMinimumDimensionViolation(TaskFragment taskFragment, Point minDimensions, + private void sendMinimumDimensionViolation(TaskFragment taskFragment, Point minDimensions, IBinder errorCallbackToken, String reason) { if (taskFragment == null || taskFragment.getTaskFragmentOrganizer() == null) { return; @@ -1672,7 +1673,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // We are reparenting activities to a new embedded TaskFragment, this operation is only // allowed if the new parent is trusted by all reparent activities. final boolean isEmbeddingDisallowed = oldParent.forAllActivities(activity -> - !newParentTF.isAllowedToEmbedActivity(activity)); + newParentTF.isAllowedToEmbedActivity(activity) == EMBEDDING_ALLOWED); if (isEmbeddingDisallowed) { final Throwable exception = new SecurityException( "The new parent is not trusted to embed the activities."); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 46cf5c9348cc..46091d842c2a 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -115,6 +115,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS_LIGHT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RESIZE; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW; +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_INSETS; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; import static com.android.server.policy.WindowManagerPolicy.TRANSIT_ENTER; @@ -392,6 +393,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP int mSyncSeqId = 0; int mLastSeqIdSentToRelayout = 0; + /** The last syncId associated with a prepareSync or 0 when no sync is active. */ + int mPrepareSyncSeqId = 0; + /** * {@code true} when the client was still drawing for sync when the sync-set was finished or * cancelled. This can happen if the window goes away during a sync. In this situation we need @@ -809,7 +813,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP }; private final Consumer<SurfaceControl.Transaction> mSetSurfacePositionConsumer = t -> { - if (mSurfaceControl != null && mSurfaceControl.isValid()) { + // Only apply the position to the surface when there's no leash created. + if (mSurfaceControl != null && mSurfaceControl.isValid() && !mSurfaceAnimator.hasLeash()) { t.setPosition(mSurfaceControl, mSurfacePosition.x, mSurfacePosition.y); } }; @@ -1246,7 +1251,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mSession.windowAddedLocked(); } - boolean updateGlobalScale() { + void updateGlobalScale() { if (hasCompatScale()) { if (mOverrideScale != 1f) { mGlobalScale = mToken.hasSizeCompatBounds() @@ -1256,11 +1261,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mGlobalScale = mToken.getSizeCompatScale(); } mInvGlobalScale = 1f / mGlobalScale; - return true; + return; } mGlobalScale = mInvGlobalScale = 1f; - return false; } /** @@ -1513,23 +1517,23 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final boolean dragResizingChanged = isDragResizeChanged() && !isDragResizingChangeReported(); + final boolean attachedFrameChanged = LOCAL_LAYOUT + && mLayoutAttached && getParentWindow().frameChanged(); + if (DEBUG) { Slog.v(TAG_WM, "Resizing " + this + ": configChanged=" + configChanged + " dragResizingChanged=" + dragResizingChanged + " last=" + mWindowFrames.mLastFrame + " frame=" + mWindowFrames.mFrame); } - // We update mLastFrame always rather than in the conditional with the last inset - // variables, because mFrameSizeChanged only tracks the width and height changing. - updateLastFrames(); - // Add a window that is using blastSync to the resizing list if it hasn't been reported // already. This because the window is waiting on a finishDrawing from the client. if (didFrameInsetsChange || configChanged || insetsChanged || dragResizingChanged - || shouldSendRedrawForSync()) { + || shouldSendRedrawForSync() + || attachedFrameChanged) { ProtoLog.v(WM_DEBUG_RESIZE, "Resize reasons for w=%s: %s configChanged=%b dragResizingChanged=%b", this, mWindowFrames.getInsetsChangedInfo(), @@ -1585,6 +1589,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } } + private boolean frameChanged() { + return !mWindowFrames.mFrame.equals(mWindowFrames.mLastFrame); + } + boolean getOrientationChanging() { // In addition to the local state flag, we must also consider the difference in the last // reported configuration vs. the current state. If the client code has not been informed of @@ -3836,6 +3844,12 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (mInvGlobalScale != 1.0f && hasCompatScale()) { outFrames.displayFrame.scale(mInvGlobalScale); } + if (mLayoutAttached) { + if (outFrames.attachedFrame == null) { + outFrames.attachedFrame = new Rect(); + } + outFrames.attachedFrame.set(getParentWindow().getCompatFrame()); + } // Note: in the cases where the window is tied to an activity, we should not send a // configuration update when the window has requested to be hidden. Doing so can lead to @@ -3887,6 +3901,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mDragResizingChangeReported = true; mWindowFrames.clearReportResizeHints(); + // We update mLastFrame always rather than in the conditional with the last inset + // variables, because mFrameSizeChanged only tracks the width and height changing. + updateLastFrames(); + final int prevRotation = mLastReportedConfiguration .getMergedConfiguration().windowConfiguration.getRotation(); fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration, @@ -3921,7 +3939,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration, getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId, mSyncSeqId, resizeMode); - if (drawPending && prevRotation != mLastReportedConfiguration + if (drawPending && prevRotation >= 0 && prevRotation != mLastReportedConfiguration .getMergedConfiguration().windowConfiguration.getRotation()) { mOrientationChangeRedrawRequestTime = SystemClock.elapsedRealtime(); ProtoLog.v(WM_DEBUG_ORIENTATION, @@ -4405,6 +4423,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP pw.println(prefix + "Requested visibilities: " + visibilityString); } } + + pw.println(prefix + "mPrepareSyncSeqId=" + mPrepareSyncSeqId); } @Override @@ -5896,6 +5916,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mWinAnimator.getSurfaceControl(); } + /** Drops a buffer for this window's view-root from a transaction */ + private void dropBufferFrom(Transaction t) { + SurfaceControl viewSurface = getClientViewRootSurface(); + if (viewSurface == null) return; + t.setBuffer(viewSurface, (android.hardware.HardwareBuffer) null); + } + @Override boolean prepareSync() { if (!mDrawHandlers.isEmpty()) { @@ -5911,7 +5938,18 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // to draw even if the children draw first or don't need to sync, so we start // in WAITING state rather than READY. mSyncState = SYNC_STATE_WAITING_FOR_DRAW; + + if (mPrepareSyncSeqId > 0) { + // another prepareSync during existing sync (eg. reparented), so pre-emptively + // drop buffer (if exists). If the buffer hasn't been received yet, it will be + // dropped in finishDrawing. + ProtoLog.d(WM_DEBUG_SYNC_ENGINE, "Preparing to sync a window that was already in the" + + " sync, so try dropping buffer. win=%s", this); + dropBufferFrom(mSyncTransaction); + } + mSyncSeqId++; + mPrepareSyncSeqId = mSyncSeqId; requestRedrawForSync(); return true; } @@ -5932,6 +5970,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mRedrawForSyncReported) { mClientWasDrawingForSync = true; } + mPrepareSyncSeqId = 0; + if (cancel) { + // This is leaving sync so any buffers left in the sync have a chance of + // being applied out-of-order and can also block the buffer queue for this + // window. To prevent this, drop the buffer. + dropBufferFrom(mSyncTransaction); + } super.finishSync(outMergedTransaction, cancel); } @@ -5953,6 +5998,17 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP .notifyStartingWindowDrawn(mActivityRecord); } + final boolean syncActive = mPrepareSyncSeqId > 0; + final boolean syncStillPending = syncActive && mPrepareSyncSeqId > syncSeqId; + if (syncStillPending && postDrawTransaction != null) { + ProtoLog.d(WM_DEBUG_SYNC_ENGINE, "Got a buffer for request id=%d but latest request is" + + " id=%d. Since the buffer is out-of-date, drop it. win=%s", syncSeqId, + mPrepareSyncSeqId, this); + // sync is waiting for a newer seqId, so this buffer is obsolete and can be dropped + // to free up the buffer queue. + dropBufferFrom(postDrawTransaction); + } + final boolean hasSyncHandlers = executeDrawHandlers(postDrawTransaction, syncSeqId); boolean skipLayout = false; @@ -5965,10 +6021,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // Layout is not needed because the window will be hidden by the fade leash. postDrawTransaction = null; skipLayout = true; - } else if (onSyncFinishedDrawing() && postDrawTransaction != null) { - mSyncTransaction.merge(postDrawTransaction); - // Consume the transaction because the sync group will merge it. - postDrawTransaction = null; + } else if (syncActive) { + if (!syncStillPending) { + onSyncFinishedDrawing(); + } + if (postDrawTransaction != null) { + mSyncTransaction.merge(postDrawTransaction); + // Consume the transaction because the sync group will merge it. + postDrawTransaction = null; + } } final boolean layoutNeeded = @@ -6198,4 +6259,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @WindowTraceLogLevel int logLevel) { dumpDebug(proto, fieldId, logLevel); } + + public boolean cancelAndRedraw() { + // Cancel any draw requests during a sync. + return mPrepareSyncSeqId > 0; + } } diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java index ab72e49f1e4f..437c9344d793 100644 --- a/services/core/java/com/android/server/wm/WindowToken.java +++ b/services/core/java/com/android/server/wm/WindowToken.java @@ -448,8 +448,14 @@ class WindowToken extends WindowContainer<WindowState> { if (mFixedRotationTransformState != null) { mFixedRotationTransformState.disassociate(this); } + // TODO(b/233855302): Remove TaskFragment override if the DisplayContent uses the same + // bounds for screenLayout calculation. + final Configuration overrideConfig = new Configuration(config); + overrideConfig.screenLayout = TaskFragment.computeScreenLayoutOverride( + overrideConfig.screenLayout, overrideConfig.screenWidthDp, + overrideConfig.screenHeightDp); mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames, - new Configuration(config), mDisplayContent.getRotation()); + overrideConfig, mDisplayContent.getRotation()); mFixedRotationTransformState.mAssociatedTokens.add(this); mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames); onFixedRotationStatePrepared(); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 05ea9cc9ac3b..06fb4b0a01d8 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -1982,6 +1982,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { synchronized (getLockObject()) { mOwners.load(); setDeviceOwnershipSystemPropertyLocked(); + if (mOwners.hasDeviceOwner()) { + setGlobalSettingDeviceOwnerType( + mOwners.getDeviceOwnerType(mOwners.getDeviceOwnerPackageName())); + } } } @@ -8811,6 +8815,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { deleteTransferOwnershipBundleLocked(userId); toggleBackupServiceActive(UserHandle.USER_SYSTEM, true); pushUserControlDisabledPackagesLocked(userId); + setGlobalSettingDeviceOwnerType(DEVICE_OWNER_TYPE_DEFAULT); } private void clearApplicationRestrictions(int userId) { @@ -18377,6 +18382,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { "Test only admins can only set the device owner type more than once"); mOwners.setDeviceOwnerType(packageName, deviceOwnerType, isAdminTestOnly); + setGlobalSettingDeviceOwnerType(deviceOwnerType); + } + + // TODO(b/237065504): Allow mainline modules to get the device owner type. This is a workaround + // to get the device owner type in PermissionController. See HibernationPolicy.kt. + private void setGlobalSettingDeviceOwnerType(int deviceOwnerType) { + mInjector.binderWithCleanCallingIdentity( + () -> mInjector.settingsGlobalPutInt("device_owner_type", deviceOwnerType)); } @Override diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index ef311c249c5f..66c9f55b0403 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -412,7 +412,7 @@ public final class SystemServer implements Dumpable { "/apex/com.android.uwb/javalib/service-uwb.jar"; private static final String UWB_SERVICE_CLASS = "com.android.server.uwb.UwbService"; private static final String BLUETOOTH_APEX_SERVICE_JAR_PATH = - "/apex/com.android.bluetooth/javalib/service-bluetooth.jar"; + "/apex/com.android.btservices/javalib/service-bluetooth.jar"; private static final String BLUETOOTH_SERVICE_CLASS = "com.android.server.bluetooth.BluetoothService"; private static final String SAFETY_CENTER_SERVICE_CLASS = diff --git a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java index b36aa0617be5..e87dd4b423b2 100644 --- a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java @@ -36,8 +36,6 @@ import android.util.Log; import androidx.test.InstrumentationRegistry; -import com.android.server.FgThread; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -48,6 +46,11 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; @@ -88,6 +91,7 @@ public final class AdbDebuggingManagerTest { private long mOriginalAllowedConnectionTime; private File mAdbKeyXmlFile; private File mAdbKeyFile; + private FakeTicker mFakeTicker; @Before public void setUp() throws Exception { @@ -96,14 +100,25 @@ public final class AdbDebuggingManagerTest { if (mAdbKeyFile.exists()) { mAdbKeyFile.delete(); } - mManager = new AdbDebuggingManager(mContext, ADB_CONFIRM_COMPONENT, mAdbKeyFile); mAdbKeyXmlFile = new File(mContext.getFilesDir(), "test_adb_keys.xml"); if (mAdbKeyXmlFile.exists()) { mAdbKeyXmlFile.delete(); } + + mFakeTicker = new FakeTicker(); + // Set the ticker time to October 22, 2008 (the day the T-Mobile G1 was released) + mFakeTicker.advance(1224658800L); + mThread = new AdbDebuggingThreadTest(); - mKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile); - mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper(), mThread, mKeyStore); + mManager = new AdbDebuggingManager( + mContext, ADB_CONFIRM_COMPONENT, mAdbKeyFile, mAdbKeyXmlFile, mThread, mFakeTicker); + + mHandler = mManager.mHandler; + mThread.setHandler(mHandler); + + mHandler.initKeyStore(); + mKeyStore = mHandler.mAdbKeyStore; + mOriginalAllowedConnectionTime = mKeyStore.getAllowedConnectionTime(); mBlockingQueue = new ArrayBlockingQueue<>(1); } @@ -122,7 +137,7 @@ public final class AdbDebuggingManagerTest { private void setAllowedConnectionTime(long connectionTime) { Settings.Global.putLong(mContext.getContentResolver(), Settings.Global.ADB_ALLOWED_CONNECTION_TIME, connectionTime); - }; + } @Test public void testAllowNewKeyOnce() throws Exception { @@ -158,20 +173,15 @@ public final class AdbDebuggingManagerTest { // Allow a connection from a new key with the 'Always allow' option selected. runAdbTest(TEST_KEY_1, true, true, false); - // Get the last connection time for the currently connected key to verify that it is updated - // after the disconnect. - long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); - - // Sleep for a small amount of time to ensure a difference can be observed in the last - // connection time after a disconnect. - Thread.sleep(10); + // Advance the clock by 10ms to ensure there's a difference + mFakeTicker.advance(10 * 1_000_000); // Send the disconnect message for the currently connected key to trigger an update of the // last connection time. disconnectKey(TEST_KEY_1); - assertNotEquals( + assertEquals( "The last connection time was not updated after the disconnect", - lastConnectionTime, + mFakeTicker.currentTimeMillis(), mKeyStore.getLastConnectionTime(TEST_KEY_1)); } @@ -244,8 +254,8 @@ public final class AdbDebuggingManagerTest { // Get the current last connection time for comparison after the scheduled job is run long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); - // Sleep a small amount of time to ensure that the updated connection time changes - Thread.sleep(10); + // Advance a small amount of time to ensure that the updated connection time changes + mFakeTicker.advance(10); // Send a message to the handler to update the last connection time for the active key updateKeyStore(); @@ -269,13 +279,13 @@ public final class AdbDebuggingManagerTest { persistKeyStore(); assertTrue( "The key with the 'Always allow' option selected was not persisted in the keystore", - mManager.new AdbKeyStore(mAdbKeyXmlFile).isKeyAuthorized(TEST_KEY_1)); + mManager.new AdbKeyStore().isKeyAuthorized(TEST_KEY_1)); // Get the current last connection time to ensure it is updated in the persisted keystore. long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); - // Sleep a small amount of time to ensure the last connection time is updated. - Thread.sleep(10); + // Advance a small amount of time to ensure the last connection time is updated. + mFakeTicker.advance(10); // Send a message to the handler to update the last connection time for the active key. updateKeyStore(); @@ -286,7 +296,7 @@ public final class AdbDebuggingManagerTest { assertNotEquals( "The last connection time in the key file was not updated after the update " + "connection time message", lastConnectionTime, - mManager.new AdbKeyStore(mAdbKeyXmlFile).getLastConnectionTime(TEST_KEY_1)); + mManager.new AdbKeyStore().getLastConnectionTime(TEST_KEY_1)); // Verify that the key is in the adb_keys file assertTrue("The key was not in the adb_keys file after persisting the keystore", isKeyInFile(TEST_KEY_1, mAdbKeyFile)); @@ -327,8 +337,8 @@ public final class AdbDebuggingManagerTest { // Set the allowed window to a small value to ensure the time is beyond the allowed window. setAllowedConnectionTime(1); - // Sleep for a small amount of time to exceed the allowed window. - Thread.sleep(10); + // Advance a small amount of time to exceed the allowed window. + mFakeTicker.advance(10); // The AdbKeyStore has a method to get the time of the next key expiration to ensure the // scheduled job runs at the time of the next expiration or after 24 hours, whichever occurs @@ -478,9 +488,12 @@ public final class AdbDebuggingManagerTest { // Set the current expiration time to a minute from expiration and verify this new value is // returned. final long newExpirationTime = 60000; - mKeyStore.setLastConnectionTime(TEST_KEY_1, - System.currentTimeMillis() - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME - + newExpirationTime, true); + mKeyStore.setLastConnectionTime( + TEST_KEY_1, + mFakeTicker.currentTimeMillis() + - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME + + newExpirationTime, + true); expirationTime = mKeyStore.getNextExpirationTime(); if (Math.abs(expirationTime - newExpirationTime) > epsilon) { fail("The expiration time for a key about to expire, " + expirationTime @@ -525,7 +538,7 @@ public final class AdbDebuggingManagerTest { // Get the last connection time for the key to verify that it is updated when the connected // key message is sent. long connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); - Thread.sleep(10); + mFakeTicker.advance(10); mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONNECTED_KEY, TEST_KEY_1).sendToTarget(); flushHandlerQueue(); @@ -536,7 +549,7 @@ public final class AdbDebuggingManagerTest { // Verify that the scheduled job updates the connection time of the key. connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); - Thread.sleep(10); + mFakeTicker.advance(10); updateKeyStore(); assertNotEquals( "The connection time for the key must be updated when the update keystore message" @@ -545,7 +558,7 @@ public final class AdbDebuggingManagerTest { // Verify that the connection time is updated when the key is disconnected. connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); - Thread.sleep(10); + mFakeTicker.advance(10); disconnectKey(TEST_KEY_1); assertNotEquals( "The connection time for the key must be updated when the disconnected message is" @@ -628,11 +641,11 @@ public final class AdbDebuggingManagerTest { setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME); // The untracked keys should be added to the keystore as part of the constructor. - AdbDebuggingManager.AdbKeyStore adbKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile); + AdbDebuggingManager.AdbKeyStore adbKeyStore = mManager.new AdbKeyStore(); // Verify that the connection time for each test key is within a small value of the current // time. - long time = System.currentTimeMillis(); + long time = mFakeTicker.currentTimeMillis(); for (String key : testKeys) { long connectionTime = adbKeyStore.getLastConnectionTime(key); if (Math.abs(time - connectionTime) > epsilon) { @@ -651,11 +664,11 @@ public final class AdbDebuggingManagerTest { runAdbTest(TEST_KEY_1, true, true, false); runAdbTest(TEST_KEY_2, true, true, false); - // Sleep a small amount of time to ensure the connection time is updated by the scheduled + // Advance a small amount of time to ensure the connection time is updated by the scheduled // job. long connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1); long connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2); - Thread.sleep(10); + mFakeTicker.advance(10); updateKeyStore(); assertNotEquals( "The connection time for test key 1 must be updated after the scheduled job runs", @@ -669,7 +682,7 @@ public final class AdbDebuggingManagerTest { disconnectKey(TEST_KEY_2); connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1); connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2); - Thread.sleep(10); + mFakeTicker.advance(10); updateKeyStore(); assertNotEquals( "The connection time for test key 1 must be updated after another key is " @@ -686,8 +699,6 @@ public final class AdbDebuggingManagerTest { // to clear the adb authorizations when adb is disabled after a boot a NullPointerException // was thrown as deleteKeyStore is invoked against the key store. This test ensures the // key store can be successfully cleared when adb is disabled. - mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper()); - clearKeyStore(); } @@ -723,6 +734,9 @@ public final class AdbDebuggingManagerTest { // Now remove one of the keys and make sure the other key is still there mKeyStore.removeKey(TEST_KEY_1); + // Wait for the handler queue to receive the MESSAGE_ADB_PERSIST_KEYSTORE + flushHandlerQueue(); + assertFalse("The key was still in the adb_keys file after removing the key", isKeyInFile(TEST_KEY_1, mAdbKeyFile)); assertTrue("The key was not in the adb_keys file after removing a different key", @@ -730,6 +744,95 @@ public final class AdbDebuggingManagerTest { } @Test + public void testAdbKeyStore_addDuplicateKey_doesNotAddDuplicateToAdbKeyFile() throws Exception { + setAllowedConnectionTime(0); + + runAdbTest(TEST_KEY_1, true, true, false); + persistKeyStore(); + runAdbTest(TEST_KEY_1, true, true, false); + persistKeyStore(); + + assertEquals("adb_keys contains duplicate keys", 1, adbKeyFileKeys(mAdbKeyFile).size()); + } + + @Test + public void testAdbKeyStore_adbTempKeysFile_readsLastConnectionTimeFromXml() throws Exception { + long insertTime = mFakeTicker.currentTimeMillis(); + runAdbTest(TEST_KEY_1, true, true, false); + persistKeyStore(); + + mFakeTicker.advance(10); + AdbDebuggingManager.AdbKeyStore newKeyStore = mManager.new AdbKeyStore(); + + assertEquals( + "KeyStore not populated from the XML file.", + insertTime, + newKeyStore.getLastConnectionTime(TEST_KEY_1)); + } + + @Test + public void test_notifyKeyFilesUpdated_filesDeletedRemovesPreviouslyAddedKey() + throws Exception { + runAdbTest(TEST_KEY_1, true, true, false); + persistKeyStore(); + + Files.delete(mAdbKeyXmlFile.toPath()); + Files.delete(mAdbKeyFile.toPath()); + + mManager.notifyKeyFilesUpdated(); + flushHandlerQueue(); + + assertFalse( + "Key is authorized after reloading deleted key files. Was state preserved?", + mKeyStore.isKeyAuthorized(TEST_KEY_1)); + } + + @Test + public void test_notifyKeyFilesUpdated_newKeyIsAuthorized() throws Exception { + runAdbTest(TEST_KEY_1, true, true, false); + persistKeyStore(); + + // Back up the existing key files + Path tempXmlFile = Files.createTempFile("adbKeyXmlFile", ".tmp"); + Path tempAdbKeysFile = Files.createTempFile("adb_keys", ".tmp"); + Files.copy(mAdbKeyXmlFile.toPath(), tempXmlFile, StandardCopyOption.REPLACE_EXISTING); + Files.copy(mAdbKeyFile.toPath(), tempAdbKeysFile, StandardCopyOption.REPLACE_EXISTING); + + // Delete the existing key files + Files.delete(mAdbKeyXmlFile.toPath()); + Files.delete(mAdbKeyFile.toPath()); + + // Notify the manager that adb key files have changed. + mManager.notifyKeyFilesUpdated(); + flushHandlerQueue(); + + // Copy the files back + Files.copy(tempXmlFile, mAdbKeyXmlFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + Files.copy(tempAdbKeysFile, mAdbKeyFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + + // Tell the manager that the key files have changed. + mManager.notifyKeyFilesUpdated(); + flushHandlerQueue(); + + assertTrue( + "Key is not authorized after reloading key files.", + mKeyStore.isKeyAuthorized(TEST_KEY_1)); + } + + @Test + public void testAdbKeyStore_adbWifiConnect_storesBssidWhenAlwaysAllow() throws Exception { + String trustedNetwork = "My Network"; + mKeyStore.addTrustedNetwork(trustedNetwork); + persistKeyStore(); + + AdbDebuggingManager.AdbKeyStore newKeyStore = mManager.new AdbKeyStore(); + + assertTrue( + "Persisted trusted network not found in new keystore instance.", + newKeyStore.isTrustedNetwork(trustedNetwork)); + } + + @Test public void testIsValidMdnsServiceName() { // Longer than 15 characters assertFalse(isValidMdnsServiceName("abcd1234abcd1234")); @@ -1030,28 +1133,27 @@ public final class AdbDebuggingManagerTest { if (key == null) { return false; } + return adbKeyFileKeys(keyFile).contains(key); + } + + private static List<String> adbKeyFileKeys(File keyFile) throws Exception { + List<String> keys = new ArrayList<>(); if (keyFile.exists()) { try (BufferedReader in = new BufferedReader(new FileReader(keyFile))) { String currKey; while ((currKey = in.readLine()) != null) { - if (key.equals(currKey)) { - return true; - } + keys.add(currKey); } } } - return false; + return keys; } /** * Helper class that extends AdbDebuggingThread to receive the response from AdbDebuggingManager * indicating whether the key should be allowed to connect. */ - class AdbDebuggingThreadTest extends AdbDebuggingManager.AdbDebuggingThread { - AdbDebuggingThreadTest() { - mManager.super(); - } - + private class AdbDebuggingThreadTest extends AdbDebuggingManager.AdbDebuggingThread { @Override public void sendResponse(String msg) { TestResult result = new TestResult(TestResult.RESULT_RESPONSE_RECEIVED, msg); @@ -1091,4 +1193,17 @@ public final class AdbDebuggingManagerTest { return "{mReturnCode = " + mReturnCode + ", mMessage = " + mMessage + "}"; } } + + private static class FakeTicker implements AdbDebuggingManager.Ticker { + private long mCurrentTime; + + private void advance(long milliseconds) { + mCurrentTime += milliseconds; + } + + @Override + public long currentTimeMillis() { + return mCurrentTime; + } + } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index 25cf8a86baad..e95924ad7109 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -20,7 +20,9 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_NEGATIVE; -import static com.android.server.biometrics.BiometricServiceStateProto.*; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -32,6 +34,8 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -280,6 +284,43 @@ public class AuthSessionTest { } @Test + public void testOnDialogAnimatedInDoesNothingDuringInvalidState() throws Exception { + setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); + final long operationId = 123; + final int userId = 10; + + final AuthSession session = createAuthSession(mSensors, + false /* checkDevicePolicyManager */, + Authenticators.BIOMETRIC_STRONG, + TEST_REQUEST_ID, + operationId, + userId); + final IBiometricAuthenticator impl = session.mPreAuthInfo.eligibleSensors.get(0).impl; + + session.goToInitialState(); + for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { + assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState()); + session.onCookieReceived( + session.mPreAuthInfo.eligibleSensors.get(sensor.id).getCookie()); + } + assertTrue(session.allCookiesReceived()); + assertEquals(STATE_AUTH_STARTED, session.getState()); + verify(impl, never()).startPreparedClient(anyInt()); + + // First invocation should start the client monitor. + session.onDialogAnimatedIn(); + assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); + verify(impl).startPreparedClient(anyInt()); + + // Subsequent invocations should not start the client monitor again. + session.onDialogAnimatedIn(); + session.onDialogAnimatedIn(); + session.onDialogAnimatedIn(); + assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); + verify(impl, times(1)).startPreparedClient(anyInt()); + } + + @Test public void testCancelAuthentication_whenStateAuthCalled_invokesCancel() throws RemoteException { testInvokesCancel(session -> session.onCancelAuthSession(false /* force */)); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java index c17347320f52..9e9d70332f00 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java @@ -80,11 +80,14 @@ public class BiometricSchedulerOperationTest { private Handler mHandler; private BiometricSchedulerOperation mOperation; + private boolean mIsDebuggable; @Before public void setUp() { mHandler = new Handler(TestableLooper.get(this).getLooper()); - mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback); + mIsDebuggable = false; + mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback, + () -> mIsDebuggable); } @Test @@ -126,6 +129,34 @@ public class BiometricSchedulerOperationTest { } @Test + public void testSecondStartWithCookieCrashesWhenDebuggable() { + final int cookie = 5; + mIsDebuggable = true; + when(mClientMonitor.getCookie()).thenReturn(cookie); + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie); + assertThat(started).isTrue(); + + assertThrows(IllegalStateException.class, + () -> mOperation.startWithCookie(mOnStartCallback, cookie)); + } + + @Test + public void testSecondStartWithCookieFailsNicelyWhenNotDebuggable() { + final int cookie = 5; + mIsDebuggable = false; + when(mClientMonitor.getCookie()).thenReturn(cookie); + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie); + assertThat(started).isTrue(); + + final boolean startedAgain = mOperation.startWithCookie(mOnStartCallback, cookie); + assertThat(startedAgain).isFalse(); + } + + @Test public void startsWhenReadyAndHalAvailable() { when(mClientMonitor.getCookie()).thenReturn(0); when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); @@ -170,7 +201,34 @@ public class BiometricSchedulerOperationTest { } @Test + public void secondStartCrashesWhenDebuggable() { + mIsDebuggable = true; + when(mClientMonitor.getCookie()).thenReturn(0); + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + final boolean started = mOperation.start(mOnStartCallback); + assertThat(started).isTrue(); + + assertThrows(IllegalStateException.class, () -> mOperation.start(mOnStartCallback)); + } + + @Test + public void secondStartFailsNicelyWhenNotDebuggable() { + mIsDebuggable = false; + when(mClientMonitor.getCookie()).thenReturn(0); + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + final boolean started = mOperation.start(mOnStartCallback); + assertThat(started).isTrue(); + + final boolean startedAgain = mOperation.start(mOnStartCallback); + assertThat(startedAgain).isFalse(); + } + + @Test public void doesNotStartWithCookie() { + // This class only throws exceptions when debuggable. + mIsDebuggable = true; when(mClientMonitor.getCookie()).thenReturn(9); assertThrows(IllegalStateException.class, () -> mOperation.start(mock(ClientMonitorCallback.class))); @@ -178,6 +236,8 @@ public class BiometricSchedulerOperationTest { @Test public void cannotRestart() { + // This class only throws exceptions when debuggable. + mIsDebuggable = true; when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); mOperation.start(mOnStartCallback); @@ -188,6 +248,8 @@ public class BiometricSchedulerOperationTest { @Test public void abortsNotRunning() { + // This class only throws exceptions when debuggable. + mIsDebuggable = true; when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); mOperation.abort(); @@ -200,7 +262,8 @@ public class BiometricSchedulerOperationTest { } @Test - public void cannotAbortRunning() { + public void abortCrashesWhenDebuggableIfOperationIsRunning() { + mIsDebuggable = true; when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); mOperation.start(mOnStartCallback); @@ -209,6 +272,16 @@ public class BiometricSchedulerOperationTest { } @Test + public void abortFailsNicelyWhenNotDebuggableIfOperationIsRunning() { + mIsDebuggable = false; + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + mOperation.start(mOnStartCallback); + + mOperation.abort(); + } + + @Test public void cancel() { when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); @@ -254,6 +327,30 @@ public class BiometricSchedulerOperationTest { } @Test + public void cancelCrashesWhenDebuggableIfOperationIsFinished() { + mIsDebuggable = true; + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + mOperation.abort(); + assertThat(mOperation.isFinished()).isTrue(); + + final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); + assertThrows(IllegalStateException.class, () -> mOperation.cancel(mHandler, cancelCb)); + } + + @Test + public void cancelFailsNicelyWhenNotDebuggableIfOperationIsFinished() { + mIsDebuggable = false; + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + mOperation.abort(); + assertThat(mOperation.isFinished()).isTrue(); + + final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); + mOperation.cancel(mHandler, cancelCb); + } + + @Test public void markCanceling() { when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index f242fda15f06..c80547ce61c0 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -213,7 +213,7 @@ public class VirtualDeviceManagerServiceTest { mContext.getSystemService(WindowManager.class), threadVerifier); mAssociationInfo = new AssociationInfo(1, 0, null, - MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0); + MacAddress.BROADCAST_ADDRESS, "", null, true, false, false, 0, 0); VirtualDeviceParams params = new VirtualDeviceParams .Builder() diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java index 0ed90d27db99..6a6cd6c914a2 100644 --- a/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java @@ -16,13 +16,11 @@ package com.android.server.display; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -34,17 +32,18 @@ import android.os.IThermalEventListener; import android.os.IThermalService; import android.os.Message; import android.os.PowerManager; -import android.os.Temperature.ThrottlingStatus; import android.os.Temperature; +import android.os.Temperature.ThrottlingStatus; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.os.BackgroundThread; import com.android.server.display.BrightnessThrottler.Injector; -import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel; import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData; +import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel; import org.junit.Before; import org.junit.Test; @@ -55,7 +54,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; @SmallTest @@ -70,6 +68,8 @@ public class BrightnessThrottlerTest { @Mock IThermalService mThermalServiceMock; @Mock Injector mInjectorMock; + DisplayModeDirectorTest.FakeDeviceConfig mDeviceConfigFake; + @Captor ArgumentCaptor<IThermalEventListener> mThermalEventListenerCaptor; @Before @@ -83,6 +83,8 @@ public class BrightnessThrottlerTest { return true; } }); + mDeviceConfigFake = new DisplayModeDirectorTest.FakeDeviceConfig(); + when(mInjectorMock.getDeviceConfig()).thenReturn(mDeviceConfigFake); } @@ -292,6 +294,170 @@ public class BrightnessThrottlerTest { assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason()); } + @Test public void testUpdateThrottlingData() throws Exception { + // Initialise brightness throttling levels + // Ensure that they are overridden by setting the data through device config. + final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, + 0.25f); + List<ThrottlingLevel> levels = new ArrayList<>(); + levels.add(level); + final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels); + mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,0.4"); + final BrightnessThrottler throttler = createThrottlerSupported(data); + + verify(mThermalServiceMock).registerThermalEventListenerWithType( + mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN)); + final IThermalEventListener listener = mThermalEventListenerCaptor.getValue(); + + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(level.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(0.4f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + + // Update thresholds + // This data is equivalent to the string "123,1,critical,0.8", passed below + final ThrottlingLevel newLevel = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, + 0.8f); + // Set new (valid) data from device config + mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,0.8"); + + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(newLevel.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(newLevel.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(newLevel.brightness, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + } + + @Test public void testInvalidThrottlingStrings() throws Exception { + // Initialise brightness throttling levels + // Ensure that they are not overridden by invalid data through device config. + final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, + 0.25f); + List<ThrottlingLevel> levels = new ArrayList<>(); + levels.add(level); + final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels); + final BrightnessThrottler throttler = createThrottlerSupported(data); + verify(mThermalServiceMock).registerThermalEventListenerWithType( + mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN)); + final IThermalEventListener listener = mThermalEventListenerCaptor.getValue(); + + // None of these are valid so shouldn't override the original data + mDeviceConfigFake.setBrightnessThrottlingData("321,1,critical,0.4"); // Not the current id + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("123,0,critical,0.4"); // Incorrect number + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("123,2,critical,0.4"); // Incorrect number + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("123,1,invalid,0.4"); // Invalid level + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,none"); // Invalid brightness + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,-3"); // Invalid brightness + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("invalid string"); // Invalid format + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData(""); // Invalid format + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + } + + private void testThrottling(BrightnessThrottler throttler, IThermalEventListener listener, + float tooLowCap, float tooHighCap) throws Exception { + final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, + tooHighCap); + + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(tooLowCap, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(level.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(tooHighCap, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + } + + @Test public void testMultipleConfigPoints() throws Exception { + // Initialise brightness throttling levels + final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, + 0.25f); + List<ThrottlingLevel> levels = new ArrayList<>(); + levels.add(level); + final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels); + + // These are identical to the string set below + final ThrottlingLevel levelSevere = new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE, + 0.9f); + final ThrottlingLevel levelCritical = new ThrottlingLevel( + PowerManager.THERMAL_STATUS_CRITICAL, 0.5f); + final ThrottlingLevel levelEmergency = new ThrottlingLevel( + PowerManager.THERMAL_STATUS_EMERGENCY, 0.1f); + + mDeviceConfigFake.setBrightnessThrottlingData( + "123,3,severe,0.9,critical,0.5,emergency,0.1"); + final BrightnessThrottler throttler = createThrottlerSupported(data); + + verify(mThermalServiceMock).registerThermalEventListenerWithType( + mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN)); + final IThermalEventListener listener = mThermalEventListenerCaptor.getValue(); + + // Ensure that the multiple levels set via the string through the device config correctly + // override the original display device config ones. + + // levelSevere + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(0.9f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + + // levelCritical + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(0.9f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(0.5f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + + //levelEmergency + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(0.5f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(0.1f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + } + private void assertThrottlingLevelsEquals( List<ThrottlingLevel> expected, List<ThrottlingLevel> actual) { @@ -307,12 +473,13 @@ public class BrightnessThrottlerTest { } private BrightnessThrottler createThrottlerUnsupported() { - return new BrightnessThrottler(mInjectorMock, mHandler, null, () -> {}); + return new BrightnessThrottler(mInjectorMock, mHandler, mHandler, null, () -> {}, null); } private BrightnessThrottler createThrottlerSupported(BrightnessThrottlingData data) { assertNotNull(data); - return new BrightnessThrottler(mInjectorMock, mHandler, data, () -> {}); + return new BrightnessThrottler(mInjectorMock, mHandler, BackgroundThread.getHandler(), + data, () -> {}, "123"); } private Temperature getSkinTemp(@ThrottlingStatus int status) { diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java index 864f31552ae3..968e1d8c546b 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java @@ -16,6 +16,7 @@ package com.android.server.display; +import static android.hardware.display.DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA; import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_HIGH_AMBIENT_BRIGHTNESS_THRESHOLDS; import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_HIGH_DISPLAY_BRIGHTNESS_THRESHOLDS; import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_LOW_AMBIENT_BRIGHTNESS_THRESHOLDS; @@ -1902,6 +1903,11 @@ public class DisplayModeDirectorTest { KEY_REFRESH_RATE_IN_HBM_HDR, String.valueOf(fps)); } + void setBrightnessThrottlingData(String brightnessThrottlingData) { + putPropertyAndNotify(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + KEY_BRIGHTNESS_THROTTLING_DATA, brightnessThrottlingData); + } + void setLowDisplayBrightnessThresholds(int[] brightnessThresholds) { String thresholds = toPropertyValue(brightnessThresholds); diff --git a/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java b/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java index 363c26b63bae..bbed1b60f8bf 100644 --- a/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java @@ -16,11 +16,14 @@ package com.android.server.display.color; +import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -1130,6 +1133,15 @@ public class ColorDisplayServiceTest { eq(ColorDisplayManager.COLOR_MODE_BOOSTED), any(), eq(Display.COLOR_MODE_INVALID)); } + @Test + public void getColorMode_noAvailableModes_returnsNotSet() { + when(mResourcesSpy.getIntArray(R.array.config_availableColorModes)) + .thenReturn(new int[] {}); + startService(); + verify(mDisplayTransformManager, never()).setColorMode(anyInt(), any(), anyInt()); + assertThat(mBinderService.getColorMode()).isEqualTo(-1); + } + /** * Configures Night display to use a custom schedule. * diff --git a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java index 75bd2ccbe635..bc2c57e8bf84 100644 --- a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java +++ b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java @@ -22,6 +22,7 @@ import static junit.framework.TestCase.fail; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; import android.app.usage.TimeSparseArray; import android.app.usage.UsageEvents.Event; @@ -440,6 +441,7 @@ public class UsageStatsDatabaseTest { prevDB.readMappingsLocked(); prevDB.init(1); prevDB.putUsageStats(UsageStatsManager.INTERVAL_DAILY, mIntervalStats); + Set<String> prevDBApps = mIntervalStats.packageStats.keySet(); // Create a backup with a specific version byte[] blob = prevDB.getBackupPayload(KEY_USAGE_STATS, version); if (version >= 1 && version <= 3) { @@ -447,6 +449,11 @@ public class UsageStatsDatabaseTest { "UsageStatsDatabase shouldn't be able to write backups as XML"); return; } + if (version < 1 || version > UsageStatsDatabase.BACKUP_VERSION) { + assertFalse(blob != null && blob.length != 0, + "UsageStatsDatabase shouldn't be able to write backups for unknown versions"); + return; + } clearUsageStatsFiles(); @@ -454,9 +461,11 @@ public class UsageStatsDatabaseTest { newDB.readMappingsLocked(); newDB.init(1); // Attempt to restore the usage stats from the backup - newDB.applyRestoredPayload(KEY_USAGE_STATS, blob); - List<IntervalStats> stats = newDB.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, 0, mEndTime, - mIntervalStatsVerifier); + Set<String> restoredApps = newDB.applyRestoredPayload(KEY_USAGE_STATS, blob); + assertTrue(restoredApps.containsAll(prevDBApps), + "List of restored apps does not match list backed-up apps list."); + List<IntervalStats> stats = newDB.queryUsageStats( + UsageStatsManager.INTERVAL_DAILY, 0, mEndTime, mIntervalStatsVerifier); if (version > UsageStatsDatabase.BACKUP_VERSION || version < 1) { assertFalse(stats != null && !stats.isEmpty(), diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java index c735bb7add0a..8a96febcd1e9 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -778,8 +778,7 @@ public class VibratorManagerServiceTest { assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(), service, TEST_TIMEOUT_MILLIS)); - vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), - HAPTIC_FEEDBACK_ATTRS); + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), HAPTIC_FEEDBACK_ATTRS); // Wait before checking it never played a second effect. assertFalse(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1, @@ -793,49 +792,78 @@ public class VibratorManagerServiceTest { } @Test - public void vibrate_withOngoingAlarmVibration_ignoresEffect() throws Exception { + public void vibrate_withNewRepeatingVibration_cancelsOngoingEffect() throws Exception { mockVibrators(1); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); VibratorManagerService service = createSystemReadyService(); VibrationEffect alarmEffect = VibrationEffect.createWaveform( new long[]{10_000, 10_000}, new int[]{128, 255}, -1); - vibrate(service, alarmEffect, new VibrationAttributes.Builder().setUsage( - VibrationAttributes.USAGE_ALARM).build()); + vibrate(service, alarmEffect, ALARM_ATTRS); // VibrationThread will start this vibration async, so wait before checking it started. assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(), service, TEST_TIMEOUT_MILLIS)); - vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), - HAPTIC_FEEDBACK_ATTRS); + VibrationEffect repeatingEffect = VibrationEffect.createWaveform( + new long[]{10_000, 10_000}, new int[]{128, 255}, 1); + vibrate(service, repeatingEffect, NOTIFICATION_ATTRS); - // Wait before checking it never played a second effect. - assertFalse(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1, - service, /* timeout= */ 50)); + // VibrationThread will start this vibration async, so wait before checking it started. + assertTrue(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1, + service, TEST_TIMEOUT_MILLIS)); + + // The second vibration should have recorded that the vibrators were turned on. + verify(mBatteryStatsMock, times(2)).noteVibratorOn(anyInt(), anyLong()); } @Test - public void vibrate_withOngoingRingtoneVibration_ignoresEffect() throws Exception { + public void vibrate_withOngoingHigherImportanceVibration_ignoresEffect() throws Exception { mockVibrators(1); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); VibratorManagerService service = createSystemReadyService(); - VibrationEffect alarmEffect = VibrationEffect.createWaveform( + VibrationEffect effect = VibrationEffect.createWaveform( new long[]{10_000, 10_000}, new int[]{128, 255}, -1); - vibrate(service, alarmEffect, new VibrationAttributes.Builder().setUsage( - VibrationAttributes.USAGE_RINGTONE).build()); + vibrate(service, effect, ALARM_ATTRS); // VibrationThread will start this vibration async, so wait before checking it started. assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(), service, TEST_TIMEOUT_MILLIS)); - vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), - HAPTIC_FEEDBACK_ATTRS); + vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS); // Wait before checking it never played a second effect. assertFalse(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1, service, /* timeout= */ 50)); + + // The second vibration shouldn't have recorded that the vibrators were turned on. + verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong()); + } + + @Test + public void vibrate_withOngoingLowerImportanceVibration_cancelsOngoingEffect() + throws Exception { + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + VibratorManagerService service = createSystemReadyService(); + + VibrationEffect effect = VibrationEffect.createWaveform( + new long[]{10_000, 10_000}, new int[]{128, 255}, -1); + vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS); + + // VibrationThread will start this vibration async, so wait before checking it started. + assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(), + service, TEST_TIMEOUT_MILLIS)); + + vibrate(service, effect, RINGTONE_ATTRS); + + // VibrationThread will start this vibration async, so wait before checking it started. + assertTrue(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1, + service, TEST_TIMEOUT_MILLIS)); + + // The second vibration should have recorded that the vibrators were turned on. + verify(mBatteryStatsMock, times(2)).noteVibratorOn(anyInt(), anyLong()); } @Test @@ -1052,15 +1080,15 @@ public class VibratorManagerServiceTest { IVibrator.CAP_COMPOSE_EFFECTS); VibratorManagerService service = createSystemReadyService(); - vibrate(service, CombinedVibration.startSequential() - .addNext(1, VibrationEffect.createOneShot(100, 125)) - .combine(), NOTIFICATION_ATTRS); - assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 1, - service, TEST_TIMEOUT_MILLIS)); - vibrate(service, VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) .compose(), HAPTIC_FEEDBACK_ATTRS); + assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 1, + service, TEST_TIMEOUT_MILLIS)); + + vibrate(service, CombinedVibration.startSequential() + .addNext(1, VibrationEffect.createOneShot(100, 125)) + .combine(), NOTIFICATION_ATTRS); assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 2, service, TEST_TIMEOUT_MILLIS)); @@ -1070,25 +1098,25 @@ public class VibratorManagerServiceTest { assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 3, service, TEST_TIMEOUT_MILLIS)); + // Ring vibrations have intensity OFF and are not played. vibrate(service, VibrationEffect.createOneShot(100, 125), RINGTONE_ATTRS); assertFalse(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() > 3, - service, TEST_TIMEOUT_MILLIS)); + service, /* timeout= */ 50)); + // Only 3 effects played successfully. assertEquals(3, fakeVibrator.getAllEffectSegments().size()); + // Haptic feedback vibrations will be scaled with SCALE_LOW or none if default is low. + assertEquals(defaultTouchIntensity > Vibrator.VIBRATION_INTENSITY_LOW, + 0.5 > ((PrimitiveSegment) fakeVibrator.getAllEffectSegments().get(0)).getScale()); + // Notification vibrations will be scaled with SCALE_HIGH or none if default is high. assertEquals(defaultNotificationIntensity < Vibrator.VIBRATION_INTENSITY_HIGH, 0.6 < fakeVibrator.getAmplitudes().get(0)); - // Haptic feedback vibrations will be scaled with SCALE_LOW or none if default is low. - assertEquals(defaultTouchIntensity > Vibrator.VIBRATION_INTENSITY_LOW, - 0.5 > ((PrimitiveSegment) fakeVibrator.getAllEffectSegments().get(1)).getScale()); - // Alarm vibration will be scaled with SCALE_NONE. assertEquals(1f, ((PrimitiveSegment) fakeVibrator.getAllEffectSegments().get(2)).getScale(), 1e-5); - - // Ring vibrations have intensity OFF and are not played. } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index fd1536c5c0f1..4550b56f6fd0 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -1622,7 +1622,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(si), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelperSpy.addAutomaticZenRule("android", zenRule, "test"); + // We need the package name to be something that's not "android" so there aren't any + // existing rules under that package. + String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test"); assertNotNull(id); } try { @@ -1632,12 +1634,41 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelperSpy.addAutomaticZenRule("android", zenRule, "test"); + String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test"); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { // yay } + } + @Test + public void testAddAutomaticZenRule_beyondSystemLimit_differentComponents() { + // Make sure the system limit is enforced per-package even with different component provider + // names. + for (int i = 0; i < RULE_LIMIT_PER_PACKAGE; i++) { + ScheduleInfo si = new ScheduleInfo(); + si.startHour = i; + AutomaticZenRule zenRule = new AutomaticZenRule("name" + i, + null, + new ComponentName("android", "ScheduleConditionProvider" + i), + ZenModeConfig.toScheduleConditionId(si), + new ZenPolicy.Builder().build(), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test"); + assertNotNull(id); + } + try { + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName("android", "ScheduleConditionProviderFinal"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + new ZenPolicy.Builder().build(), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test"); + fail("allowed too many rules to be created"); + } catch (IllegalArgumentException e) { + // yay + } } @Test 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 2477f6c41d4f..0c3b270518cf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -29,6 +29,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT; +import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; +import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; import static android.content.pm.ActivityInfo.FLAG_SUPPORTS_PICTURE_IN_PICTURE; import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_ALWAYS; import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_DEFAULT; @@ -182,6 +184,10 @@ public class ActivityRecordTests extends WindowTestsBase { private final String mPackageName = getInstrumentation().getTargetContext().getPackageName(); + private static final int ORIENTATION_CONFIG_CHANGES = + CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT | CONFIG_SCREEN_SIZE + | CONFIG_SMALLEST_SCREEN_SIZE; + @Before public void setUp() throws Exception { setBooted(mAtm); @@ -487,7 +493,7 @@ public class ActivityRecordTests extends WindowTestsBase { public void testSetRequestedOrientationUpdatesConfiguration() throws Exception { final ActivityRecord activity = new ActivityBuilder(mAtm) .setCreateTask(true) - .setConfigChanges(CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT) + .setConfigChanges(ORIENTATION_CONFIG_CHANGES) .build(); activity.setState(RESUMED, "Testing"); @@ -710,7 +716,7 @@ public class ActivityRecordTests extends WindowTestsBase { final ActivityRecord activity = new ActivityBuilder(mAtm) .setCreateTask(true) .setLaunchTaskBehind(true) - .setConfigChanges(CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT) + .setConfigChanges(ORIENTATION_CONFIG_CHANGES) .build(); final Task task = activity.getTask(); activity.setState(STOPPED, "Testing"); @@ -779,7 +785,7 @@ public class ActivityRecordTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } }, 0, 0)); activity.updateOptionsLocked(opts); @@ -1996,7 +2002,8 @@ public class ActivityRecordTests extends WindowTestsBase { any() /* window */, any() /* attrs */, anyInt() /* viewVisibility */, anyInt() /* displayId */, any() /* requestedVisibilities */, any() /* outInputChannel */, - any() /* outInsetsState */, any() /* outActiveControls */); + any() /* outInsetsState */, any() /* outActiveControls */, + any() /* outAttachedFrame */); mAtm.mWindowManager.mStartingSurfaceController .createTaskSnapshotSurface(activity, snapshot); } catch (RemoteException ignored) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 1176786eacc7..c78bc59612d4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -37,6 +37,7 @@ import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED; import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; +import static android.content.pm.ActivityInfo.FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING; import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK; @@ -52,6 +53,11 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.wm.ActivityStarter.canEmbedActivity; +import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; +import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION; +import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_NEW_TASK; +import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_UNTRUSTED_HOST; import static com.android.server.wm.WindowContainer.POSITION_BOTTOM; import static com.android.server.wm.WindowContainer.POSITION_TOP; @@ -59,6 +65,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.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -87,6 +94,7 @@ import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.service.voice.IVoiceInteractionSession; import android.util.Pair; +import android.util.Size; import android.view.Gravity; import android.window.TaskFragmentOrganizerToken; @@ -1171,6 +1179,7 @@ public class ActivityStarterTests extends WindowTestsBase { null /* inTask */, taskFragment); assertFalse(taskFragment.hasChild()); + assertNotNull("Target record must be started on Task.", targetRecord.getParent().asTask()); } @Test @@ -1341,6 +1350,58 @@ public class ActivityStarterTests extends WindowTestsBase { any()); } + @Test + public void testCanEmbedActivity() { + final Size minDimensions = new Size(1000, 1000); + final WindowLayout windowLayout = new WindowLayout(0, 0, 0, 0, 0, + minDimensions.getWidth(), minDimensions.getHeight()); + final ActivityRecord starting = new ActivityBuilder(mAtm) + .setUid(UNIMPORTANT_UID) + .setWindowLayout(windowLayout) + .build(); + + // Task fragment hasn't attached to a task yet. Start activity to a new task. + TaskFragment taskFragment = new TaskFragmentBuilder(mAtm).build(); + final Task task = new TaskBuilder(mSupervisor).build(); + + assertEquals(EMBEDDING_DISALLOWED_NEW_TASK, + canEmbedActivity(taskFragment, starting, task)); + + // Starting activity is going to be started on a task different from task fragment's parent + // task. Start activity to a new task. + task.addChild(taskFragment, POSITION_TOP); + final Task newTask = new TaskBuilder(mSupervisor).build(); + + assertEquals(EMBEDDING_DISALLOWED_NEW_TASK, + canEmbedActivity(taskFragment, starting, newTask)); + + // Make task fragment bounds exceed task bounds. + final Rect taskBounds = task.getBounds(); + taskFragment.setBounds(taskBounds.left, taskBounds.top, taskBounds.right + 1, + taskBounds.bottom + 1); + + assertEquals(EMBEDDING_DISALLOWED_UNTRUSTED_HOST, + canEmbedActivity(taskFragment, starting, task)); + + taskFragment.setBounds(taskBounds); + starting.info.flags |= FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING; + + assertEquals(EMBEDDING_ALLOWED, canEmbedActivity(taskFragment, starting, task)); + + starting.info.flags &= ~FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING; + // Set task fragment's uid as the same as starting activity's uid. + taskFragment.setTaskFragmentOrganizer(mock(TaskFragmentOrganizerToken.class), + UNIMPORTANT_UID, "test"); + + assertEquals(EMBEDDING_ALLOWED, canEmbedActivity(taskFragment, starting, task)); + + // Make task fragment bounds smaller than starting activity's minimum dimensions + taskFragment.setBounds(0, 0, minDimensions.getWidth() - 1, minDimensions.getHeight() - 1); + + assertEquals(EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION, + canEmbedActivity(taskFragment, starting, task)); + } + private static void startActivityInner(ActivityStarter starter, ActivityRecord target, ActivityRecord source, ActivityOptions options, Task inTask, TaskFragment inTaskFragment) { diff --git a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java index 71f19148d616..b5764f54ff92 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java @@ -87,7 +87,7 @@ public class AppChangeTransitionTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java index 8656a4fecef1..f2d6273f2b26 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java @@ -806,7 +806,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { } @Override - public void onAnimationCancelled() throws RemoteException { + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { mFinishedCallback = null; } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java index 436cf36587d8..74154609b22e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java @@ -522,7 +522,7 @@ public class AppTransitionTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { mCancelled = true; } diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index 2c1c38f3bee8..fc41a94d3355 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -181,7 +181,8 @@ public class BackNavigationControllerTests extends WindowTestsBase { Task task = createTopTaskWithActivity(); WindowState appWindow = task.getTopVisibleAppMainWindow(); - WindowOnBackInvokedDispatcher dispatcher = new WindowOnBackInvokedDispatcher(); + WindowOnBackInvokedDispatcher dispatcher = + new WindowOnBackInvokedDispatcher(true /* applicationCallbackEnabled */); doAnswer(invocation -> { appWindow.setOnBackInvokedCallbackInfo(invocation.getArgument(1)); return null; diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index d737963f80e7..40e266c71328 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1699,6 +1699,13 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(displayContent.mPinnedTaskController.isFreezingTaskConfig(pinnedTask)); assertEquals(pinnedActivity.getConfiguration().orientation, displayContent.getConfiguration().orientation); + + // No need to apply rotation if the display ignores orientation request. + doCallRealMethod().when(displayContent).rotationForActivityInDifferentOrientation(any()); + pinnedActivity.mOrientation = SCREEN_ORIENTATION_LANDSCAPE; + displayContent.setIgnoreOrientationRequest(true); + assertEquals(WindowConfiguration.ROTATION_UNDEFINED, + displayContent.rotationForActivityInDifferentOrientation(pinnedActivity)); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java index db3a51ca4791..2956c14155b9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java @@ -39,7 +39,6 @@ import static com.android.server.wm.SizeCompatTests.rotateDisplay; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -211,10 +210,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { assertThat(activityConfigBounds.width()).isEqualTo(activityBounds.width()); assertThat(activityConfigBounds.height()).isEqualTo(activityBounds.height()); assertThat(activitySizeCompatBounds.height()).isEqualTo(newTaskBounds.height()); - final float defaultAspectRatio = mFirstActivity.mWmService.mLetterboxConfiguration - .getDefaultMinAspectRatioForUnresizableApps(); - assertEquals(activitySizeCompatBounds.width(), - newTaskBounds.height() / defaultAspectRatio, 0.5); + assertThat(activitySizeCompatBounds.width()).isEqualTo( + newTaskBounds.height() * newTaskBounds.height() / newTaskBounds.width()); } @Test @@ -234,9 +231,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); assertThat(taskBounds).isEqualTo(dagBounds); assertThat(activityBounds.width()).isEqualTo(dagBounds.width()); - final float defaultAspectRatio = mFirstActivity.mWmService.mLetterboxConfiguration - .getDefaultMinAspectRatioForUnresizableApps(); - assertEquals(activityBounds.height(), dagBounds.width() / defaultAspectRatio, 0.5); + assertThat(activityBounds.height()) + .isEqualTo(dagBounds.width() * dagBounds.width() / dagBounds.height()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java index 204c7e6fb8d4..027f5218f820 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java @@ -43,6 +43,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -210,7 +211,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); adapter.onAnimationCancelled(mMockLeash); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); } @Test @@ -226,7 +227,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { mClock.fastForward(10500); mHandler.timeAdvance(); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), eq(adapter)); } @@ -247,12 +248,12 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { mClock.fastForward(10500); mHandler.timeAdvance(); - verify(mMockRunner, never()).onAnimationCancelled(); + verify(mMockRunner, never()).onAnimationCancelled(anyBoolean()); mClock.fastForward(52500); mHandler.timeAdvance(); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), eq(adapter)); } finally { @@ -264,7 +265,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { public void testZeroAnimations() throws Exception { mController.goodToGo(TRANSIT_OLD_NONE); verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); } @Test @@ -274,7 +275,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); } @Test @@ -316,7 +317,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { win.mActivityRecord.removeImmediately(); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), eq(adapter)); } @@ -574,7 +575,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { // Cancel the wallpaper window animator and ensure the runner is not canceled wallpaperWindowToken.cancelAnimation(); - verify(mMockRunner, never()).onAnimationCancelled(); + verify(mMockRunner, never()).onAnimationCancelled(anyBoolean()); } finally { mDisplayContent.mOpeningApps.clear(); } 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 7f70882a70fc..324e244c46f5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -1471,6 +1471,8 @@ public class SizeCompatTests extends WindowTestsBase { final float fixedOrientationLetterboxAspectRatio = 1.1f; mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio( fixedOrientationLetterboxAspectRatio); + mActivity.mWmService.mLetterboxConfiguration.setDefaultMinAspectRatioForUnresizableApps( + 1.5f); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); final Rect displayBounds = new Rect(mActivity.mDisplayContent.getBounds()); @@ -1496,7 +1498,9 @@ public class SizeCompatTests extends WindowTestsBase { @Test public void testSplitAspectRatioForUnresizablePortraitApps() { // Set up a display in landscape and ignoring orientation request. - setUpDisplaySizeWithApp(1600, 1400); + int screenWidth = 1600; + int screenHeight = 1400; + setUpDisplaySizeWithApp(screenWidth, screenHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mActivity.mWmService.mLetterboxConfiguration .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(true); @@ -1520,6 +1524,7 @@ public class SizeCompatTests extends WindowTestsBase { new TestSplitOrganizer(mAtm, mActivity.getDisplayContent()); // Move activity to split screen which takes half of the screen. mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test"); + organizer.mPrimary.setBounds(0, 0, getExpectedSplitSize(screenWidth), screenHeight); assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode()); assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // Checking that there is no size compat mode. @@ -1528,8 +1533,10 @@ public class SizeCompatTests extends WindowTestsBase { @Test public void testSplitAspectRatioForUnresizableLandscapeApps() { - // Set up a display in landscape and ignoring orientation request. - setUpDisplaySizeWithApp(1400, 1600); + // Set up a display in portrait and ignoring orientation request. + int screenWidth = 1400; + int screenHeight = 1600; + setUpDisplaySizeWithApp(screenWidth, screenHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mActivity.mWmService.mLetterboxConfiguration .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(true); @@ -1553,6 +1560,7 @@ public class SizeCompatTests extends WindowTestsBase { new TestSplitOrganizer(mAtm, mActivity.getDisplayContent()); // Move activity to split screen which takes half of the screen. mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test"); + organizer.mPrimary.setBounds(0, 0, screenWidth, getExpectedSplitSize(screenHeight)); assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode()); assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // Checking that there is no size compat mode. @@ -2071,12 +2079,7 @@ public class SizeCompatTests extends WindowTestsBase { // Activity bounds fill split screen. final Rect primarySplitBounds = new Rect(organizer.mPrimary.getBounds()); final Rect letterboxedBounds = new Rect(mActivity.getBounds()); - // Activity is letterboxed for aspect ratio. - assertEquals(primarySplitBounds.height(), letterboxedBounds.height()); - final float defaultAspectRatio = mActivity.mWmService.mLetterboxConfiguration - .getDefaultMinAspectRatioForUnresizableApps(); - assertEquals(primarySplitBounds.height() / defaultAspectRatio, - letterboxedBounds.width(), 0.5); + assertEquals(primarySplitBounds, letterboxedBounds); } @Test @@ -2618,6 +2621,16 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(newDensity, mActivity.getConfiguration().densityDpi); } + private int getExpectedSplitSize(int dimensionToSplit) { + int dividerWindowWidth = + mActivity.mWmService.mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.docked_stack_divider_thickness); + int dividerInsets = + mActivity.mWmService.mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.docked_stack_divider_insets); + return (dimensionToSplit - (dividerWindowWidth - dividerInsets * 2)) / 2; + } + private void assertHorizontalPositionForDifferentDisplayConfigsForLandscapeActivity( float letterboxHorizontalPositionMultiplier) { // Set up a display in landscape and ignoring orientation request. diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java index 2a9fcb9d070b..7f09606d1c3a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java @@ -762,12 +762,20 @@ public class TaskDisplayAreaTests extends WindowTestsBase { Task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */, 0 /* launchFlags */, candidateTask); - assertSame(rootTask, actualRootTask.getRootTask()); + assertSame(rootTask, actualRootTask); // Verify the launch root task without candidate task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */, 0 /* launchFlags */); - assertSame(adjacentRootTask, actualRootTask.getRootTask()); + assertSame(adjacentRootTask, actualRootTask); + + final Task pinnedTask = createTask( + mDisplayContent, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); + // Verify not adjusting launch target for pinned candidate task + actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED, + ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */, + 0 /* launchFlags */, pinnedTask /* candidateTask */); + assertNull(actualRootTask); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 1c3b869e02d0..e47bcc98b908 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.testing.Assert.assertThrows; @@ -530,7 +531,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { mWindowOrganizerController.mLaunchTaskFragments .put(mFragmentToken, mTaskFragment); mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token); - doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity); + doReturn(EMBEDDING_ALLOWED).when(mTaskFragment).isAllowedToEmbedActivity(activity); clearInvocations(mAtm.mRootWindowContainer); mAtm.getWindowOrganizerController().applyTransaction(mTransaction); @@ -920,7 +921,6 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .setOrganizer(mOrganizer) .setBounds(mTaskFragBounds) .build(); - doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity); mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); // Reparent activity to mTaskFragment, which is smaller than activity's @@ -956,7 +956,6 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .setOrganizer(mOrganizer) .setBounds(mTaskFragBounds) .build(); - doReturn(true).when(mTaskFragment).isAllowedToEmbedActivity(activity); mWindowOrganizerController.mLaunchTaskFragments.put(oldFragToken, oldTaskFrag); mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 228cb65aab38..5f3096356bc5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -475,5 +475,13 @@ public class TaskFragmentTest extends WindowTestsBase { assertFalse(activity0.isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(activity1.isLetterboxedForFixedOrientationAndAspectRatio()); assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation()); + + tf0.setResumedActivity(activity0, "test"); + tf1.setResumedActivity(activity1, "test"); + mDisplayContent.mFocusedApp = activity1; + + // Making the activity0 be the focused activity and ensure the focused app is updated. + activity0.moveFocusableActivityToTop("test"); + assertEquals(activity0, mDisplayContent.mFocusedApp); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index 5743922d0428..1715a295ded3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -979,7 +979,7 @@ public class WindowContainerTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } }, 0, 0, false); adapter.setCallingPidUid(123, 456); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java index ea18e58fe999..739e783e7c1b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java @@ -72,7 +72,7 @@ public class WindowLayoutTests { private static final Insets WATERFALL_INSETS = Insets.of(6, 0, 12, 0); private final WindowLayout mWindowLayout = new WindowLayout(); - private final ClientWindowFrames mOutFrames = new ClientWindowFrames(); + private final ClientWindowFrames mFrames = new ClientWindowFrames(); private WindowManager.LayoutParams mAttrs; private InsetsState mState; @@ -82,7 +82,6 @@ public class WindowLayoutTests { private int mRequestedWidth; private int mRequestedHeight; private InsetsVisibilities mRequestedVisibilities; - private Rect mAttachedWindowFrame; private float mCompatScale; @Before @@ -100,14 +99,14 @@ public class WindowLayoutTests { mRequestedWidth = DISPLAY_WIDTH; mRequestedHeight = DISPLAY_HEIGHT; mRequestedVisibilities = new InsetsVisibilities(); - mAttachedWindowFrame = null; mCompatScale = 1f; + mFrames.attachedFrame = null; } private void computeFrames() { mWindowLayout.computeFrames(mAttrs, mState, mDisplayCutoutSafe, mWindowBounds, mWindowingMode, mRequestedWidth, mRequestedHeight, mRequestedVisibilities, - mAttachedWindowFrame, mCompatScale, mOutFrames); + mCompatScale, mFrames); } private void addDisplayCutout() { @@ -145,9 +144,9 @@ public class WindowLayoutTests { public void defaultParams() { computeFrames(); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.frame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.frame); } @Test @@ -156,9 +155,9 @@ public class WindowLayoutTests { mRequestedHeight = UNSPECIFIED_LENGTH; computeFrames(); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.frame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.frame); } @Test @@ -172,9 +171,9 @@ public class WindowLayoutTests { mAttrs.gravity = Gravity.LEFT | Gravity.TOP; computeFrames(); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame); - assertRect(0, STATUS_BAR_HEIGHT, width, STATUS_BAR_HEIGHT + height, mOutFrames.frame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame); + assertRect(0, STATUS_BAR_HEIGHT, width, STATUS_BAR_HEIGHT + height, mFrames.frame); } @Test @@ -186,11 +185,24 @@ public class WindowLayoutTests { computeFrames(); assertRect(0, top, DISPLAY_WIDTH, DISPLAY_HEIGHT - NAVIGATION_BAR_HEIGHT, - mOutFrames.displayFrame); + mFrames.displayFrame); assertRect(0, top, DISPLAY_WIDTH, DISPLAY_HEIGHT - NAVIGATION_BAR_HEIGHT, - mOutFrames.parentFrame); + mFrames.parentFrame); assertRect(0, top, DISPLAY_WIDTH, DISPLAY_HEIGHT - NAVIGATION_BAR_HEIGHT, - mOutFrames.frame); + mFrames.frame); + } + + @Test + public void attachedFrame() { + final int bottom = (DISPLAY_HEIGHT - STATUS_BAR_HEIGHT - NAVIGATION_BAR_HEIGHT) / 2; + mFrames.attachedFrame = new Rect(0, STATUS_BAR_HEIGHT, DISPLAY_WIDTH, bottom); + mRequestedWidth = UNSPECIFIED_LENGTH; + mRequestedHeight = UNSPECIFIED_LENGTH; + computeFrames(); + + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame); + assertEquals(mFrames.attachedFrame, mFrames.parentFrame); + assertEquals(mFrames.attachedFrame, mFrames.frame); } @Test @@ -198,9 +210,9 @@ public class WindowLayoutTests { mAttrs.setFitInsetsTypes(WindowInsets.Type.statusBars()); computeFrames(); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.displayFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.parentFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.frame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.displayFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.parentFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.frame); } @Test @@ -208,9 +220,9 @@ public class WindowLayoutTests { mAttrs.setFitInsetsTypes(WindowInsets.Type.navigationBars()); computeFrames(); - assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame); - assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame); - assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mOutFrames.frame); + assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame); + assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame); + assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mFrames.frame); } @Test @@ -218,9 +230,9 @@ public class WindowLayoutTests { mAttrs.setFitInsetsTypes(0); computeFrames(); - assertInsetByTopBottom(0, 0, mOutFrames.displayFrame); - assertInsetByTopBottom(0, 0, mOutFrames.parentFrame); - assertInsetByTopBottom(0, 0, mOutFrames.frame); + assertInsetByTopBottom(0, 0, mFrames.displayFrame); + assertInsetByTopBottom(0, 0, mFrames.parentFrame); + assertInsetByTopBottom(0, 0, mFrames.frame); } @Test @@ -228,9 +240,9 @@ public class WindowLayoutTests { mAttrs.setFitInsetsSides(WindowInsets.Side.all()); computeFrames(); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.frame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.frame); } @Test @@ -238,9 +250,9 @@ public class WindowLayoutTests { mAttrs.setFitInsetsSides(WindowInsets.Side.TOP); computeFrames(); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.displayFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.parentFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.frame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.displayFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.parentFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.frame); } @Test @@ -248,9 +260,9 @@ public class WindowLayoutTests { mAttrs.setFitInsetsSides(0); computeFrames(); - assertInsetByTopBottom(0, 0, mOutFrames.displayFrame); - assertInsetByTopBottom(0, 0, mOutFrames.parentFrame); - assertInsetByTopBottom(0, 0, mOutFrames.frame); + assertInsetByTopBottom(0, 0, mFrames.displayFrame); + assertInsetByTopBottom(0, 0, mFrames.parentFrame); + assertInsetByTopBottom(0, 0, mFrames.frame); } @Test @@ -259,9 +271,9 @@ public class WindowLayoutTests { mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(false); computeFrames(); - assertInsetByTopBottom(0, 0, mOutFrames.displayFrame); - assertInsetByTopBottom(0, 0, mOutFrames.parentFrame); - assertInsetByTopBottom(0, 0, mOutFrames.frame); + assertInsetByTopBottom(0, 0, mFrames.displayFrame); + assertInsetByTopBottom(0, 0, mFrames.parentFrame); + assertInsetByTopBottom(0, 0, mFrames.frame); } @Test @@ -271,9 +283,9 @@ public class WindowLayoutTests { mAttrs.setFitInsetsIgnoringVisibility(true); computeFrames(); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.frame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.frame); } @Test @@ -284,9 +296,9 @@ public class WindowLayoutTests { mAttrs.privateFlags |= PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME; computeFrames(); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, IME_HEIGHT, mOutFrames.parentFrame); - assertInsetByTopBottom(STATUS_BAR_HEIGHT, IME_HEIGHT, mOutFrames.frame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, IME_HEIGHT, mFrames.parentFrame); + assertInsetByTopBottom(STATUS_BAR_HEIGHT, IME_HEIGHT, mFrames.frame); } @Test @@ -297,11 +309,11 @@ public class WindowLayoutTests { computeFrames(); assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.displayFrame); + mFrames.displayFrame); assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.parentFrame); + mFrames.parentFrame); assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.frame); + mFrames.frame); } @Test @@ -312,11 +324,11 @@ public class WindowLayoutTests { computeFrames(); assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.displayFrame); + mFrames.displayFrame); assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.parentFrame); + mFrames.parentFrame); assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.frame); + mFrames.frame); } @Test @@ -327,9 +339,9 @@ public class WindowLayoutTests { mAttrs.setFitInsetsTypes(0); computeFrames(); - assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.displayFrame); - assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.parentFrame); - assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.frame); + assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.displayFrame); + assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.parentFrame); + assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.frame); } @Test @@ -344,9 +356,9 @@ public class WindowLayoutTests { mAttrs.privateFlags |= PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; computeFrames(); - assertRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, mOutFrames.displayFrame); - assertRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, mOutFrames.parentFrame); - assertRect(0, 0, DISPLAY_WIDTH, height + DISPLAY_CUTOUT_HEIGHT, mOutFrames.frame); + assertRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, mFrames.displayFrame); + assertRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, mFrames.parentFrame); + assertRect(0, 0, DISPLAY_WIDTH, height + DISPLAY_CUTOUT_HEIGHT, mFrames.frame); } @Test @@ -359,11 +371,11 @@ public class WindowLayoutTests { computeFrames(); assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.displayFrame); + mFrames.displayFrame); assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.parentFrame); + mFrames.parentFrame); assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.frame); + mFrames.frame); } @Test @@ -373,9 +385,9 @@ public class WindowLayoutTests { mAttrs.setFitInsetsTypes(0); computeFrames(); - assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.displayFrame); - assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.parentFrame); - assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.frame); + assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.displayFrame); + assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.parentFrame); + assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.frame); } @Test @@ -386,11 +398,11 @@ public class WindowLayoutTests { computeFrames(); assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.displayFrame); + mFrames.displayFrame); assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.parentFrame); + mFrames.parentFrame); assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0, - mOutFrames.frame); + mFrames.frame); } @Test @@ -400,8 +412,8 @@ public class WindowLayoutTests { mAttrs.setFitInsetsTypes(0); computeFrames(); - assertInsetByTopBottom(0, 0, mOutFrames.displayFrame); - assertInsetByTopBottom(0, 0, mOutFrames.parentFrame); - assertInsetByTopBottom(0, 0, mOutFrames.frame); + assertInsetByTopBottom(0, 0, mFrames.displayFrame); + assertInsetByTopBottom(0, 0, mFrames.parentFrame); + assertInsetByTopBottom(0, 0, mFrames.frame); } } 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 a0c20c2c6b68..1a64f5e3a356 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -49,6 +49,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.pm.PackageManager; +import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; @@ -60,6 +61,7 @@ import android.view.InsetsState; import android.view.InsetsVisibilities; import android.view.View; import android.view.WindowManager; +import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; @@ -281,7 +283,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { mWm.addWindow(session, new TestIWindow(), params, View.VISIBLE, DEFAULT_DISPLAY, UserHandle.USER_SYSTEM, new InsetsVisibilities(), null, new InsetsState(), - new InsetsSourceControl[0]); + new InsetsSourceControl[0], new Rect()); verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener(any(), any(), anyInt(), anyInt(), any()); @@ -316,4 +318,76 @@ public class WindowManagerServiceTests extends WindowTestsBase { verify(mWm.mInputManager).setInTouchMode( !currentTouchMode, callingPid, callingUid, /* hasPermission= */ false); } + + @Test + public void testGetTaskWindowContainerTokenForLaunchCookie_nullCookie() { + WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(null); + assertThat(wct).isNull(); + } + + @Test + public void testGetTaskWindowContainerTokenForLaunchCookie_invalidCookie() { + Binder cookie = new Binder("test cookie"); + WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie); + assertThat(wct).isNull(); + + final ActivityRecord testActivity = new ActivityBuilder(mAtm) + .setCreateTask(true) + .build(); + + wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie); + assertThat(wct).isNull(); + } + + @Test + public void testGetTaskWindowContainerTokenForLaunchCookie_validCookie() { + final Binder cookie = new Binder("ginger cookie"); + final WindowContainerToken launchRootTask = mock(WindowContainerToken.class); + setupActivityWithLaunchCookie(cookie, launchRootTask); + + WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie); + assertThat(wct).isEqualTo(launchRootTask); + } + + @Test + public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies() { + final Binder cookie1 = new Binder("ginger cookie"); + final WindowContainerToken launchRootTask1 = mock(WindowContainerToken.class); + setupActivityWithLaunchCookie(cookie1, launchRootTask1); + + setupActivityWithLaunchCookie(new Binder("choc chip cookie"), + mock(WindowContainerToken.class)); + + setupActivityWithLaunchCookie(new Binder("peanut butter cookie"), + mock(WindowContainerToken.class)); + + WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie1); + assertThat(wct).isEqualTo(launchRootTask1); + } + + @Test + public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies_noneValid() { + setupActivityWithLaunchCookie(new Binder("ginger cookie"), + mock(WindowContainerToken.class)); + + setupActivityWithLaunchCookie(new Binder("choc chip cookie"), + mock(WindowContainerToken.class)); + + setupActivityWithLaunchCookie(new Binder("peanut butter cookie"), + mock(WindowContainerToken.class)); + + WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie( + new Binder("some other cookie")); + assertThat(wct).isNull(); + } + + private void setupActivityWithLaunchCookie(IBinder launchCookie, WindowContainerToken wct) { + final WindowContainer.RemoteToken remoteToken = mock(WindowContainer.RemoteToken.class); + when(remoteToken.toWindowContainerToken()).thenReturn(wct); + final ActivityRecord testActivity = new ActivityBuilder(mAtm) + .setCreateTask(true) + .build(); + testActivity.mLaunchCookie = launchCookie; + testActivity.getTask().mRemoteToken = remoteToken; + } } diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java index cc33f88d396d..26a1e9d8af11 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java +++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java @@ -16,6 +16,7 @@ package com.android.server.usage; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.usage.TimeSparseArray; import android.app.usage.UsageEvents; @@ -24,6 +25,7 @@ import android.app.usage.UsageStatsManager; import android.os.Build; import android.os.SystemProperties; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.AtomicFile; import android.util.Slog; import android.util.SparseArray; @@ -55,8 +57,11 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; /** * Provides an interface to query for UsageStat data from a Protocol Buffer database. @@ -1252,6 +1257,10 @@ public class UsageStatsDatabase { Slog.wtf(TAG, "Attempting to backup UsageStats as XML with version " + version); return null; } + if (version < 1 || version > BACKUP_VERSION) { + Slog.wtf(TAG, "Attempting to backup UsageStats with an unknown version: " + version); + return null; + } synchronized (mLock) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); if (KEY_USAGE_STATS.equals(key)) { @@ -1300,14 +1309,26 @@ public class UsageStatsDatabase { } return baos.toByteArray(); } + } + /** + * Updates the set of packages given to only include those that have been used within the + * given timeframe (as defined by {@link UsageStats#getLastTimePackageUsed()}). + */ + private void calculatePackagesUsedWithinTimeframe( + IntervalStats stats, Set<String> packagesList, long timeframeMs) { + for (UsageStats stat : stats.packageStats.values()) { + if (stat.getLastTimePackageUsed() > timeframeMs) { + packagesList.add(stat.mPackageName); + } + } } /** * @hide */ @VisibleForTesting - public void applyRestoredPayload(String key, byte[] payload) { + public @NonNull Set<String> applyRestoredPayload(String key, byte[] payload) { synchronized (mLock) { if (KEY_USAGE_STATS.equals(key)) { // Read stats files for the current device configs @@ -1320,12 +1341,15 @@ public class UsageStatsDatabase { IntervalStats yearlyConfigSource = getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY); + final Set<String> packagesRestored = new ArraySet<>(); try { DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload)); int backupDataVersion = in.readInt(); // Can't handle this backup set - if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) return; + if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) { + return packagesRestored; + } // Delete all stats files // Do this after reading version and before actually restoring @@ -1333,10 +1357,14 @@ public class UsageStatsDatabase { deleteDirectoryContents(mIntervalDirs[i]); } + // 90 days before today in epoch + final long timeframe = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(90); int fileCount = in.readInt(); for (int i = 0; i < fileCount; i++) { IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), backupDataVersion); + calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe); + packagesRestored.addAll(stats.packageStats.keySet()); stats = mergeStats(stats, dailyConfigSource); putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats); } @@ -1345,6 +1373,7 @@ public class UsageStatsDatabase { for (int i = 0; i < fileCount; i++) { IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), backupDataVersion); + calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe); stats = mergeStats(stats, weeklyConfigSource); putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats); } @@ -1353,6 +1382,7 @@ public class UsageStatsDatabase { for (int i = 0; i < fileCount; i++) { IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), backupDataVersion); + calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe); stats = mergeStats(stats, monthlyConfigSource); putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats); } @@ -1361,6 +1391,7 @@ public class UsageStatsDatabase { for (int i = 0; i < fileCount; i++) { IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), backupDataVersion); + calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe); stats = mergeStats(stats, yearlyConfigSource); putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats); } @@ -1370,7 +1401,9 @@ public class UsageStatsDatabase { } finally { indexFilesLocked(); } + return packagesRestored; } + return Collections.EMPTY_SET; } } diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index ef13cd964f6c..f595c3de104e 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -3034,7 +3034,8 @@ public class UsageStatsService extends SystemService implements if (userStats == null) { return; // user was stopped or removed } - userStats.applyRestoredPayload(key, payload); + final Set<String> restoredApps = userStats.applyRestoredPayload(key, payload); + mAppStandby.restoreAppsToRare(restoredApps, user); } } } diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java index c609add0b5d7..34c6c1623eec 100644 --- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java @@ -63,6 +63,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Set; /** * A per-user UsageStatsService. All methods are meant to be called with the main lock held @@ -1374,8 +1375,8 @@ class UserUsageStatsService { return mDatabase.getBackupPayload(key); } - void applyRestoredPayload(String key, byte[] payload){ + Set<String> applyRestoredPayload(String key, byte[] payload) { checkAndGetTimeLocked(); - mDatabase.applyRestoredPayload(key, payload); + return mDatabase.applyRestoredPayload(key, payload); } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 434663bd8167..25db81fa2667 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -31,11 +31,18 @@ import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERV import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_UNKNOWN_TIMEOUT; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__AUDIO_SERVICE_DIED; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__SCHEDULE; +import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__APP_REQUEST_UPDATE_STATE; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_UPDATE_STATE_AFTER_TIMEOUT; +import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECTED; +import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION; +import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_REJECTED; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__ON_CONNECTED; +import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__ON_DISCONNECTED; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_BIND_SERVICE; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_BIND_SERVICE_FAIL; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_UPDATE_STATE; +import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__START_EXTERNAL_SOURCE_DETECTION; +import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__START_SOFTWARE_DETECTION; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED; @@ -146,6 +153,13 @@ final class HotwordDetectionConnection { private static final int METRICS_KEYPHRASE_TRIGGERED_REJECT_UNEXPECTED_CALLBACK = HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECT_UNEXPECTED_CALLBACK; + private static final int METRICS_EXTERNAL_SOURCE_DETECTED = + HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECTED; + private static final int METRICS_EXTERNAL_SOURCE_REJECTED = + HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_REJECTED; + private static final int METRICS_EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION = + HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION; + private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool(); // TODO: This may need to be a Handler(looper) private final ScheduledExecutorService mScheduledExecutorService = @@ -382,6 +396,10 @@ final class HotwordDetectionConnection { } void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory) { + HotwordMetricsLogger.writeDetectorEvent(mDetectorType, + HOTWORD_DETECTOR_EVENTS__EVENT__APP_REQUEST_UPDATE_STATE, + mVoiceInteractionServiceUid); + // Prevent doing the init late, so restart is handled equally to a clean process start. // TODO(b/191742511): this logic needs a test if (!mUpdateStateAfterStartFinished.get() @@ -422,14 +440,23 @@ final class HotwordDetectionConnection { Slog.d(TAG, "onDetected"); } synchronized (mLock) { + HotwordMetricsLogger.writeKeyphraseTriggerEvent( + mDetectorType, + HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED); if (!mPerformingSoftwareHotwordDetection) { Slog.i(TAG, "Hotword detection has already completed"); + HotwordMetricsLogger.writeKeyphraseTriggerEvent( + mDetectorType, + METRICS_KEYPHRASE_TRIGGERED_DETECT_UNEXPECTED_CALLBACK); return; } mPerformingSoftwareHotwordDetection = false; try { enforcePermissionsForDataDelivery(); } catch (SecurityException e) { + HotwordMetricsLogger.writeKeyphraseTriggerEvent( + mDetectorType, + METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION); mSoftwareCallback.onError(); return; } @@ -449,6 +476,9 @@ final class HotwordDetectionConnection { if (DEBUG) { Slog.wtf(TAG, "onRejected"); } + HotwordMetricsLogger.writeKeyphraseTriggerEvent( + mDetectorType, + HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED); // onRejected isn't allowed here, and we are not expecting it. } }; @@ -460,6 +490,9 @@ final class HotwordDetectionConnection { null, null, internalCallback)); + HotwordMetricsLogger.writeDetectorEvent(mDetectorType, + HOTWORD_DETECTOR_EVENTS__EVENT__START_SOFTWARE_DETECTION, + mVoiceInteractionServiceUid); } public void startListeningFromExternalSource( @@ -891,6 +924,9 @@ final class HotwordDetectionConnection { @Override public void onRejected(HotwordRejectedResult result) throws RemoteException { + HotwordMetricsLogger.writeDetectorEvent(mDetectorType, + METRICS_EXTERNAL_SOURCE_REJECTED, + mVoiceInteractionServiceUid); mScheduledExecutorService.schedule( () -> { bestEffortClose(serviceAudioSink, audioSource); @@ -912,6 +948,9 @@ final class HotwordDetectionConnection { @Override public void onDetected(HotwordDetectedResult triggerResult) throws RemoteException { + HotwordMetricsLogger.writeDetectorEvent(mDetectorType, + METRICS_EXTERNAL_SOURCE_DETECTED, + mVoiceInteractionServiceUid); mScheduledExecutorService.schedule( () -> { bestEffortClose(serviceAudioSink, audioSource); @@ -922,6 +961,9 @@ final class HotwordDetectionConnection { try { enforcePermissionsForDataDelivery(); } catch (SecurityException e) { + HotwordMetricsLogger.writeDetectorEvent(mDetectorType, + METRICS_EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION, + mVoiceInteractionServiceUid); callback.onError(); return; } @@ -942,6 +984,9 @@ final class HotwordDetectionConnection { // A copy of this has been created and passed to the hotword validator bestEffortClose(serviceAudioSource); }); + HotwordMetricsLogger.writeDetectorEvent(mDetectorType, + HOTWORD_DETECTOR_EVENTS__EVENT__START_EXTERNAL_SOURCE_DETECTION, + mVoiceInteractionServiceUid); } private class ServiceConnectionFactory { @@ -1002,7 +1047,12 @@ final class HotwordDetectionConnection { return; } mIsBound = connected; - if (connected && !mIsLoggedFirstConnect) { + + if (!connected) { + HotwordMetricsLogger.writeDetectorEvent(mDetectorType, + HOTWORD_DETECTOR_EVENTS__EVENT__ON_DISCONNECTED, + mVoiceInteractionServiceUid); + } else if (!mIsLoggedFirstConnect) { mIsLoggedFirstConnect = true; HotwordMetricsLogger.writeDetectorEvent(mDetectorType, HOTWORD_DETECTOR_EVENTS__EVENT__ON_CONNECTED, diff --git a/telecomm/java/android/telecom/InCallService.java b/telecomm/java/android/telecom/InCallService.java index 0ddd52dfc76d..64a86db38396 100644 --- a/telecomm/java/android/telecom/InCallService.java +++ b/telecomm/java/android/telecom/InCallService.java @@ -22,6 +22,7 @@ import android.annotation.SystemApi; import android.app.Service; import android.app.UiModeManager; import android.bluetooth.BluetoothDevice; +import android.content.ComponentName; import android.content.Intent; import android.hardware.camera2.CameraManager; import android.net.Uri; @@ -47,7 +48,7 @@ import java.util.List; * in a call. It also provides the user with a means to initiate calls and see a history of calls * on their device. A device is bundled with a system provided default dialer/phone app. The user * may choose a single app to take over this role from the system app. An app which wishes to - * fulfill one this role uses the {@link android.app.role.RoleManager} to request that they fill the + * fulfill this role uses the {@link android.app.role.RoleManager} to request that they fill the * {@link android.app.role.RoleManager#ROLE_DIALER} role. * <p> * The default phone app provides a user interface while the device is in a call, and the device is @@ -63,13 +64,23 @@ import java.util.List; * UI, as well as an ongoing call UI.</li> * </ul> * <p> - * Note: If the app filling the {@link android.app.role.RoleManager#ROLE_DIALER} crashes during - * {@link InCallService} binding, the Telecom framework will automatically fall back to using the - * dialer app pre-loaded on the device. The system will display a notification to the user to let - * them know that the app has crashed and that their call was continued using the pre-loaded dialer - * app. + * Note: If the app filling the {@link android.app.role.RoleManager#ROLE_DIALER} returns a + * {@code null} {@link InCallService} during binding, the Telecom framework will automatically fall + * back to using the dialer app preloaded on the device. The system will display a notification to + * the user to let them know that their call was continued using the preloaded dialer app. Your + * app should never return a {@code null} binding; doing so means it does not fulfil the + * requirements of {@link android.app.role.RoleManager#ROLE_DIALER}. * <p> - * The pre-loaded dialer will ALWAYS be used when the user places an emergency call, even if your + * Note: If your app fills {@link android.app.role.RoleManager#ROLE_DIALER} and makes changes at + * runtime which cause it to no longer fulfil the requirements of this role, + * {@link android.app.role.RoleManager} will automatically remove your app from the role and close + * your app. For example, if you use + * {@link android.content.pm.PackageManager#setComponentEnabledSetting(ComponentName, int, int)} to + * programmatically disable the {@link InCallService} your app declares in its manifest, your app + * will no longer fulfil the requirements expected of + * {@link android.app.role.RoleManager#ROLE_DIALER}. + * <p> + * The preloaded dialer will ALWAYS be used when the user places an emergency call, even if your * app fills the {@link android.app.role.RoleManager#ROLE_DIALER} role. To ensure an optimal * experience when placing an emergency call, the default dialer should ALWAYS use * {@link android.telecom.TelecomManager#placeCall(Uri, Bundle)} to place calls (including diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt index 0e5a1775bd6a..315c40ffa9ba 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt @@ -21,8 +21,10 @@ import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.traces.region.RegionSubject import com.android.server.wm.traces.common.FlickerComponentName -val LAUNCHER_COMPONENT = FlickerComponentName("com.google.android.apps.nexuslauncher", - "com.google.android.apps.nexuslauncher.NexusLauncherActivity") +val LAUNCHER_COMPONENT = FlickerComponentName( + "com.google.android.apps.nexuslauncher", + "com.google.android.apps.nexuslauncher.NexusLauncherActivity" +) /** * Checks that [FlickerComponentName.STATUS_BAR] window is visible and above the app windows in @@ -110,9 +112,9 @@ fun FlickerTestParameter.statusBarLayerIsVisible() { fun FlickerTestParameter.navBarLayerPositionStart() { assertLayersStart { val display = this.entry.displays.minByOrNull { it.id } - ?: throw RuntimeException("There is no display!") + ?: throw RuntimeException("There is no display!") this.visibleRegion(FlickerComponentName.NAV_BAR) - .coversExactly(WindowUtils.getNavigationBarPosition(display)) + .coversExactly(WindowUtils.getNavigationBarPosition(display, isGesturalNavigation)) } } @@ -123,9 +125,9 @@ fun FlickerTestParameter.navBarLayerPositionStart() { fun FlickerTestParameter.navBarLayerPositionEnd() { assertLayersEnd { val display = this.entry.displays.minByOrNull { it.id } - ?: throw RuntimeException("There is no display!") + ?: throw RuntimeException("There is no display!") this.visibleRegion(FlickerComponentName.NAV_BAR) - .coversExactly(WindowUtils.getNavigationBarPosition(display)) + .coversExactly(WindowUtils.getNavigationBarPosition(display, isGesturalNavigation)) } } @@ -244,11 +246,11 @@ fun FlickerTestParameter.replacesLayer( assertLayersStart { this.isVisible(originalLayer) - .isInvisible(newLayer) + .isInvisible(newLayer) } assertLayersEnd { this.isInvisible(originalLayer) - .isVisible(newLayer) + .isVisible(newLayer) } } |