diff options
62 files changed, 2502 insertions, 429 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index bbdcd6de9cb0..5efa74779c06 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -34768,7 +34768,7 @@ package android.os { method @FlaggedApi("android.os.message_queue_testability") public boolean isBlockedOnSyncBarrier(); method public android.os.Message next(); method @FlaggedApi("android.os.message_queue_testability") @Nullable public Long peekWhen(); - method @FlaggedApi("android.os.message_queue_testability") @Nullable public android.os.Message pop(); + method @FlaggedApi("android.os.message_queue_testability") @Nullable public android.os.Message poll(); method public void recycle(android.os.Message); method public void release(); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index dfbd97879f0f..9590e1ab19c9 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -12713,14 +12713,14 @@ package android.security.authenticationpolicy { } @FlaggedApi("android.security.secure_lockdown") public final class DisableSecureLockDeviceParams implements android.os.Parcelable { - ctor public DisableSecureLockDeviceParams(@NonNull String); + ctor public DisableSecureLockDeviceParams(@NonNull CharSequence); method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.security.authenticationpolicy.DisableSecureLockDeviceParams> CREATOR; } @FlaggedApi("android.security.secure_lockdown") public final class EnableSecureLockDeviceParams implements android.os.Parcelable { - ctor public EnableSecureLockDeviceParams(@NonNull String); + ctor public EnableSecureLockDeviceParams(@NonNull CharSequence); method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.security.authenticationpolicy.EnableSecureLockDeviceParams> CREATOR; diff --git a/core/java/android/app/BroadcastStickyCache.java b/core/java/android/app/BroadcastStickyCache.java index 0b6cf59876d2..1f627794a0f4 100644 --- a/core/java/android/app/BroadcastStickyCache.java +++ b/core/java/android/app/BroadcastStickyCache.java @@ -214,7 +214,8 @@ public class BroadcastStickyCache { // We only need 1 entry per cache but just to be on the safer side we are choosing 32 // although we don't expect more than 1. sActionConfigMap.put(action, - new Config(32, IpcDataCache.MODULE_SYSTEM, sActionApiNameMap.get(action))); + new Config(32, IpcDataCache.MODULE_SYSTEM, + sActionApiNameMap.get(action)).cacheNulls(true)); } return sActionConfigMap.get(action); diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java index 469521668d25..a04f96a9f6e3 100644 --- a/core/java/android/app/jank/JankTracker.java +++ b/core/java/android/app/jank/JankTracker.java @@ -29,6 +29,7 @@ import android.view.ViewTreeObserver; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; +import java.util.HashMap; /** * This class is responsible for registering callbacks that will receive JankData batches. @@ -174,6 +175,15 @@ public class JankTracker { } /** + * Retrieve all pending jank stats before they are logged, this is intended for testing + * purposes only. + */ + @VisibleForTesting + public HashMap<String, JankDataProcessor.PendingJankStat> getPendingJankStats() { + return mJankDataProcessor.getPendingJankStats(); + } + + /** * Only intended to be used by tests, the runnable that registers the listeners may not run * in time for tests to pass. This forces them to run immediately. */ @@ -192,7 +202,11 @@ public class JankTracker { */ } - private boolean shouldTrack() { + /** + * Returns whether jank tracking is enabled or not. + */ + @VisibleForTesting + public boolean shouldTrack() { return mTrackingEnabled && mListenersRegistered; } diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index 40de2985f68a..67ad4594599f 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -37,36 +37,44 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.Intent.FilterComparison; import android.content.IntentSender; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.ShortcutInfo; import android.graphics.Rect; +import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerExecutor; import android.os.HandlerThread; +import android.os.IBinder; import android.os.Looper; import android.os.PersistableBundle; import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; +import android.util.ArrayMap; import android.util.DisplayMetrics; import android.util.Log; +import android.util.Pair; import android.widget.RemoteViews; import com.android.internal.appwidget.IAppWidgetService; import com.android.internal.os.BackgroundThread; import com.android.internal.util.FunctionalUtils; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Updates AppWidget state; gets information about installed AppWidget providers and other @@ -592,6 +600,8 @@ public class AppWidgetManager { private boolean mHasPostedLegacyLists = false; + private @NonNull ServiceCollectionCache mServiceCollectionCache; + /** * Get the AppWidgetManager instance to use for the supplied {@link android.content.Context * Context} object. @@ -612,6 +622,7 @@ public class AppWidgetManager { mPackageName = context.getOpPackageName(); mService = service; mDisplayMetrics = context.getResources().getDisplayMetrics(); + mServiceCollectionCache = new ServiceCollectionCache(context, /* timeout= */ 5000L); if (mService == null) { return; } @@ -649,7 +660,7 @@ public class AppWidgetManager { final RemoteViews viewsCopy = new RemoteViews(original); Runnable updateWidgetWithTask = () -> { try { - viewsCopy.collectAllIntents(mMaxBitmapMemory).get(); + viewsCopy.collectAllIntents(mMaxBitmapMemory, mServiceCollectionCache).get(); action.acceptOrThrow(viewsCopy); } catch (Exception e) { Log.e(TAG, failureMsg, e); @@ -1629,4 +1640,106 @@ public class AppWidgetManager { thread.start(); return thread.getThreadHandler(); } + + /** + * @hide + */ + public static class ServiceCollectionCache { + + private final Context mContext; + private final Handler mHandler; + private final long mTimeOut; + + private final Map<FilterComparison, ConnectionTask> mActiveConnections = + new ArrayMap<>(); + + public ServiceCollectionCache(Context context, long timeOut) { + mContext = context; + mHandler = new Handler(BackgroundThread.getHandler().getLooper()); + mTimeOut = timeOut; + } + + /** + * Connect to the service indicated by the {@code Intent}, and consume the binder on the + * specified executor + */ + public void connectAndConsume(Intent intent, Consumer<IBinder> task, Executor executor) { + mHandler.post(() -> connectAndConsumeInner(intent, task, executor)); + } + + private void connectAndConsumeInner(Intent intent, Consumer<IBinder> task, + Executor executor) { + ConnectionTask activeConnection = mActiveConnections.computeIfAbsent( + new FilterComparison(intent), ConnectionTask::new); + activeConnection.add(task, executor); + } + + private class ConnectionTask implements ServiceConnection { + + private final Runnable mDestroyAfterTimeout = this::onDestroyTimeout; + private final ArrayDeque<Pair<Consumer<IBinder>, Executor>> mTaskQueue = + new ArrayDeque<>(); + + private boolean mOnDestroyTimeout = false; + private IBinder mIBinder; + + ConnectionTask(@NonNull FilterComparison filter) { + mContext.bindService(filter.getIntent(), + Context.BindServiceFlags.of(Context.BIND_AUTO_CREATE), + mHandler::post, + this); + } + + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + mIBinder = iBinder; + mHandler.post(this::handleNext); + } + + @Override + public void onNullBinding(ComponentName name) { + // Use an empty binder, follow up tasks will handle the failure + onServiceConnected(name, new Binder()); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { } + + void add(Consumer<IBinder> task, Executor executor) { + mTaskQueue.add(Pair.create(task, executor)); + if (mOnDestroyTimeout) { + // If we are waiting for timeout, cancel it and execute the next task + handleNext(); + } + } + + private void handleNext() { + mHandler.removeCallbacks(mDestroyAfterTimeout); + Pair<Consumer<IBinder>, Executor> next = mTaskQueue.pollFirst(); + if (next != null) { + mOnDestroyTimeout = false; + next.second.execute(() -> { + next.first.accept(mIBinder); + mHandler.post(this::handleNext); + }); + } else { + // Finished all tasks, start a timeout to unbind this service + mOnDestroyTimeout = true; + mHandler.postDelayed(mDestroyAfterTimeout, mTimeOut); + } + } + + /** + * Called after we have waited for {@link #mTimeOut} after the last task is finished + */ + private void onDestroyTimeout() { + if (!mTaskQueue.isEmpty()) { + handleNext(); + return; + } + mContext.unbindService(this); + mActiveConnections.values().remove(this); + } + } + } } diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java index 42028f67f400..e5717ac87d88 100644 --- a/core/java/android/hardware/radio/ProgramSelector.java +++ b/core/java/android/hardware/radio/ProgramSelector.java @@ -43,22 +43,20 @@ import java.util.stream.Stream; * <li>DAB channel info</li> * </ui> * - * <p>The primary ID uniquely identifies a station and can be used for equality - * check. The secondary IDs are supplementary and can speed up tuning process, - * but the primary ID is sufficient (ie. after a full band scan). - * - * <p>Two selectors with different secondary IDs, but the same primary ID are - * considered equal. In particular, secondary IDs vector may get updated for + * <p>Except for DAB radio, two selectors with different secondary IDs, but the same primary + * ID are considered equal. In particular, secondary IDs vector may get updated for * an entry on the program list (ie. when a better frequency for a given - * station is found). + * station is found). For DAB radio, two selectors with the same primary ID and the same + * DAB frequency and DAB ensemble secondary IDs (if exist) are considered equal. * * <p>The primaryId of a given programType MUST be of a specific type: * <ui> - * <li>AM, FM: RDS_PI if the station broadcasts RDS, AMFM_FREQUENCY otherwise;</li> - * <li>AM_HD, FM_HD: HD_STATION_ID_EXT;</li> - * <li>DAB: DAB_SIDECC;</li> - * <li>DRMO: DRMO_SERVICE_ID;</li> - * <li>SXM: SXM_SERVICE_ID;</li> + * <li>AM, FM: {@link #IDENTIFIER_TYPE_RDS_PI} if the station broadcasts RDS, + * {@link #IDENTIFIER_TYPE_AMFM_FREQUENCY} otherwise;</li> + * <li>AM_HD, FM_HD: {@link #IDENTIFIER_TYPE_HD_STATION_ID_EXT};</li> + * <li>DAB: {@link #IDENTIFIER_TYPE_DAB_SID_EXT} or + * {@link #IDENTIFIER_TYPE_DAB_DMB_SID_EXT};</li> + * <li>DRMO: {@link #IDENTIFIER_TYPE_DRMO_SERVICE_ID};</li> * <li>VENDOR: VENDOR_PRIMARY.</li> * </ui> * @hide @@ -597,9 +595,9 @@ public final class ProgramSelector implements Parcelable { * negatives. In particular, it may be way off for certain regions. * The main purpose is to avoid passing improper units, ie. MHz instead of kHz. * - * @param isAm true, if AM, false if FM. + * @param isAm {@code true}, if AM, {@code false} if FM. * @param frequencyKhz the frequency in kHz. - * @return true, if the frequency is roughly valid. + * @return {@code true}, if the frequency is roughly valid. */ private static boolean isValidAmFmFrequency(boolean isAm, int frequencyKhz) { if (isAm) { @@ -785,8 +783,8 @@ public final class ProgramSelector implements Parcelable { * ProgramLists for category entries. * * @see ProgramList.Filter#areCategoriesIncluded - * @return False if this identifier's type is not tunable (e.g. DAB ensemble or - * vendor-specified type). True otherwise. + * @return {@link false} if this identifier's type is not tunable (e.g. DAB ensemble or + * vendor-specified type). {@link true} otherwise. */ public boolean isCategoryType() { return (mType >= IDENTIFIER_TYPE_VENDOR_START && mType <= IDENTIFIER_TYPE_VENDOR_END) diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java index ce56a4f63a75..b509c7a441d3 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -1272,7 +1272,7 @@ public final class MessageQueue { return true; } - private Message legacyPeekOrPop(boolean peek) { + private Message legacyPeekOrPoll(boolean peek) { synchronized (this) { // Try to retrieve the next message. Return if found. final long now = SystemClock.uptimeMillis(); @@ -1331,7 +1331,7 @@ public final class MessageQueue { if (mUseConcurrent) { ret = nextMessage(true); } else { - ret = legacyPeekOrPop(true); + ret = legacyPeekOrPoll(true); } return ret != null ? ret.when : null; } @@ -1344,12 +1344,12 @@ public final class MessageQueue { */ @SuppressLint("VisiblySynchronized") // Legacy MessageQueue synchronizes on this @Nullable - Message popForTest() { + Message pollForTest() { throwIfNotTest(); if (mUseConcurrent) { return nextMessage(false); } else { - return legacyPeekOrPop(false); + return legacyPeekOrPoll(false); } } diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java index 576c4cc5b442..de0259eb1e36 100644 --- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java +++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java @@ -1102,7 +1102,7 @@ public final class MessageQueue { */ @SuppressLint("VisiblySynchronized") // Legacy MessageQueue synchronizes on this @Nullable - Message popForTest() { + Message pollForTest() { throwIfNotTest(); return nextMessage(false); } diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java index 10d090444c59..5e1e1fdca5c8 100644 --- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java +++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java @@ -740,7 +740,7 @@ public final class MessageQueue { return true; } - private Message legacyPeekOrPop(boolean peek) { + private Message legacyPeekOrPoll(boolean peek) { synchronized (this) { // Try to retrieve the next message. Return if found. final long now = SystemClock.uptimeMillis(); @@ -795,7 +795,7 @@ public final class MessageQueue { @SuppressLint("VisiblySynchronized") // Legacy MessageQueue synchronizes on this Long peekWhenForTest() { throwIfNotTest(); - Message ret = legacyPeekOrPop(true); + Message ret = legacyPeekOrPoll(true); return ret != null ? ret.when : null; } @@ -807,9 +807,9 @@ public final class MessageQueue { */ @SuppressLint("VisiblySynchronized") // Legacy MessageQueue synchronizes on this @Nullable - Message popForTest() { + Message pollForTest() { throwIfNotTest(); - return legacyPeekOrPop(false); + return legacyPeekOrPoll(false); } /** diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java index 6431f3ce73f3..e2169925fdd3 100644 --- a/core/java/android/os/TestLooperManager.java +++ b/core/java/android/os/TestLooperManager.java @@ -95,8 +95,8 @@ public class TestLooperManager { } /** - * Returns the next message that should be executed by this queue, and removes it from the - * queue. If the queue is empty or no messages are deliverable, returns null. + * Retrieves and removes the next message that should be executed by this queue. + * If the queue is empty or no messages are deliverable, returns null. * This method never blocks. * * <p>Callers should always call {@link #recycle(Message)} on the message when all interactions @@ -104,15 +104,19 @@ public class TestLooperManager { */ @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY) @Nullable - public Message pop() { + public Message poll() { checkReleased(); - return mQueue.popForTest(); + return mQueue.pollForTest(); } /** - * Returns the values of {@link Message#when} of the next message that should be executed by - * this queue. If the queue is empty or no messages are deliverable, returns null. + * Retrieves, but does not remove, the values of {@link Message#when} of next message that + * should be executed by this queue. + * If the queue is empty or no messages are deliverable, returns null. * This method never blocks. + * + * <p>Callers should always call {@link #recycle(Message)} on the message when all interactions + * with it have completed. */ @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY) @SuppressWarnings("AutoBoxing") // box the primitive long, or return null to indicate no value diff --git a/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.java b/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.java index 64a3f0f60f96..568a6d7cb923 100644 --- a/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.java +++ b/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.java @@ -38,23 +38,30 @@ public final class DisableSecureLockDeviceParams implements Parcelable { /** * Client message associated with the request to disable secure lock on the device. This message * will be shown on the device when secure lock mode is disabled. + * + * Since this text is shown in a restricted lockscreen state, typeface properties such as color, + * font weight, or other formatting may not be honored. */ - private final @NonNull String mMessage; + private final @NonNull CharSequence mMessage; /** * Creates DisableSecureLockDeviceParams with the given params. * * @param message Allows clients to pass in a message with information about the request to * disable secure lock on the device. This message will be shown to the user when - * secure lock mode is disabled. If an empty string is provided, it will default - * to a system-defined string (e.g. "Secure lock mode has been disabled.") + * secure lock mode is disabled. If an empty CharSequence is provided, it will + * default to a system-defined CharSequence (e.g. "Secure lock mode has been + * disabled.") + * + * Since this text is shown in a restricted lockscreen state, typeface properties + * such as color, font weight, or other formatting may not be honored. */ - public DisableSecureLockDeviceParams(@NonNull String message) { + public DisableSecureLockDeviceParams(@NonNull CharSequence message) { mMessage = message; } private DisableSecureLockDeviceParams(@NonNull Parcel in) { - mMessage = Objects.requireNonNull(in.readString8()); + mMessage = Objects.requireNonNull(in.readCharSequence()); } public static final @NonNull Creator<DisableSecureLockDeviceParams> CREATOR = @@ -77,6 +84,6 @@ public final class DisableSecureLockDeviceParams implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeString8(mMessage); + dest.writeCharSequence(mMessage); } } diff --git a/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.java b/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.java index 1d727727ce37..dfa391fcc85d 100644 --- a/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.java +++ b/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.java @@ -38,23 +38,30 @@ public final class EnableSecureLockDeviceParams implements Parcelable { /** * Client message associated with the request to enable secure lock on the device. This message * will be shown on the device when secure lock mode is enabled. + * + * Since this text is shown in a restricted lockscreen state, typeface properties such as color, + * font weight, or other formatting may not be honored. */ - private final @NonNull String mMessage; + private final @NonNull CharSequence mMessage; /** * Creates EnableSecureLockDeviceParams with the given params. * * @param message Allows clients to pass in a message with information about the request to * enable secure lock on the device. This message will be shown to the user when - * secure lock mode is enabled. If an empty string is provided, it will default - * to a system-defined string (e.g. "Device is securely locked remotely.") + * secure lock mode is enabled. If an empty CharSequence is provided, it will + * default to a system-defined CharSequence (e.g. "Device is securely locked + * remotely.") + * + * Since this text is shown in a restricted lockscreen state, typeface properties + * such as color, font weight, or other formatting may not be honored. */ - public EnableSecureLockDeviceParams(@NonNull String message) { + public EnableSecureLockDeviceParams(@NonNull CharSequence message) { mMessage = message; } private EnableSecureLockDeviceParams(@NonNull Parcel in) { - mMessage = Objects.requireNonNull(in.readString8()); + mMessage = Objects.requireNonNull(in.readCharSequence()); } public static final @NonNull Creator<EnableSecureLockDeviceParams> CREATOR = @@ -77,6 +84,6 @@ public final class EnableSecureLockDeviceParams implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeString8(mMessage); + dest.writeCharSequence(mMessage); } } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 2cd390113040..9c2833b91a2b 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -47,6 +47,7 @@ import android.app.LoadedApk; import android.app.PendingIntent; import android.app.RemoteInput; import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager.ServiceCollectionCache; import android.appwidget.flags.Flags; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; @@ -54,7 +55,6 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentSender; -import android.content.ServiceConnection; import android.content.om.FabricatedOverlay; import android.content.om.OverlayInfo; import android.content.om.OverlayManager; @@ -82,7 +82,6 @@ import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; -import android.os.IBinder; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; @@ -127,8 +126,8 @@ import android.widget.CompoundButton.OnCheckedChangeListener; import com.android.internal.R; import com.android.internal.util.Preconditions; import com.android.internal.widget.IRemoteViewsFactory; -import com.android.internal.widget.remotecompose.core.operations.Theme; import com.android.internal.widget.remotecompose.core.CoreDocument; +import com.android.internal.widget.remotecompose.core.operations.Theme; import com.android.internal.widget.remotecompose.player.RemoteComposeDocument; import com.android.internal.widget.remotecompose.player.RemoteComposePlayer; @@ -1391,8 +1390,10 @@ public class RemoteViews implements Parcelable, Filter { /** * @hide */ - public CompletableFuture<Void> collectAllIntents(int bitmapSizeLimit) { - return mCollectionCache.collectAllIntentsNoComplete(this, bitmapSizeLimit); + public CompletableFuture<Void> collectAllIntents(int bitmapSizeLimit, + @NonNull ServiceCollectionCache collectionCache) { + return mCollectionCache.collectAllIntentsNoComplete(this, bitmapSizeLimit, + collectionCache); } private class RemoteCollectionCache { @@ -1446,7 +1447,8 @@ public class RemoteViews implements Parcelable, Filter { } public @NonNull CompletableFuture<Void> collectAllIntentsNoComplete( - @NonNull RemoteViews inViews, int bitmapSizeLimit) { + @NonNull RemoteViews inViews, int bitmapSizeLimit, + @NonNull ServiceCollectionCache collectionCache) { SparseArray<Intent> idToIntentMapping = new SparseArray<>(); // Collect the number of uinque Intent (which is equal to the number of new connections // to make) for size allocation and exclude certain collections from being written to @@ -1478,7 +1480,7 @@ public class RemoteViews implements Parcelable, Filter { / numOfIntents; return connectAllUniqueIntents(individualSize, individualBitmapSizeLimit, - idToIntentMapping); + idToIntentMapping, collectionCache); } private void collectAllIntentsInternal(@NonNull RemoteViews inViews, @@ -1544,13 +1546,14 @@ public class RemoteViews implements Parcelable, Filter { } private @NonNull CompletableFuture<Void> connectAllUniqueIntents(int individualSize, - int individualBitmapSize, @NonNull SparseArray<Intent> idToIntentMapping) { + int individualBitmapSize, @NonNull SparseArray<Intent> idToIntentMapping, + @NonNull ServiceCollectionCache collectionCache) { List<CompletableFuture<Void>> intentFutureList = new ArrayList<>(); for (int i = 0; i < idToIntentMapping.size(); i++) { String currentIntentUri = mIdToUriMapping.get(idToIntentMapping.keyAt(i)); Intent currentIntent = idToIntentMapping.valueAt(i); intentFutureList.add(getItemsFutureFromIntentWithTimeout(currentIntent, - individualSize, individualBitmapSize) + individualSize, individualBitmapSize, collectionCache) .thenAccept(items -> { items.setHierarchyRootData(getHierarchyRootData()); mUriToCollectionMapping.put(currentIntentUri, items); @@ -1561,7 +1564,8 @@ public class RemoteViews implements Parcelable, Filter { } private static CompletableFuture<RemoteCollectionItems> getItemsFutureFromIntentWithTimeout( - Intent intent, int individualSize, int individualBitmapSize) { + Intent intent, int individualSize, int individualBitmapSize, + @NonNull ServiceCollectionCache collectionCache) { if (intent == null) { Log.e(LOG_TAG, "Null intent received when generating adapter future"); return CompletableFuture.completedFuture(new RemoteCollectionItems @@ -1581,39 +1585,24 @@ public class RemoteViews implements Parcelable, Filter { return result; } - context.bindService(intent, Context.BindServiceFlags.of(Context.BIND_AUTO_CREATE), - result.defaultExecutor(), new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName componentName, - IBinder iBinder) { - RemoteCollectionItems items; - try { - items = IRemoteViewsFactory.Stub.asInterface(iBinder) - .getRemoteCollectionItems(individualSize, - individualBitmapSize); - } catch (RemoteException re) { - items = new RemoteCollectionItems.Builder().build(); - Log.e(LOG_TAG, "Error getting collection items from the" - + " factory", re); - } finally { - context.unbindService(this); - } - - if (items == null) { - items = new RemoteCollectionItems.Builder().build(); - } - - result.complete(items); - } + collectionCache.connectAndConsume(intent, iBinder -> { + RemoteCollectionItems items; + try { + items = IRemoteViewsFactory.Stub.asInterface(iBinder) + .getRemoteCollectionItems(individualSize, + individualBitmapSize); + } catch (RemoteException re) { + items = new RemoteCollectionItems.Builder().build(); + Log.e(LOG_TAG, "Error getting collection items from the" + + " factory", re); + } - @Override - public void onNullBinding(ComponentName name) { - context.unbindService(this); - } + if (items == null) { + items = new RemoteCollectionItems.Builder().build(); + } - @Override - public void onServiceDisconnected(ComponentName componentName) { } - }); + result.complete(items); + }, result.defaultExecutor()); result.completeOnTimeout( new RemoteCollectionItems.Builder().build(), diff --git a/core/java/com/android/internal/policy/KeyInterceptionInfo.java b/core/java/com/android/internal/policy/KeyInterceptionInfo.java index b20f6d225b69..fed8fe3b4cc0 100644 --- a/core/java/com/android/internal/policy/KeyInterceptionInfo.java +++ b/core/java/com/android/internal/policy/KeyInterceptionInfo.java @@ -27,11 +27,13 @@ public class KeyInterceptionInfo { // Debug friendly name to help identify the window public final String windowTitle; public final int windowOwnerUid; + public final int inputFeaturesFlags; - public KeyInterceptionInfo(int type, int flags, String title, int uid) { + public KeyInterceptionInfo(int type, int flags, String title, int uid, int inputFeaturesFlags) { layoutParamsType = type; layoutParamsPrivateFlags = flags; windowTitle = title; windowOwnerUid = uid; + this.inputFeaturesFlags = inputFeaturesFlags; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index a368245db25f..2998a076a56c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -512,6 +512,11 @@ public class SplitScreenController implements SplitDragPolicy.Starter, mStageCoordinator.getStageBounds(outTopOrLeftBounds, outBottomOrRightBounds); } + /** Get the parent-based coordinates for split stages. */ + public void getRefStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { + mStageCoordinator.getRefStageBounds(outTopOrLeftBounds, outBottomOrRightBounds); + } + public void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { mStageCoordinator.registerSplitScreenListener(listener); } 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 b40996f52bd3..ba724edb9747 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 @@ -1776,6 +1776,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, outBottomOrRightBounds.set(mSplitLayout.getBottomRightBounds()); } + void getRefStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { + outTopOrLeftBounds.set(mSplitLayout.getTopLeftRefBounds()); + outBottomOrRightBounds.set(mSplitLayout.getBottomRightRefBounds()); + } + private void runForActiveStages(Consumer<StageTaskListener> consumer) { mStageOrderOperator.getActiveStages().forEach(consumer); } @@ -2150,8 +2155,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.setForceTranslucent(mRootTaskInfo.token, translucent); } - /** Callback when split roots visiblility changed. - * NOTICE: This only be called on legacy transition. */ + /** Callback when split roots visiblility changed. */ @Override public void onStageVisibilityChanged(StageTaskListener stageListener) { // If split didn't active, just ignore this callback because we should already did these diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 4a37169add36..f1245ba26cc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -240,12 +240,20 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { @Override @CallSuper public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: taskId=%d taskAct=%s " - + "stageId=%s", - taskInfo.taskId, taskInfo.baseActivity, stageTypeToString(mId)); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, + "onTaskInfoChanged: taskId=%d vis=%b reqVis=%b baseAct=%s stageId=%s", + taskInfo.taskId, taskInfo.isVisible, taskInfo.isVisibleRequested, + taskInfo.baseActivity, stageTypeToString(mId)); mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo)); if (mRootTaskInfo.taskId == taskInfo.taskId) { mRootTaskInfo = taskInfo; + boolean isVisible = taskInfo.isVisible && taskInfo.isVisibleRequested; + if (mVisible != isVisible) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: currentVis=%b newVis=%b", + mVisible, isVisible); + mVisible = isVisible; + mCallbacks.onStageVisibilityChanged(this); + } } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { if (!taskInfo.supportsMultiWindow || !ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType()) @@ -260,7 +268,6 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { return; } mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); - mVisible = isStageVisible(); mCallbacks.onChildTaskStatusChanged(this, taskInfo.taskId, true /* present */, taskInfo.isVisible && taskInfo.isVisibleRequested); } else { @@ -309,19 +316,6 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { t.reparent(sc, findTaskSurface(taskId)); } - /** - * Checks against all children task info and return true if any are marked as visible. - */ - private boolean isStageVisible() { - for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { - if (mChildrenTaskInfo.valueAt(i).isVisible - && mChildrenTaskInfo.valueAt(i).isVisibleRequested) { - return true; - } - } - return false; - } - private SurfaceControl findTaskSurface(int taskId) { if (mRootTaskInfo.taskId == taskId) { return mRootLeash; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt index 4d95cde1492f..01fc6440712d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt @@ -19,19 +19,18 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.graphics.Point -import android.graphics.Rect +import android.view.View import android.view.WindowManager import android.window.TaskSnapshot import androidx.compose.ui.graphics.toArgb +import com.android.wm.shell.R import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.multiinstance.ManageWindowsViewContainer -import com.android.wm.shell.shared.split.SplitScreenConstants import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer +import com.android.wm.shell.windowdecor.common.calculateMenuPosition import com.android.wm.shell.windowdecor.common.DecorThemeUtil -import com.android.wm.shell.windowdecor.extension.isFullscreen -import com.android.wm.shell.windowdecor.extension.isMultiWindow /** * Implementation of [ManageWindowsViewContainer] meant to be used in desktop header and app @@ -59,35 +58,19 @@ class DesktopHandleManageWindowsMenu( } private fun calculateMenuPosition(): Point { - val position = Point() - val nonFreeformX = (captionX + (captionWidth / 2) - (menuView.menuWidth / 2)) - when { - callerTaskInfo.isFreeform -> { - val taskBounds = callerTaskInfo.getConfiguration().windowConfiguration.bounds - position.set(taskBounds.left, taskBounds.top) - } - callerTaskInfo.isFullscreen -> { - position.set(nonFreeformX, 0) - } - callerTaskInfo.isMultiWindow -> { - val splitPosition = splitScreenController.getSplitPosition(callerTaskInfo.taskId) - val leftOrTopStageBounds = Rect() - val rightOrBottomStageBounds = Rect() - splitScreenController.getStageBounds(leftOrTopStageBounds, rightOrBottomStageBounds) - // TODO(b/343561161): This needs to be calculated differently if the task is in - // top/bottom split. - when (splitPosition) { - SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> { - position.set(leftOrTopStageBounds.width() + nonFreeformX, /* y= */ 0) - } - - SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> { - position.set(nonFreeformX, /* y= */ 0) - } - } - } - } - return position + return calculateMenuPosition( + splitScreenController, + callerTaskInfo, + marginStart = 0, + marginTop = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_margin_top + ), + captionX, + 0, + captionWidth, + menuView.menuWidth, + context.isRtl() + ) } override fun addToContainer(menuView: ManageWindowsView) { @@ -109,4 +92,7 @@ class DesktopHandleManageWindowsMenu( override fun removeFromContainer() { menuViewContainer?.releaseView() } + + private fun Context.isRtl() = + resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 049b8d621427..1179b0cd226c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -47,12 +47,12 @@ import androidx.compose.ui.graphics.toArgb import androidx.core.view.isGone import com.android.window.flags.Flags import com.android.wm.shell.R -import com.android.wm.shell.apptoweb.isBrowserApp import com.android.wm.shell.shared.split.SplitScreenConstants import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.calculateMenuPosition import com.android.wm.shell.windowdecor.extension.isFullscreen import com.android.wm.shell.windowdecor.extension.isMultiWindow import com.android.wm.shell.windowdecor.extension.isPinned @@ -240,7 +240,19 @@ class HandleMenu( val menuX: Int val menuY: Int val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds - updateGlobalMenuPosition(taskBounds, captionX, captionY) + globalMenuPosition.set( + calculateMenuPosition( + splitScreenController, + taskInfo, + marginStart = marginMenuStart, + marginMenuTop, + captionX, + captionY, + captionWidth, + menuWidth, + context.isRtl() + ) + ) if (layoutResId == R.layout.desktop_mode_app_header) { // Align the handle menu to the start of the header. menuX = if (context.isRtl()) { @@ -265,53 +277,6 @@ class HandleMenu( handleMenuPosition.set(menuX.toFloat(), menuY.toFloat()) } - private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int, captionY: Int) { - val nonFreeformX = captionX + (captionWidth / 2) - (menuWidth / 2) - when { - taskInfo.isFreeform -> { - if (context.isRtl()) { - globalMenuPosition.set( - /* x= */ taskBounds.right - menuWidth - marginMenuStart, - /* y= */ taskBounds.top + captionY + marginMenuTop - ) - } else { - globalMenuPosition.set( - /* x= */ taskBounds.left + marginMenuStart, - /* y= */ taskBounds.top + captionY + marginMenuTop - ) - } - } - taskInfo.isFullscreen -> { - globalMenuPosition.set( - /* x = */ nonFreeformX, - /* y = */ marginMenuTop + captionY - ) - } - taskInfo.isMultiWindow -> { - val splitPosition = splitScreenController.getSplitPosition(taskInfo.taskId) - val leftOrTopStageBounds = Rect() - val rightOrBottomStageBounds = Rect() - splitScreenController.getStageBounds(leftOrTopStageBounds, rightOrBottomStageBounds) - // TODO(b/343561161): This needs to be calculated differently if the task is in - // top/bottom split. - when (splitPosition) { - SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> { - globalMenuPosition.set( - /* x = */ leftOrTopStageBounds.width() + nonFreeformX, - /* y = */ captionY + marginMenuTop - ) - } - SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> { - globalMenuPosition.set( - /* x = */ nonFreeformX, - /* y = */ captionY + marginMenuTop - ) - } - } - } - } - } - /** * Update pill layout, in case task changes have caused positioning to change. */ @@ -374,8 +339,6 @@ class HandleMenu( ) if (splitScreenController.getSplitPosition(taskInfo.taskId) == SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT) { - // TODO(b/343561161): This also needs to be calculated differently if - // the task is in top/bottom split. val leftStageBounds = Rect() splitScreenController.getStageBounds(leftStageBounds, Rect()) inputRelativeToMenu.x += leftStageBounds.width().toFloat() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/DesktopMenuPositionUtility.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/DesktopMenuPositionUtility.kt new file mode 100644 index 000000000000..6def3daf8c8d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/DesktopMenuPositionUtility.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.common + +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Point +import android.graphics.Rect +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT +import com.android.wm.shell.splitscreen.SplitScreenController +import com.android.wm.shell.windowdecor.extension.isFullscreen + +/** Utility function used for calculating position of desktop mode menus. */ +fun calculateMenuPosition( + splitScreenController: SplitScreenController, + taskInfo: RunningTaskInfo, + marginStart: Int, + marginTop: Int, + captionX: Int, + captionY: Int, + captionWidth: Int, + menuWidth: Int, + isRtl: Boolean, +): Point { + if (taskInfo.isFreeform) { + val taskBounds = taskInfo.configuration.windowConfiguration.bounds + return if (isRtl) { + Point( + /* x= */ taskBounds.right - menuWidth - marginStart, + /* y= */ taskBounds.top + marginTop, + ) + } else { + Point(/* x= */ taskBounds.left + marginStart, /* y= */ taskBounds.top + marginTop) + } + } + val nonFreeformPosition = Point(captionX + (captionWidth / 2) - (menuWidth / 2), captionY) + if (taskInfo.isFullscreen) { + return Point(nonFreeformPosition.x, nonFreeformPosition.y + marginTop) + } + // Only the splitscreen case left. + val splitPosition = splitScreenController.getSplitPosition(taskInfo.taskId) + val leftOrTopStageBounds = Rect() + val rightOrBottomStageBounds = Rect() + splitScreenController.getRefStageBounds(leftOrTopStageBounds, rightOrBottomStageBounds) + if (splitScreenController.isLeftRightSplit) { + val rightStageModifier = + if (splitPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) { + rightOrBottomStageBounds.left + } else { + 0 + } + return Point( + /* x = */ rightStageModifier + nonFreeformPosition.x, + /* y = */ nonFreeformPosition.y + marginTop, + ) + } else { + val bottomSplitModifier = + if (splitPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) { + rightOrBottomStageBounds.top + } else { + 0 + } + return Point( + /* x = */ nonFreeformPosition.x, + /* y = */ nonFreeformPosition.y + bottomSplitModifier + marginTop, + ) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index 6babf817686a..3bcbcbdd9105 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -257,7 +257,7 @@ class HandleMenuTest : ShellTestCase() { if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { (SPLIT_LEFT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) } else { - (SPLIT_RIGHT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) + SPLIT_LEFT_BOUNDS.width() + (SPLIT_RIGHT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) } } else -> error("Invalid windowing mode") diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/DesktopMenuPositionUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/DesktopMenuPositionUtilityTest.kt new file mode 100644 index 000000000000..7b42ff4548ac --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/DesktopMenuPositionUtilityTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.common + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT +import com.android.wm.shell.splitscreen.SplitScreenController +import org.junit.runner.RunWith +import kotlin.test.Test +import kotlin.test.assertEquals +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** + * Tests for [DesktopMenuPositionUtility]. + * + * Build/Install/Run: atest WMShellUnitTests:DesktopMenuPositionUtilityTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopMenuPositionUtilityTest : ShellTestCase() { + + @Mock private val mockSplitScreenController = mock<SplitScreenController>() + + @Test + fun testFullscreenPositionCalculation() { + val task = setupTaskInfo(WINDOWING_MODE_FULLSCREEN) + val result = + calculateMenuPosition( + splitScreenController = mockSplitScreenController, + taskInfo = task, + MARGIN_START, + MARGIN_TOP, + CAPTION_X, + CAPTION_Y, + CAPTION_WIDTH, + MENU_WIDTH, + isRtl = false, + ) + assertEquals(CAPTION_X + (CAPTION_WIDTH / 2) - (MENU_WIDTH / 2), result.x) + assertEquals(CAPTION_Y + MARGIN_TOP, result.y) + } + + @Test + fun testSplitLeftPositionCalculation() { + val task = setupTaskInfo(WINDOWING_MODE_MULTI_WINDOW) + setupMockSplitScreenController( + splitPosition = SPLIT_POSITION_TOP_OR_LEFT, + isLeftRightSplit = true, + ) + val result = + calculateMenuPosition( + splitScreenController = mockSplitScreenController, + taskInfo = task, + MARGIN_START, + MARGIN_TOP, + CAPTION_X, + CAPTION_Y, + CAPTION_WIDTH, + MENU_WIDTH, + isRtl = false, + ) + assertEquals(CAPTION_X + (CAPTION_WIDTH / 2) - (MENU_WIDTH / 2), result.x) + assertEquals(CAPTION_Y + MARGIN_TOP, result.y) + } + + @Test + fun testSplitRightPositionCalculation() { + val task = setupTaskInfo(WINDOWING_MODE_MULTI_WINDOW) + setupMockSplitScreenController( + splitPosition = SPLIT_POSITION_BOTTOM_OR_RIGHT, + isLeftRightSplit = true, + ) + val result = + calculateMenuPosition( + splitScreenController = mockSplitScreenController, + taskInfo = task, + MARGIN_START, + MARGIN_TOP, + CAPTION_X, + CAPTION_Y, + CAPTION_WIDTH, + MENU_WIDTH, + isRtl = false, + ) + assertEquals( + CAPTION_X + (CAPTION_WIDTH / 2) - (MENU_WIDTH / 2) + SPLIT_LEFT_BOUNDS.width(), + result.x, + ) + assertEquals(CAPTION_Y + MARGIN_TOP, result.y) + } + + @Test + fun testSplitTopPositionCalculation() { + val task = setupTaskInfo(WINDOWING_MODE_MULTI_WINDOW) + setupMockSplitScreenController( + splitPosition = SPLIT_POSITION_TOP_OR_LEFT, + isLeftRightSplit = false, + ) + val result = + calculateMenuPosition( + splitScreenController = mockSplitScreenController, + taskInfo = task, + MARGIN_START, + MARGIN_TOP, + CAPTION_X, + CAPTION_Y, + CAPTION_WIDTH, + MENU_WIDTH, + isRtl = false, + ) + assertEquals(CAPTION_X + (CAPTION_WIDTH / 2) - (MENU_WIDTH / 2), result.x) + assertEquals(CAPTION_Y + MARGIN_TOP, result.y) + } + + @Test + fun testSplitBottomPositionCalculation() { + val task = setupTaskInfo(WINDOWING_MODE_MULTI_WINDOW) + setupMockSplitScreenController( + splitPosition = SPLIT_POSITION_BOTTOM_OR_RIGHT, + isLeftRightSplit = false, + ) + val result = + calculateMenuPosition( + splitScreenController = mockSplitScreenController, + taskInfo = task, + MARGIN_START, + MARGIN_TOP, + CAPTION_X, + CAPTION_Y, + CAPTION_WIDTH, + MENU_WIDTH, + isRtl = false, + ) + assertEquals(CAPTION_X + (CAPTION_WIDTH / 2) - (MENU_WIDTH / 2), result.x) + assertEquals(CAPTION_Y + MARGIN_TOP + SPLIT_TOP_BOUNDS.height(), result.y) + } + + private fun setupTaskInfo(windowingMode: Int): RunningTaskInfo { + return TestRunningTaskInfoBuilder().setWindowingMode(windowingMode).build() + } + + private fun setupMockSplitScreenController(isLeftRightSplit: Boolean, splitPosition: Int) { + whenever(mockSplitScreenController.getSplitPosition(anyInt())).thenReturn(splitPosition) + whenever(mockSplitScreenController.getRefStageBounds(any(), any())).thenAnswer { + (it.arguments.first() as Rect).set( + if (isLeftRightSplit) { + SPLIT_LEFT_BOUNDS + } else { + SPLIT_TOP_BOUNDS + } + ) + (it.arguments[1] as Rect).set( + if (isLeftRightSplit) { + SPLIT_RIGHT_BOUNDS + } else { + SPLIT_BOTTOM_BOUNDS + } + ) + } + whenever(mockSplitScreenController.isLeftRightSplit).thenReturn(isLeftRightSplit) + } + + companion object { + private val SPLIT_LEFT_BOUNDS = Rect(0, 0, 1280, 1600) + private val SPLIT_RIGHT_BOUNDS = Rect(1280, 0, 2560, 1600) + private val SPLIT_TOP_BOUNDS = Rect(0, 0, 2560, 800) + private val SPLIT_BOTTOM_BOUNDS = Rect(0, 800, 2560, 1600) + private const val CAPTION_X = 800 + private const val CAPTION_Y = 50 + private const val MARGIN_START = 30 + private const val MARGIN_TOP = 50 + private const val MENU_WIDTH = 500 + private const val CAPTION_WIDTH = 200 + } +} diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java index 0317d5f095a1..d0404ec02306 100644 --- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java @@ -18,8 +18,11 @@ package com.android.systemui.animation; import android.annotation.Nullable; import android.graphics.Canvas; +import android.graphics.Outline; +import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.Rect; +import android.graphics.RectF; import android.os.Build; import android.util.Log; import android.view.Surface; @@ -44,6 +47,9 @@ import java.util.concurrent.Executor; public class ViewUIComponent implements UIComponent { private static final String TAG = "ViewUIComponent"; private static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG); + private final Path mClippingPath = new Path(); + private final Outline mClippingOutline = new Outline(); + private final OnDrawListener mOnDrawListener = this::postDraw; private final View mView; @@ -182,6 +188,17 @@ public class ViewUIComponent implements UIComponent { canvas.scale( (float) renderBounds.width() / realBounds.width(), (float) renderBounds.height() / realBounds.height()); + + if (mView.getClipToOutline()) { + mView.getOutlineProvider().getOutline(mView, mClippingOutline); + mClippingPath.reset(); + RectF rect = new RectF(0, 0, mView.getWidth(), mView.getHeight()); + final float cornerRadius = mClippingOutline.getRadius(); + mClippingPath.addRoundRect(rect, cornerRadius, cornerRadius, Path.Direction.CW); + mClippingPath.close(); + canvas.clipPath(mClippingPath); + } + canvas.saveLayerAlpha(null, (int) (255 * mView.getAlpha())); mView.draw(canvas); canvas.restore(); diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt index da78eed2612b..1cee4d67df3b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt @@ -77,7 +77,7 @@ constructor( resources.getDimensionPixelSize( R.dimen.keyguard_status_view_bottom_margin ) - } + }, ) ) { if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) { @@ -87,6 +87,7 @@ constructor( val paddingBelowClockStart = dimensionResource(R.dimen.below_clock_padding_start) val paddingBelowClockEnd = dimensionResource(R.dimen.below_clock_padding_end) + val paddingCardHorizontal = paddingBelowClockEnd if (keyguardSmartspaceViewModel.isDateWeatherDecoupled) { Row( @@ -96,16 +97,14 @@ constructor( // All items will be constrained to be as tall as the shortest // item. .height(IntrinsicSize.Min) - .padding( - start = paddingBelowClockStart, - ), + .padding(start = paddingBelowClockStart), ) { Date( modifier = Modifier.burnInAware( viewModel = aodBurnInViewModel, params = burnInParams, - ), + ) ) Spacer(modifier = Modifier.width(4.dp)) Weather( @@ -113,7 +112,7 @@ constructor( Modifier.burnInAware( viewModel = aodBurnInViewModel, params = burnInParams, - ), + ) ) } } @@ -121,14 +120,8 @@ constructor( Card( modifier = Modifier.fillMaxWidth() - .padding( - start = paddingBelowClockStart, - end = paddingBelowClockEnd, - ) - .burnInAware( - viewModel = aodBurnInViewModel, - params = burnInParams, - ), + .padding(start = paddingCardHorizontal, end = paddingCardHorizontal) + .burnInAware(viewModel = aodBurnInViewModel, params = burnInParams) ) } } @@ -136,9 +129,7 @@ constructor( } @Composable - private fun Card( - modifier: Modifier = Modifier, - ) { + private fun Card(modifier: Modifier = Modifier) { AndroidView( factory = { context -> FrameLayout(context).apply { @@ -161,9 +152,7 @@ constructor( } @Composable - private fun Weather( - modifier: Modifier = Modifier, - ) { + private fun Weather(modifier: Modifier = Modifier) { val isVisible by keyguardSmartspaceViewModel.isWeatherVisible.collectAsStateWithLifecycle() if (!isVisible) { return @@ -188,9 +177,7 @@ constructor( } @Composable - private fun Date( - modifier: Modifier = Modifier, - ) { + private fun Date(modifier: Modifier = Modifier) { val isVisible by keyguardSmartspaceViewModel.isDateVisible.collectAsStateWithLifecycle() if (!isVisible) { return diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt index 7842d75db6c4..f9b44886ec3e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt @@ -111,6 +111,8 @@ class CommunalSmartspaceControllerTest : SysuiTestCase() { override fun getCurrentCardTopPadding(): Int { return 0 } + + override fun setHorizontalPaddings(horizontalPadding: Int) {} } @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt index c83c82dc0914..70ba00e82241 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt @@ -127,6 +127,8 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { override fun getCurrentCardTopPadding(): Int { return 0 } + + override fun setHorizontalPaddings(horizontalPadding: Int) {} } @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt index fe287ef98729..611ae3dbefcf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt @@ -1109,6 +1109,9 @@ class LockscreenSmartspaceControllerTest : SysuiTestCase() { override fun getCurrentCardTopPadding(): Int { return 0 } + + override fun setHorizontalPaddings(horizontalPadding: Int) { + } }) } } diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java index dcb15a7cd9aa..b2083c2921c9 100644 --- a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java +++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java @@ -213,6 +213,13 @@ public interface BcSmartspaceDataPlugin extends Plugin { default int getCurrentCardTopPadding() { throw new UnsupportedOperationException("Not implemented by " + getClass()); } + + /** + * Set the horizontal paddings for applicable children inside the SmartspaceView. + */ + default void setHorizontalPaddings(int horizontalPadding) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } } /** Interface for launching Intents, which can differ on the lockscreen */ diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index ba0d7de1d481..bfb37a0d97a7 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -110,6 +110,7 @@ <dimen name="below_clock_padding_start">32dp</dimen> <dimen name="below_clock_padding_end">16dp</dimen> <dimen name="below_clock_padding_start_icons">28dp</dimen> + <dimen name="smartspace_padding_horizontal">16dp</dimen> <!-- Proportion of the screen height to use to set the maximum height of the bouncer to when the device is in the DEVICE_POSTURE_HALF_OPENED posture, for the PIN/pattern entry. 0 will diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index eab752877520..85725d24758d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -368,8 +368,8 @@ constructor( SceneContainerFlag.isEnabled, ) ) - val startPadding: Int = smartspaceViewModel.getSmartspaceStartPadding(previewContext) - val endPadding: Int = smartspaceViewModel.getSmartspaceEndPadding(previewContext) + val startPadding: Int = smartspaceViewModel.getDateWeatherStartPadding(previewContext) + val endPadding: Int = smartspaceViewModel.getDateWeatherEndPadding(previewContext) smartSpaceView?.let { it.setPaddingRelative(startPadding, topPadding, endPadding, 0) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index d54d411b7de6..73e14b1524f3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -33,8 +33,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardSmartspaceViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel -import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.res.R as R +import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import dagger.Lazy @@ -113,8 +113,9 @@ constructor( override fun applyConstraints(constraintSet: ConstraintSet) { if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return - val horizontalPaddingStart = KeyguardSmartspaceViewModel.getSmartspaceStartMargin(context) - val horizontalPaddingEnd = KeyguardSmartspaceViewModel.getSmartspaceEndMargin(context) + val dateWeatherPaddingStart = KeyguardSmartspaceViewModel.getDateWeatherStartMargin(context) + val smartspaceHorizontalPadding = + KeyguardSmartspaceViewModel.getSmartspaceHorizontalMargin(context) constraintSet.apply { // migrate addDateWeatherView, addWeatherView from KeyguardClockSwitchController constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) @@ -124,7 +125,7 @@ constructor( ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, - horizontalPaddingStart, + dateWeatherPaddingStart, ) // migrate addSmartspaceView from KeyguardClockSwitchController @@ -135,7 +136,7 @@ constructor( ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, - horizontalPaddingStart, + smartspaceHorizontalPadding, ) connect( sharedR.id.bc_smartspace_view, @@ -143,7 +144,7 @@ constructor( if (keyguardSmartspaceViewModel.isShadeLayoutWide.value) R.id.split_shade_guideline else ConstraintSet.PARENT_ID, ConstraintSet.END, - horizontalPaddingEnd, + smartspaceHorizontalPadding, ) if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt index 0280d17f4ae2..15b696e71164 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt @@ -62,12 +62,12 @@ constructor( overrideClockSize.value = clockSize } - fun getSmartspaceStartPadding(context: Context): Int { - return KeyguardSmartspaceViewModel.getSmartspaceStartMargin(context) + fun getDateWeatherStartPadding(context: Context): Int { + return KeyguardSmartspaceViewModel.getDateWeatherStartMargin(context) } - fun getSmartspaceEndPadding(context: Context): Int { - return KeyguardSmartspaceViewModel.getSmartspaceEndMargin(context) + fun getDateWeatherEndPadding(context: Context): Int { + return KeyguardSmartspaceViewModel.getDateWeatherEndMargin(context) } /* diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt index 3266dc45427a..5ee80a7b7442 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt @@ -94,14 +94,19 @@ constructor( val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide companion object { - fun getSmartspaceStartMargin(context: Context): Int { + fun getDateWeatherStartMargin(context: Context): Int { return context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + context.resources.getDimensionPixelSize(customR.dimen.status_view_margin_horizontal) } - fun getSmartspaceEndMargin(context: Context): Int { + fun getDateWeatherEndMargin(context: Context): Int { return context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_end) + context.resources.getDimensionPixelSize(customR.dimen.status_view_margin_horizontal) } + + fun getSmartspaceHorizontalMargin(context: Context): Int { + return context.resources.getDimensionPixelSize(R.dimen.smartspace_padding_horizontal) + + context.resources.getDimensionPixelSize(customR.dimen.status_view_margin_horizontal) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileCoroutineScopeFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileCoroutineScopeFactory.kt index dde36289f139..5f476ea7e274 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileCoroutineScopeFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileCoroutineScopeFactory.kt @@ -17,18 +17,17 @@ package com.android.systemui.qs.tiles.base.viewmodel import com.android.systemui.coroutines.newTracingContext -import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob /** Creates a [CoroutineScope] for the [QSTileViewModelImpl]. */ class QSTileCoroutineScopeFactory @Inject -constructor(@Application private val applicationScope: CoroutineScope) { +constructor(@Background private val bgDispatcher: CoroutineDispatcher) { fun create(): CoroutineScope = - CoroutineScope( - applicationScope.coroutineContext + SupervisorJob() + newTracingContext("QSTileScope") - ) + CoroutineScope(bgDispatcher + SupervisorJob() + newTracingContext("QSTileScope")) } 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 b9e38abf8ab2..f37f7f990cf9 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 @@ -2630,6 +2630,7 @@ public class NotificationStackScrollLayout private void updateContentHeight() { if (SceneContainerFlag.isEnabled()) { updateIntrinsicStackHeight(); + updateStackEndHeightAndStackHeight(mAmbientState.getExpansionFraction()); return; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 11b19f95c1c0..99467cb11282 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -1275,6 +1275,37 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + @EnableSceneContainer + public void testChildHeightUpdated_whenMaxDisplayedNotificationsSet_updatesStackHeight() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + int maxNotifs = 1; // any non-zero limit + float stackTop = 100; + float stackCutoff = 1100; + mStackScroller.setStackTop(stackTop); + mStackScroller.setStackCutoff(stackCutoff); + + // Given we have a limit on max displayed notifications + int stackHeightBeforeUpdate = 100; + when(mStackSizeCalculator.computeHeight(eq(mStackScroller), eq(maxNotifs), anyFloat())) + .thenReturn((float) stackHeightBeforeUpdate); + mStackScroller.setMaxDisplayedNotifications(maxNotifs); + + // And the stack heights are set + assertThat(mStackScroller.getIntrinsicStackHeight()).isEqualTo(stackHeightBeforeUpdate); + assertThat(mAmbientState.getStackEndHeight()).isEqualTo(stackHeightBeforeUpdate); + + // When a child changes its height + int stackHeightAfterUpdate = 300; + when(mStackSizeCalculator.computeHeight(eq(mStackScroller), eq(maxNotifs), anyFloat())) + .thenReturn((float) stackHeightAfterUpdate); + mStackScroller.onChildHeightChanged(row, /* needsAnimation = */ false); + + // Then the stack heights are updated + assertThat(mStackScroller.getIntrinsicStackHeight()).isEqualTo(stackHeightAfterUpdate); + assertThat(mAmbientState.getStackEndHeight()).isEqualTo(stackHeightAfterUpdate); + } + + @Test @DisableSceneContainer public void testSetMaxDisplayedNotifications_notifiesListeners() { ExpandableView.OnHeightChangedListener listener = diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java index ccc44a41759b..d84a892e4f54 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -16,9 +16,14 @@ package com.android.server; +import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap; + +import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis; import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.PendingIntent; import android.app.StatusBarManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -33,6 +38,7 @@ import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.hardware.TriggerEvent; import android.hardware.TriggerEventListener; +import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; import android.os.PowerManager.WakeLock; @@ -41,10 +47,12 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; +import android.service.quickaccesswallet.QuickAccessWalletClient; import android.util.MutableBoolean; import android.util.Slog; import android.view.KeyEvent; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEvent; @@ -70,7 +78,8 @@ public class GestureLauncherService extends SystemService { * Time in milliseconds in which the power button must be pressed twice so it will be considered * as a camera launch. */ - @VisibleForTesting static final long CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS = 300; + @VisibleForTesting + static final long POWER_DOUBLE_TAP_MAX_TIME_MS = 300; /** @@ -100,10 +109,23 @@ public class GestureLauncherService extends SystemService { @VisibleForTesting static final int EMERGENCY_GESTURE_POWER_BUTTON_COOLDOWN_PERIOD_MS_MAX = 5000; - /** - * Number of taps required to launch camera shortcut. - */ - private static final int CAMERA_POWER_TAP_COUNT_THRESHOLD = 2; + /** Indicates camera should be launched on power double tap. */ + @VisibleForTesting static final int LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER = 0; + + /** Indicates wallet should be launched on power double tap. */ + @VisibleForTesting static final int LAUNCH_WALLET_ON_DOUBLE_TAP_POWER = 1; + + /** Number of taps required to launch the double tap shortcut (either camera or wallet). */ + public static final int DOUBLE_POWER_TAP_COUNT_THRESHOLD = 2; + + /** Bundle to send with PendingIntent to grant background activity start privileges. */ + private static final Bundle GRANT_BACKGROUND_START_PRIVILEGES = + ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) + .toBundle(); + + private final QuickAccessWalletClient mQuickAccessWalletClient; /** The listener that receives the gesture event. */ private final GestureEventListener mGestureListener = new GestureEventListener(); @@ -158,6 +180,9 @@ public class GestureLauncherService extends SystemService { */ private boolean mCameraDoubleTapPowerEnabled; + /** Whether wallet double tap power button gesture is currently enabled. */ + private boolean mWalletDoubleTapPowerEnabled; + /** * Whether emergency gesture is currently enabled */ @@ -204,14 +229,22 @@ public class GestureLauncherService extends SystemService { } } public GestureLauncherService(Context context) { - this(context, new MetricsLogger(), new UiEventLoggerImpl()); + this( + context, + new MetricsLogger(), + QuickAccessWalletClient.create(context), + new UiEventLoggerImpl()); } @VisibleForTesting - GestureLauncherService(Context context, MetricsLogger metricsLogger, + public GestureLauncherService( + Context context, + MetricsLogger metricsLogger, + QuickAccessWalletClient quickAccessWalletClient, UiEventLogger uiEventLogger) { super(context); mContext = context; + mQuickAccessWalletClient = quickAccessWalletClient; mMetricsLogger = metricsLogger; mUiEventLogger = uiEventLogger; } @@ -237,6 +270,9 @@ public class GestureLauncherService extends SystemService { "GestureLauncherService"); updateCameraRegistered(); updateCameraDoubleTapPowerEnabled(); + if (launchWalletOptionOnPowerDoubleTap()) { + updateWalletDoubleTapPowerEnabled(); + } updateEmergencyGestureEnabled(); updateEmergencyGesturePowerButtonCooldownPeriodMs(); @@ -292,6 +328,14 @@ public class GestureLauncherService extends SystemService { } @VisibleForTesting + void updateWalletDoubleTapPowerEnabled() { + boolean enabled = isWalletDoubleTapPowerSettingEnabled(mContext, mUserId); + synchronized (this) { + mWalletDoubleTapPowerEnabled = enabled; + } + } + + @VisibleForTesting void updateEmergencyGestureEnabled() { boolean enabled = isEmergencyGestureSettingEnabled(mContext, mUserId); synchronized (this) { @@ -418,10 +462,33 @@ public class GestureLauncherService extends SystemService { Settings.Secure.CAMERA_GESTURE_DISABLED, 0, userId) == 0); } + /** Checks if camera should be launched on double press of the power button. */ public static boolean isCameraDoubleTapPowerSettingEnabled(Context context, int userId) { - return isCameraDoubleTapPowerEnabled(context.getResources()) - && (Settings.Secure.getIntForUser(context.getContentResolver(), - Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0); + boolean res; + + if (launchWalletOptionOnPowerDoubleTap()) { + res = isDoubleTapPowerGestureSettingEnabled(context, userId) + && getDoubleTapPowerGestureAction(context, userId) + == LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER; + } else { + // These are legacy settings that will be deprecated once the option to launch both + // wallet and camera has been created. + res = isCameraDoubleTapPowerEnabled(context.getResources()) + && (Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0); + } + return res; + } + + /** Checks if wallet should be launched on double tap of the power button. */ + public static boolean isWalletDoubleTapPowerSettingEnabled(Context context, int userId) { + if (!launchWalletOptionOnPowerDoubleTap()) { + return false; + } + + return isDoubleTapPowerGestureSettingEnabled(context, userId) + && getDoubleTapPowerGestureAction(context, userId) + == LAUNCH_WALLET_ON_DOUBLE_TAP_POWER; } public static boolean isCameraLiftTriggerSettingEnabled(Context context, int userId) { @@ -441,6 +508,28 @@ public class GestureLauncherService extends SystemService { isDefaultEmergencyGestureEnabled(context.getResources()) ? 1 : 0, userId) != 0; } + private static int getDoubleTapPowerGestureAction(Context context, int userId) { + return Settings.Secure.getIntForUser( + context.getContentResolver(), + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE, + LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER, + userId); + } + + /** Whether the shortcut to launch app on power double press is enabled. */ + private static boolean isDoubleTapPowerGestureSettingEnabled(Context context, int userId) { + return Settings.Secure.getIntForUser( + context.getContentResolver(), + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED, + isDoubleTapConfigEnabled(context.getResources()) ? 1 : 0, + userId) + == 1; + } + + private static boolean isDoubleTapConfigEnabled(Resources resources) { + return resources.getBoolean(R.bool.config_doubleTapPowerGestureEnabled); + } + /** * Gets power button cooldown period in milliseconds after emergency gesture is triggered. The * value is capped at a maximum @@ -494,10 +583,56 @@ public class GestureLauncherService extends SystemService { * Whether GestureLauncherService should be enabled according to system properties. */ public static boolean isGestureLauncherEnabled(Resources resources) { - return isCameraLaunchEnabled(resources) - || isCameraDoubleTapPowerEnabled(resources) - || isCameraLiftTriggerEnabled(resources) - || isEmergencyGestureEnabled(resources); + boolean res = + isCameraLaunchEnabled(resources) + || isCameraLiftTriggerEnabled(resources) + || isEmergencyGestureEnabled(resources); + if (launchWalletOptionOnPowerDoubleTap()) { + res |= isDoubleTapConfigEnabled(resources); + } else { + res |= isCameraDoubleTapPowerEnabled(resources); + } + return res; + } + + /** + * Processes a power key event in GestureLauncherService without performing an action. This + * method is called on every KEYCODE_POWER ACTION_DOWN event and ensures that, even if + * KEYCODE_POWER events are passed to and handled by the app, the GestureLauncherService still + * keeps track of all running KEYCODE_POWER events for its gesture detection and relevant + * actions. + */ + public void processPowerKeyDown(KeyEvent event) { + if (mEmergencyGestureEnabled && mEmergencyGesturePowerButtonCooldownPeriodMs >= 0 + && event.getEventTime() - mLastEmergencyGestureTriggered + < mEmergencyGesturePowerButtonCooldownPeriodMs) { + return; + } + if (event.isLongPress()) { + return; + } + + final long powerTapInterval; + + synchronized (this) { + powerTapInterval = event.getEventTime() - mLastPowerDown; + mLastPowerDown = event.getEventTime(); + if (powerTapInterval >= POWER_SHORT_TAP_SEQUENCE_MAX_INTERVAL_MS) { + // Tap too slow, reset consecutive tap counts. + mFirstPowerDown = event.getEventTime(); + mPowerButtonConsecutiveTaps = 1; + mPowerButtonSlowConsecutiveTaps = 1; + } else if (powerTapInterval >= POWER_DOUBLE_TAP_MAX_TIME_MS) { + // Tap too slow for shortcuts + mFirstPowerDown = event.getEventTime(); + mPowerButtonConsecutiveTaps = 1; + mPowerButtonSlowConsecutiveTaps++; + } else if (powerTapInterval > 0) { + // Fast consecutive tap + mPowerButtonConsecutiveTaps++; + mPowerButtonSlowConsecutiveTaps++; + } + } } /** @@ -507,8 +642,8 @@ public class GestureLauncherService extends SystemService { * @param outLaunched true if some action is taken as part of the key intercept (eg, app launch) * @return true if the key down event is intercepted */ - public boolean interceptPowerKeyDown(KeyEvent event, boolean interactive, - MutableBoolean outLaunched) { + public boolean interceptPowerKeyDown( + KeyEvent event, boolean interactive, MutableBoolean outLaunched) { if (mEmergencyGestureEnabled && mEmergencyGesturePowerButtonCooldownPeriodMs >= 0 && event.getEventTime() - mLastEmergencyGestureTriggered < mEmergencyGesturePowerButtonCooldownPeriodMs) { @@ -530,6 +665,7 @@ public class GestureLauncherService extends SystemService { return false; } boolean launchCamera = false; + boolean launchWallet = false; boolean launchEmergencyGesture = false; boolean intercept = false; long powerTapInterval; @@ -541,12 +677,12 @@ public class GestureLauncherService extends SystemService { mFirstPowerDown = event.getEventTime(); mPowerButtonConsecutiveTaps = 1; mPowerButtonSlowConsecutiveTaps = 1; - } else if (powerTapInterval >= CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS) { + } else if (powerTapInterval >= POWER_DOUBLE_TAP_MAX_TIME_MS) { // Tap too slow for shortcuts mFirstPowerDown = event.getEventTime(); mPowerButtonConsecutiveTaps = 1; mPowerButtonSlowConsecutiveTaps++; - } else { + } else if (!overridePowerKeyBehaviorInFocusedWindow() || powerTapInterval > 0) { // Fast consecutive tap mPowerButtonConsecutiveTaps++; mPowerButtonSlowConsecutiveTaps++; @@ -586,10 +722,16 @@ public class GestureLauncherService extends SystemService { } } if (mCameraDoubleTapPowerEnabled - && powerTapInterval < CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - && mPowerButtonConsecutiveTaps == CAMERA_POWER_TAP_COUNT_THRESHOLD) { + && powerTapInterval < POWER_DOUBLE_TAP_MAX_TIME_MS + && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD) { launchCamera = true; intercept = interactive; + } else if (launchWalletOptionOnPowerDoubleTap() + && mWalletDoubleTapPowerEnabled + && powerTapInterval < POWER_DOUBLE_TAP_MAX_TIME_MS + && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD) { + launchWallet = true; + intercept = interactive; } } if (mPowerButtonConsecutiveTaps > 1 || mPowerButtonSlowConsecutiveTaps > 1) { @@ -608,6 +750,10 @@ public class GestureLauncherService extends SystemService { (int) powerTapInterval); mUiEventLogger.log(GestureLauncherEvent.GESTURE_CAMERA_DOUBLE_TAP_POWER); } + } else if (launchWallet) { + Slog.i(TAG, "Power button double tap gesture detected, launching wallet. Interval=" + + powerTapInterval + "ms"); + launchWallet = sendGestureTargetActivityPendingIntent(); } else if (launchEmergencyGesture) { Slog.i(TAG, "Emergency gesture detected, launching."); launchEmergencyGesture = handleEmergencyGesture(); @@ -623,11 +769,74 @@ public class GestureLauncherService extends SystemService { mPowerButtonSlowConsecutiveTaps); mMetricsLogger.histogram("power_double_tap_interval", (int) powerTapInterval); - outLaunched.value = launchCamera || launchEmergencyGesture; + outLaunched.value = launchCamera || launchEmergencyGesture || launchWallet; // Intercept power key event if the press is part of a gesture (camera, eGesture) and the // user has completed setup. return intercept && isUserSetupComplete(); } + + /** + * Fetches and sends gestureTargetActivityPendingIntent from QuickAccessWallet, which is a + * specific activity that QuickAccessWalletService has defined to be launch on detection of the + * power button gesture. + */ + private boolean sendGestureTargetActivityPendingIntent() { + boolean userSetupComplete = isUserSetupComplete(); + if (mQuickAccessWalletClient == null + || !mQuickAccessWalletClient.isWalletServiceAvailable()) { + Slog.w(TAG, "QuickAccessWalletService is not available, ignoring wallet gesture."); + return false; + } + + if (!userSetupComplete) { + if (DBG) { + Slog.d(TAG, "userSetupComplete = false, ignoring wallet gesture."); + } + return false; + } + if (DBG) { + Slog.d(TAG, "userSetupComplete = true, performing wallet gesture."); + } + + mQuickAccessWalletClient.getGestureTargetActivityPendingIntent( + getContext().getMainExecutor(), + gesturePendingIntent -> { + if (gesturePendingIntent == null) { + Slog.d(TAG, "getGestureTargetActivityPendingIntent is null."); + sendFallbackPendingIntent(); + return; + } + sendPendingIntentWithBackgroundStartPrivileges(gesturePendingIntent); + }); + return true; + } + + /** + * If gestureTargetActivityPendingIntent is null, this method is invoked to start the activity + * that QuickAccessWalletService has defined to host the Wallet view, which is typically the + * home screen of the Wallet application. + */ + private void sendFallbackPendingIntent() { + mQuickAccessWalletClient.getWalletPendingIntent( + getContext().getMainExecutor(), + walletPendingIntent -> { + if (walletPendingIntent == null) { + Slog.w(TAG, "getWalletPendingIntent returns null. Not launching " + + "anything for wallet."); + return; + } + sendPendingIntentWithBackgroundStartPrivileges(walletPendingIntent); + }); + } + + private void sendPendingIntentWithBackgroundStartPrivileges(PendingIntent pendingIntent) { + try { + pendingIntent.send(GRANT_BACKGROUND_START_PRIVILEGES); + } catch (PendingIntent.CanceledException e) { + Slog.e(TAG, "PendingIntent was canceled", e); + } + } + /** * @return true if camera was launched, false otherwise. */ @@ -700,31 +909,39 @@ public class GestureLauncherService extends SystemService { Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0; } - private final BroadcastReceiver mUserReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) { - mUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); - mContext.getContentResolver().unregisterContentObserver(mSettingObserver); - registerContentObservers(); - updateCameraRegistered(); - updateCameraDoubleTapPowerEnabled(); - updateEmergencyGestureEnabled(); - updateEmergencyGesturePowerButtonCooldownPeriodMs(); - } - } - }; - - private final ContentObserver mSettingObserver = new ContentObserver(new Handler()) { - public void onChange(boolean selfChange, android.net.Uri uri, int userId) { - if (userId == mUserId) { - updateCameraRegistered(); - updateCameraDoubleTapPowerEnabled(); - updateEmergencyGestureEnabled(); - updateEmergencyGesturePowerButtonCooldownPeriodMs(); - } - } - }; + private final BroadcastReceiver mUserReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) { + mUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); + mContext.getContentResolver().unregisterContentObserver(mSettingObserver); + registerContentObservers(); + updateCameraRegistered(); + updateCameraDoubleTapPowerEnabled(); + if (launchWalletOptionOnPowerDoubleTap()) { + updateWalletDoubleTapPowerEnabled(); + } + updateEmergencyGestureEnabled(); + updateEmergencyGesturePowerButtonCooldownPeriodMs(); + } + } + }; + + private final ContentObserver mSettingObserver = + new ContentObserver(new Handler()) { + public void onChange(boolean selfChange, android.net.Uri uri, int userId) { + if (userId == mUserId) { + updateCameraRegistered(); + updateCameraDoubleTapPowerEnabled(); + if (launchWalletOptionOnPowerDoubleTap()) { + updateWalletDoubleTapPowerEnabled(); + } + updateEmergencyGestureEnabled(); + updateEmergencyGesturePowerButtonCooldownPeriodMs(); + } + } + }; private final class GestureEventListener implements SensorEventListener { @Override diff --git a/services/core/java/com/android/server/am/BroadcastFilter.java b/services/core/java/com/android/server/am/BroadcastFilter.java index 05aeb42dbc9f..83276391493f 100644 --- a/services/core/java/com/android/server/am/BroadcastFilter.java +++ b/services/core/java/com/android/server/am/BroadcastFilter.java @@ -19,7 +19,6 @@ package com.android.server.am; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledSince; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.os.Binder; @@ -41,7 +40,6 @@ public final class BroadcastFilter extends IntentFilter { * ({@link IntentFilter#SYSTEM_LOW_PRIORITY}, {@link IntentFilter#SYSTEM_HIGH_PRIORITY}). */ @ChangeId - @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) @VisibleForTesting static final long RESTRICT_PRIORITY_VALUES = 371309185L; diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index a1ab1eea3d3e..8d0805d3fa13 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -43,7 +43,6 @@ import android.app.BackgroundStartPrivileges; import android.app.BroadcastOptions; import android.app.BroadcastOptions.DeliveryGroupPolicy; import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledSince; import android.content.ComponentName; import android.content.IIntentReceiver; import android.content.Intent; @@ -86,7 +85,6 @@ final class BroadcastRecord extends Binder { * will only influence the order of broadcast delivery within the same process. */ @ChangeId - @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) @VisibleForTesting static final long LIMIT_PRIORITY_SCOPE = 371307720L; diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index 251344395ae3..5d9db65fe2b2 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java @@ -19,17 +19,19 @@ package com.android.server.devicestate; import static android.Manifest.permission.CONTROL_DEVICE_STATE; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static android.frameworks.devicestate.DeviceStateConfiguration.DeviceStatePropertyValue.FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED; -import static android.frameworks.devicestate.DeviceStateConfiguration.DeviceStatePropertyValue.FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN; -import static android.frameworks.devicestate.DeviceStateConfiguration.DeviceStatePropertyValue.FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN; import static android.frameworks.devicestate.DeviceStateConfiguration.DeviceStatePropertyValue.FEATURE_DUAL_DISPLAY; import static android.frameworks.devicestate.DeviceStateConfiguration.DeviceStatePropertyValue.FEATURE_REAR_DISPLAY; import static android.frameworks.devicestate.DeviceStateConfiguration.DeviceStatePropertyValue.FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY; import static android.frameworks.devicestate.DeviceStateConfiguration.DeviceStatePropertyValue.FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY; +import static android.frameworks.devicestate.DeviceStateConfiguration.DeviceStatePropertyValue.FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED; +import static android.frameworks.devicestate.DeviceStateConfiguration.DeviceStatePropertyValue.FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN; +import static android.frameworks.devicestate.DeviceStateConfiguration.DeviceStatePropertyValue.FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN; import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT; import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY; +import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT; import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY; import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY; +import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED; import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_AVAILABLE_FOR_APP_REQUEST; import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS; import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP; @@ -98,7 +100,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.WeakHashMap; @@ -854,7 +855,7 @@ public final class DeviceStateManagerService extends SystemService { } } - private void requestStateInternal(int state, int flags, int callingPid, int callingUid, + private void requestStateInternal(int requestedState, int flags, int callingPid, int callingUid, @NonNull IBinder token, boolean hasControlDeviceStatePermission) { synchronized (mLock) { final ProcessRecord processRecord = mProcessRecords.get(callingPid); @@ -869,19 +870,30 @@ public final class DeviceStateManagerService extends SystemService { + " token: " + token); } - final Optional<DeviceState> deviceState = getStateLocked(state); - if (!deviceState.isPresent()) { - throw new IllegalArgumentException("Requested state: " + state + final Optional<DeviceState> requestedDeviceState = getStateLocked(requestedState); + if (requestedDeviceState.isEmpty()) { + throw new IllegalArgumentException("Requested state: " + requestedState + " is not supported."); } - OverrideRequest request = new OverrideRequest(token, callingPid, callingUid, - deviceState.get(), flags, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); + final OverrideRequest request = new OverrideRequest(token, callingPid, callingUid, + requestedDeviceState.get(), flags, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); if (Flags.deviceStatePropertyMigration()) { - // If we don't have the CONTROL_DEVICE_STATE permission, we want to show the overlay - if (!hasControlDeviceStatePermission && deviceState.get().hasProperty( - PROPERTY_FEATURE_REAR_DISPLAY)) { + final boolean isRequestingRdm = requestedDeviceState.get() + .hasProperty(PROPERTY_FEATURE_REAR_DISPLAY); + final boolean isRequestingRdmOuterDefault = requestedDeviceState.get() + .hasProperty(PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT); + + final boolean isDeviceClosed = mCommittedState.isEmpty() ? false + : mCommittedState.get().hasProperty( + PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED); + + final boolean shouldShowRdmEduDialog = isRequestingRdm && shouldShowRdmEduDialog( + hasControlDeviceStatePermission, isRequestingRdmOuterDefault, + isDeviceClosed); + + if (shouldShowRdmEduDialog) { showRearDisplayEducationalOverlayLocked(request); } else { mOverrideRequestController.addRequest(request); @@ -889,7 +901,7 @@ public final class DeviceStateManagerService extends SystemService { } else { // If we don't have the CONTROL_DEVICE_STATE permission, we want to show the overlay if (!hasControlDeviceStatePermission && mRearDisplayState != null - && state == mRearDisplayState.getIdentifier()) { + && requestedState == mRearDisplayState.getIdentifier()) { showRearDisplayEducationalOverlayLocked(request); } else { mOverrideRequestController.addRequest(request); @@ -899,6 +911,28 @@ public final class DeviceStateManagerService extends SystemService { } /** + * Determines if the system should show an educational dialog before entering rear display mode + * @param hasControlDeviceStatePermission If the app has the CONTROL_DEVICE_STATE permission, we + * don't need to show the overlay + * @param requestingRdmOuterDefault True if the system is requesting + * PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT + * @param isDeviceClosed True if the device is closed (folded) when the request was made + */ + @VisibleForTesting + static boolean shouldShowRdmEduDialog(boolean hasControlDeviceStatePermission, + boolean requestingRdmOuterDefault, boolean isDeviceClosed) { + if (hasControlDeviceStatePermission) { + return false; + } + + if (requestingRdmOuterDefault) { + return isDeviceClosed; + } else { + return true; + } + } + + /** * If we get a request to enter rear display mode, we need to display an educational * overlay to let the user know what will happen. This calls into the * {@link StatusBarManagerInternal} to notify SystemUI to display the educational dialog. diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index 4a533f42ffea..ef73463122ff 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -186,7 +186,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** Invoked when the underlying binder of this broker has died at the client process. */ @Override public void binderDied() { - unregister(); + if (mIsRegistered.get()) { + unregister(); + } } /* package */ void attachDeathRecipient() throws RemoteException { diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java index 8c5095f35f0d..155a92a21368 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java @@ -65,8 +65,12 @@ import java.util.concurrent.ConcurrentHashMap; /** Variables for managing endpoint ID creation */ private final Object mEndpointLock = new Object(); + /** + * The next available endpoint ID to register. Per EndpointId.aidl definition, dynamic + * endpoint IDs must have the left-most bit as 1, and the values 0/-1 are invalid. + */ @GuardedBy("mEndpointLock") - private long mNextEndpointId = 0; + private long mNextEndpointId = -2; /** The minimum session ID reservable by endpoints (retrieved from HAL) */ private final int mMinSessionId; @@ -282,10 +286,10 @@ import java.util.concurrent.ConcurrentHashMap; /** @return an available endpoint ID */ private long getNewEndpointId() { synchronized (mEndpointLock) { - if (mNextEndpointId == Long.MAX_VALUE) { + if (mNextEndpointId >= 0) { throw new IllegalStateException("Too many endpoints connected"); } - return mNextEndpointId++; + return mNextEndpointId--; } } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index f1a481155458..d5e4c3f1eb9f 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -88,10 +88,12 @@ import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGest import static com.android.hardware.input.Flags.inputManagerLifecycleSupport; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.hardware.input.Flags.modifierShortcutDump; +import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; +import static com.android.server.GestureLauncherService.DOUBLE_POWER_TAP_COUNT_THRESHOLD; import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; -import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_UNCOVERED; @@ -432,6 +434,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { "android.intent.action.VOICE_ASSIST_RETAIL"; /** + * Maximum amount of time in milliseconds between consecutive power onKeyDown events to be + * considered a multi-press, only used for the power button. + * Note: To maintain backwards compatibility for the power button, we are measuring the times + * between consecutive down events instead of the first tap's up event and the second tap's + * down event. + */ + @VisibleForTesting public static final int POWER_MULTI_PRESS_TIMEOUT_MILLIS = + ViewConfiguration.getMultiPressTimeout(); + + /** * Lock protecting internal state. Must not call out into window * manager with lock held. (This lock will be acquired in places * where the window manager is calling in with its own lock held.) @@ -492,6 +504,32 @@ public class PhoneWindowManager implements WindowManagerPolicy { private WindowWakeUpPolicy mWindowWakeUpPolicy; + /** + * The three variables below are used for custom power key gesture detection in + * PhoneWindowManager. They are used to detect when the power button has been double pressed + * and, when it does happen, makes the behavior overrideable by the app. + * + * We cannot use the {@link PowerKeyRule} for this because multi-press power gesture detection + * and behaviors are handled by {@link com.android.server.GestureLauncherService}, and the + * {@link PowerKeyRule} only handles single and long-presses of the power button. As a result, + * overriding the double tap behavior requires custom gesture detection here that mimics the + * logic in {@link com.android.server.GestureLauncherService}. + * + * Long-term, it would be beneficial to move all power gesture detection to + * {@link PowerKeyRule} so that this custom logic isn't required. + */ + // Time of last power down event. + private long mLastPowerDown; + + // Number of power button events consecutively triggered (within a specific timeout threshold). + private int mPowerButtonConsecutiveTaps = 0; + + // Whether a double tap of the power button has been detected. + volatile boolean mDoubleTapPowerDetected; + + // Runnable that is queued on a delay when the first power keyDown event is sent to the app. + private Runnable mPowerKeyDelayedRunnable = null; + boolean mSafeMode; // Whether to allow dock apps with METADATA_DOCK_HOME to temporarily take over the Home key. @@ -1097,6 +1135,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { mPowerKeyHandled = mPowerKeyHandled || hungUp || handledByPowerManager || isKeyGestureTriggered || mKeyCombinationManager.isPowerKeyIntercepted(); + + if (overridePowerKeyBehaviorInFocusedWindow()) { + mPowerKeyHandled |= mDoubleTapPowerDetected; + } + if (!mPowerKeyHandled) { if (!interactive) { wakeUpFromWakeKey(event); @@ -2669,7 +2712,19 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mShouldEarlyShortPressOnPower) { return; } - powerPress(downTime, 1 /*count*/, displayId); + // TODO(b/380433365): Remove deferring single power press action when refactoring. + if (overridePowerKeyBehaviorInFocusedWindow()) { + mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); + mDeferredKeyActionExecutor.queueKeyAction( + KEYCODE_POWER, + downTime, + () -> { + powerPress(downTime, 1 /*count*/, displayId); + }); + } else { + powerPress(downTime, 1 /*count*/, displayId); + } + } @Override @@ -2700,7 +2755,17 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override void onMultiPress(long downTime, int count, int displayId) { - powerPress(downTime, count, displayId); + if (overridePowerKeyBehaviorInFocusedWindow()) { + mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); + mDeferredKeyActionExecutor.queueKeyAction( + KEYCODE_POWER, + downTime, + () -> { + powerPress(downTime, count, displayId); + }); + } else { + powerPress(downTime, count, displayId); + } } @Override @@ -3477,6 +3542,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + if (overridePowerKeyBehaviorInFocusedWindow() && event.getKeyCode() == KEYCODE_POWER + && event.getAction() == KeyEvent.ACTION_UP + && mDoubleTapPowerDetected) { + mDoubleTapPowerDetected = false; + } + return needToConsumeKey ? keyConsumed : keyNotConsumed; } @@ -3992,6 +4063,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } + case KeyEvent.KEYCODE_POWER: + return interceptPowerKeyBeforeDispatching(focusedToken, event); case KeyEvent.KEYCODE_SCREENSHOT: if (firstDown) { interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); @@ -4047,6 +4120,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } + case KeyEvent.KEYCODE_POWER: + return interceptPowerKeyBeforeDispatching(focusedToken, event); } if (isValidGlobalKey(keyCode) && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) { @@ -4057,6 +4132,90 @@ public class PhoneWindowManager implements WindowManagerPolicy { return (metaState & KeyEvent.META_META_ON) != 0; } + /** + * Called by interceptKeyBeforeDispatching to handle interception logic for KEYCODE_POWER + * KeyEvents. + * + * @return true if intercepting the key, false if sending to app. + */ + private boolean interceptPowerKeyBeforeDispatching(IBinder focusedToken, KeyEvent event) { + if (!overridePowerKeyBehaviorInFocusedWindow()) { + //Flag disabled: intercept the power key and do not send to app. + return true; + } + if (event.getKeyCode() != KEYCODE_POWER) { + Log.wtf(TAG, "interceptPowerKeyBeforeDispatching received a non-power KeyEvent " + + "with key code: " + event.getKeyCode()); + return false; + } + + // Intercept keys (don't send to app) for 3x, 4x, 5x gestures) + if (mPowerButtonConsecutiveTaps > DOUBLE_POWER_TAP_COUNT_THRESHOLD) { + setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime()); + return true; + } + + // UP key; just reuse the original decision. + if (event.getAction() == KeyEvent.ACTION_UP) { + final Set<Integer> consumedKeys = mConsumedKeysForDevice.get(event.getDeviceId()); + return consumedKeys != null + && consumedKeys.contains(event.getKeyCode()); + } + + KeyInterceptionInfo info = + mWindowManagerInternal.getKeyInterceptionInfoFromToken(focusedToken); + + if (info == null || !mButtonOverridePermissionChecker.canWindowOverridePowerKey(mContext, + info.windowOwnerUid, info.inputFeaturesFlags)) { + // The focused window does not have the permission to override power key behavior. + if (DEBUG_INPUT) { + String interceptReason = ""; + if (info == null) { + interceptReason = "Window is null"; + } else if (!mButtonOverridePermissionChecker.canAppOverrideSystemKey(mContext, + info.windowOwnerUid)) { + interceptReason = "Application does not have " + + "OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission"; + } else { + interceptReason = "Window does not have inputFeatureFlag set"; + } + + Log.d(TAG, String.format("Intercepting KEYCODE_POWER event. action=%d, " + + "eventTime=%d to window=%s. interceptReason=%s. " + + "mDoubleTapPowerDetected=%b", + event.getAction(), event.getEventTime(), (info != null) + ? info.windowTitle : "null", interceptReason, + mDoubleTapPowerDetected)); + } + // Intercept the key (i.e. do not send to app) + setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime()); + return true; + } + + if (DEBUG_INPUT) { + Log.d(TAG, String.format("Sending KEYCODE_POWER to app. action=%d, " + + "eventTime=%d to window=%s. mDoubleTapPowerDetected=%b", + event.getAction(), event.getEventTime(), info.windowTitle, + mDoubleTapPowerDetected)); + } + + if (!mDoubleTapPowerDetected) { + //Single press: post a delayed runnable for the single press power action that will be + // called if it's not cancelled by a double press. + final var downTime = event.getDownTime(); + mPowerKeyDelayedRunnable = () -> + setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, downTime); + mHandler.postDelayed(mPowerKeyDelayedRunnable, POWER_MULTI_PRESS_TIMEOUT_MILLIS); + } else if (mPowerKeyDelayedRunnable != null) { + //Double press detected: cancel the single press runnable. + mHandler.removeCallbacks(mPowerKeyDelayedRunnable); + mPowerKeyDelayedRunnable = null; + } + + // Focused window has permission. Send to app. + return false; + } + @SuppressLint("MissingPermission") private void initKeyGestures() { if (!useKeyGestureEventHandler()) { @@ -4633,6 +4792,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } + if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) { + handleUnhandledSystemKey(event); + return true; + } + if (useKeyGestureEventHandler()) { return false; } @@ -5467,8 +5631,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { KeyEvent.actionToString(event.getAction()), mPowerKeyHandled ? 1 : 0, mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER)); - // Any activity on the power button stops the accessibility shortcut - result &= ~ACTION_PASS_TO_USER; + if (overridePowerKeyBehaviorInFocusedWindow()) { + result |= ACTION_PASS_TO_USER; + } else { + // Any activity on the power button stops the accessibility shortcut + result &= ~ACTION_PASS_TO_USER; + } + isWakeKey = false; // wake-up will be handled separately if (down) { interceptPowerKeyDown(event, interactiveAndAwake, isKeyGestureTriggered); @@ -5730,6 +5899,35 @@ public class PhoneWindowManager implements WindowManagerPolicy { } if (event.getKeyCode() == KEYCODE_POWER && event.getAction() == KeyEvent.ACTION_DOWN) { + if (overridePowerKeyBehaviorInFocusedWindow()) { + if (event.getRepeatCount() > 0) { + return; + } + if (mGestureLauncherService != null) { + mGestureLauncherService.processPowerKeyDown(event); + } + + if (detectDoubleTapPower(event)) { + mDoubleTapPowerDetected = true; + + // Copy of the event for handler in case the original event gets recycled. + KeyEvent eventCopy = KeyEvent.obtain(event); + mDeferredKeyActionExecutor.queueKeyAction( + KeyEvent.KEYCODE_POWER, + eventCopy.getEventTime(), + () -> { + if (!handleCameraGesture(eventCopy, interactive)) { + mSingleKeyGestureDetector.interceptKey( + eventCopy, interactive, defaultDisplayOn); + } else { + mSingleKeyGestureDetector.reset(); + } + eventCopy.recycle(); + }); + return; + } + } + mPowerKeyHandled = handleCameraGesture(event, interactive); if (mPowerKeyHandled) { // handled by camera gesture. @@ -5741,6 +5939,25 @@ public class PhoneWindowManager implements WindowManagerPolicy { mSingleKeyGestureDetector.interceptKey(event, interactive, defaultDisplayOn); } + private boolean detectDoubleTapPower(KeyEvent event) { + if (event.getKeyCode() != KEYCODE_POWER || event.getAction() != KeyEvent.ACTION_DOWN + || event.getRepeatCount() != 0) { + return false; + } + + final long powerTapInterval = event.getEventTime() - mLastPowerDown; + mLastPowerDown = event.getEventTime(); + if (powerTapInterval >= POWER_MULTI_PRESS_TIMEOUT_MILLIS) { + // Tap too slow for double press + mPowerButtonConsecutiveTaps = 1; + } else { + mPowerButtonConsecutiveTaps++; + } + + return powerTapInterval < POWER_MULTI_PRESS_TIMEOUT_MILLIS + && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD; + } + // The camera gesture will be detected by GestureLauncherService. private boolean handleCameraGesture(KeyEvent event, boolean interactive) { // camera gesture. @@ -7597,6 +7814,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { null) == PERMISSION_GRANTED; } + + boolean canWindowOverridePowerKey(Context context, int uid, int inputFeaturesFlags) { + return canAppOverrideSystemKey(context, uid) + && (inputFeaturesFlags & WindowManager.LayoutParams + .INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) != 0; + } } private int getTargetDisplayIdForKeyEvent(KeyEvent event) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 810aa0454246..c8befb21fe13 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3864,6 +3864,9 @@ class Task extends TaskFragment { if (mLaunchAdjacentDisabled) { pw.println(prefix + "mLaunchAdjacentDisabled=true"); } + if (mReparentLeafTaskIfRelaunch) { + pw.println(prefix + "mReparentLeafTaskIfRelaunch=true"); + } } @Override diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 2c71c1a1f4f3..c1a33c468cee 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -31,6 +31,7 @@ import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.Display.INVALID_DISPLAY; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ORIENTATION; +import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TASKS; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerService.TAG_ROOT_TASK; import static com.android.server.wm.DisplayContent.alwaysCreateRootTask; @@ -918,6 +919,10 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { if (candidateTask.getParent() == null) { addChild(candidateTask, position); } else { + if (candidateTask.getRootTask().mReparentLeafTaskIfRelaunch) { + ProtoLog.d(WM_DEBUG_TASKS, "Reparenting to display area on relaunch: " + + "rootTaskId=%d toTop=%b", candidateTask.mTaskId, onTop); + } candidateTask.reparent(this, onTop); } } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index cebe790bb1b9..447d443282bd 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5750,9 +5750,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP || mKeyInterceptionInfo.layoutParamsPrivateFlags != getAttrs().privateFlags || mKeyInterceptionInfo.layoutParamsType != getAttrs().type || mKeyInterceptionInfo.windowTitle != getWindowTag() - || mKeyInterceptionInfo.windowOwnerUid != getOwningUid()) { + || mKeyInterceptionInfo.windowOwnerUid != getOwningUid() + || mKeyInterceptionInfo.inputFeaturesFlags != getAttrs().inputFeatures) { mKeyInterceptionInfo = new KeyInterceptionInfo(getAttrs().type, getAttrs().privateFlags, - getWindowTag().toString(), getOwningUid()); + getWindowTag().toString(), getOwningUid(), getAttrs().inputFeatures); } return mKeyInterceptionInfo; } diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java index 8024915692aa..9850cb09ae2b 100644 --- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java @@ -16,7 +16,12 @@ package com.android.server; -import static com.android.server.GestureLauncherService.CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; +import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP; +import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap; + +import static com.android.server.GestureLauncherService.LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER; +import static com.android.server.GestureLauncherService.LAUNCH_WALLET_ON_DOUBLE_TAP_POWER; +import static com.android.server.GestureLauncherService.POWER_DOUBLE_TAP_MAX_TIME_MS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -24,19 +29,27 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.PendingIntent; import android.app.StatusBarManager; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Resources; import android.os.Looper; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; +import android.service.quickaccesswallet.QuickAccessWalletClient; import android.telecom.TelecomManager; import android.test.mock.MockContentResolver; import android.testing.TestableLooper; @@ -55,6 +68,7 @@ import com.android.server.statusbar.StatusBarManagerInternal; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -62,6 +76,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * Unit tests for {@link GestureLauncherService}. @@ -90,9 +106,32 @@ public class GestureLauncherServiceTest { private @Mock TelecomManager mTelecomManager; private @Mock MetricsLogger mMetricsLogger; @Mock private UiEventLogger mUiEventLogger; + @Mock private QuickAccessWalletClient mQuickAccessWalletClient; private MockContentResolver mContentResolver; private GestureLauncherService mGestureLauncherService; + private Context mInstrumentationContext = + InstrumentationRegistry.getInstrumentation().getContext(); + + private static final String LAUNCH_TEST_WALLET_ACTION = "LAUNCH_TEST_WALLET_ACTION"; + private static final String LAUNCH_FALLBACK_ACTION = "LAUNCH_FALLBACK_ACTION"; + private PendingIntent mGesturePendingIntent = + PendingIntent.getBroadcast( + mInstrumentationContext, + 0, + new Intent(LAUNCH_TEST_WALLET_ACTION), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + + private PendingIntent mFallbackPendingIntent = + PendingIntent.getBroadcast( + mInstrumentationContext, + 0, + new Intent(LAUNCH_FALLBACK_ACTION), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @BeforeClass public static void oneTimeInitialization() { if (Looper.myLooper() == null) { @@ -115,9 +154,49 @@ public class GestureLauncherServiceTest { when(mContext.getContentResolver()).thenReturn(mContentResolver); when(mContext.getSystemService(Context.TELECOM_SERVICE)).thenReturn(mTelecomManager); when(mTelecomManager.createLaunchEmergencyDialerIntent(null)).thenReturn(new Intent()); + when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(true); + + mGestureLauncherService = + new GestureLauncherService( + mContext, mMetricsLogger, mQuickAccessWalletClient, mUiEventLogger); + + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } - mGestureLauncherService = new GestureLauncherService(mContext, mMetricsLogger, - mUiEventLogger); + private WalletLaunchedReceiver registerWalletLaunchedReceiver(String action) { + IntentFilter filter = new IntentFilter(action); + WalletLaunchedReceiver receiver = new WalletLaunchedReceiver(); + mInstrumentationContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED); + return receiver; + } + + /** + * A simple {@link BroadcastReceiver} implementation that counts down a {@link CountDownLatch} + * when a matching message is received + */ + private static final class WalletLaunchedReceiver extends BroadcastReceiver { + private static final int TIMEOUT_SECONDS = 3; + + private final CountDownLatch mLatch; + + WalletLaunchedReceiver() { + mLatch = new CountDownLatch(1); + } + + @Override + public void onReceive(Context context, Intent intent) { + mLatch.countDown(); + context.unregisterReceiver(this); + } + + Boolean waitUntilShown() { + try { + return mLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return false; + } + } } @Test @@ -134,37 +213,123 @@ public class GestureLauncherServiceTest { @Test public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingDisabled() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(false); + withDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingEnabled() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(0); - assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( - mContext, FAKE_USER_ID)); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(false); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(0); + assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } } @Test public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingDisabled() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(1); + } assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingEnabled() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(0); + } assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_actionWallet() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertFalse( + mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertTrue( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled_configDisabled() { + withDoubleTapPowerEnabledConfigValue(false); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertTrue( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled_settingDisabled() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertFalse( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled_actionCamera() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + + assertFalse( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test public void testIsEmergencyGestureSettingEnabled_settingDisabled() { withEmergencyGestureEnabledConfigValue(true); withEmergencyGestureEnabledSettingValue(false); @@ -245,12 +410,9 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_firstPowerDownCameraPowerGestureOnInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); - long eventTime = INITIAL_EVENT_TIME_MILLIS + - CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + long eventTime = INITIAL_EVENT_TIME_MILLIS + POWER_DOUBLE_TAP_MAX_TIME_MS - 1; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); boolean interactive = true; @@ -284,8 +446,12 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerGestureEnableSettingValue(false); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -298,7 +464,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -329,8 +495,12 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOffInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerGestureEnableSettingValue(false); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -343,7 +513,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -422,10 +592,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnInteractiveSetupComplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); - withUserSetupCompleteValue(true); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -437,7 +604,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -470,15 +637,145 @@ public class GestureLauncherServiceTest { } @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void + testInterceptPowerKeyDown_fiveInboundPresses_walletAndEmergencyEnabled_bothLaunch() { + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableEmergencyGesture(); + enableWalletGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + assertTrue(receiver.waitUntilShown()); + + // Presses 3 and 4 should not trigger any gesture + for (int i = 0; i < 2; i++) { + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false); + } + + // Fifth button press should trigger the emergency flow + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + verify(mUiEventLogger, times(1)) + .log(GestureLauncherService.GestureLauncherEvent.GESTURE_EMERGENCY_TAP_POWER); + verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected(); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_intervalInBoundsWalletPowerGesture() { + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableWalletGesture(); + enableEmergencyGesture(); + + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + assertTrue(receiver.waitUntilShown()); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_walletGestureOn_quickAccessWalletServiceUnavailable() { + when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(false); + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableWalletGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false); + + assertFalse(receiver.waitUntilShown()); + } + + @Test + public void testInterceptPowerKeyDown_walletGestureOn_userSetupIncomplete() { + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableWalletGesture(); + withUserSetupCompleteValue(false); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + assertFalse(receiver.waitUntilShown()); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_walletPowerGesture_nullPendingIntent() { + WalletLaunchedReceiver gestureReceiver = + registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(null); + WalletLaunchedReceiver fallbackReceiver = + registerWalletLaunchedReceiver(LAUNCH_FALLBACK_ACTION); + setUpWalletFallbackPendingIntent(mFallbackPendingIntent); + enableWalletGesture(); + enableEmergencyGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + assertFalse(gestureReceiver.waitUntilShown()); + assertTrue(fallbackReceiver.waitUntilShown()); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_walletPowerGesture_intervalOutOfBounds() { + WalletLaunchedReceiver gestureReceiver = + registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(null); + WalletLaunchedReceiver fallbackReceiver = + registerWalletLaunchedReceiver(LAUNCH_FALLBACK_ACTION); + setUpWalletFallbackPendingIntent(mFallbackPendingIntent); + enableWalletGesture(); + enableEmergencyGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + assertFalse(gestureReceiver.waitUntilShown()); + assertFalse(fallbackReceiver.waitUntilShown()); + } + + @Test public void testInterceptPowerKeyDown_fiveInboundPresses_cameraAndEmergencyEnabled_bothLaunch() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - withEmergencyGestureEnabledConfigValue(true); - withEmergencyGestureEnabledSettingValue(true); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); - mGestureLauncherService.updateEmergencyGestureEnabled(); - withUserSetupCompleteValue(true); + enableCameraGesture(); + enableEmergencyGesture(); // First button press does nothing long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -491,7 +788,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; // 2nd button triggers camera eventTime += interval; @@ -580,7 +877,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; // 3 more button presses which should not trigger any gesture (camera gesture disabled) for (int i = 0; i < 3; i++) { eventTime += interval; @@ -634,7 +931,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; // 3 more button presses which should not trigger any gesture, but intercepts action. for (int i = 0; i < 3; i++) { eventTime += interval; @@ -737,7 +1034,7 @@ public class GestureLauncherServiceTest { interactive, outLaunched); assertTrue(intercepted); assertFalse(outLaunched.value); - interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; } } @@ -765,7 +1062,7 @@ public class GestureLauncherServiceTest { interactive, outLaunched); assertTrue(intercepted); assertFalse(outLaunched.value); - interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; } } @@ -916,7 +1213,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT, IGNORED_META_STATE, IGNORED_DEVICE_ID, IGNORED_SCANCODE, @@ -947,9 +1244,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnInteractiveSetupIncomplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); withUserSetupCompleteValue(false); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -962,7 +1257,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -995,9 +1290,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOnInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1009,7 +1302,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1042,9 +1335,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOnInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1087,8 +1378,12 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffNotInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerGestureEnableSettingValue(false); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -1101,7 +1396,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1146,7 +1441,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1223,10 +1518,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnNotInteractiveSetupComplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); - withUserSetupCompleteValue(true); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1238,7 +1530,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1272,9 +1564,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnNotInteractiveSetupIncomplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); withUserSetupCompleteValue(false); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -1287,7 +1577,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1332,7 +1622,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1365,9 +1655,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOnNotInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1409,12 +1697,53 @@ public class GestureLauncherServiceTest { } /** + * If processPowerKeyDown is called instead of interceptPowerKeyDown (meaning the double tap + * gesture isn't performed), the emergency gesture is still launched. + */ + @Test + public void + testProcessPowerKeyDown_fiveInboundPresses_cameraDoesNotLaunch_emergencyGestureLaunches() { + enableCameraGesture(); + enableEmergencyGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + //Second event; call processPowerKeyDown without calling interceptPowerKeyDown + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + KeyEvent keyEvent = + new KeyEvent( + IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); + mGestureLauncherService.processPowerKeyDown(keyEvent); + + verify(mMetricsLogger, never()) + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + verify(mUiEventLogger, never()).log(any()); + + // Presses 3 and 4 should not trigger any gesture + for (int i = 0; i < 2; i++) { + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false); + } + + // Fifth button press should still trigger the emergency flow + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + verify(mUiEventLogger, times(1)) + .log(GestureLauncherService.GestureLauncherEvent.GESTURE_EMERGENCY_TAP_POWER); + verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected(); + } + + /** * Helper method to trigger emergency gesture by pressing button for 5 times. * * @return last event time. */ private long triggerEmergencyGesture() { - return triggerEmergencyGesture(CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1); + return triggerEmergencyGesture(POWER_DOUBLE_TAP_MAX_TIME_MS - 1); } /** @@ -1473,6 +1802,27 @@ public class GestureLauncherServiceTest { .thenReturn(enableConfigValue); } + private void withDoubleTapPowerEnabledConfigValue(boolean enable) { + when(mResources.getBoolean(com.android.internal.R.bool.config_doubleTapPowerGestureEnabled)) + .thenReturn(enable); + } + + private void withDoubleTapPowerGestureEnableSettingValue(boolean enable) { + Settings.Secure.putIntForUser( + mContentResolver, + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED, + enable ? 1 : 0, + UserHandle.USER_CURRENT); + } + + private void withDefaultDoubleTapPowerGestureAction(int action) { + Settings.Secure.putIntForUser( + mContentResolver, + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE, + action, + UserHandle.USER_CURRENT); + } + private void withEmergencyGestureEnabledConfigValue(boolean enableConfigValue) { when(mResources.getBoolean( com.android.internal.R.bool.config_emergencyGestureEnabled)) @@ -1510,4 +1860,72 @@ public class GestureLauncherServiceTest { userSetupCompleteValue, UserHandle.USER_CURRENT); } + + private void setUpGetGestureTargetActivityPendingIntent(PendingIntent pendingIntent) { + doAnswer( + invocation -> { + QuickAccessWalletClient.GesturePendingIntentCallback callback = + (QuickAccessWalletClient.GesturePendingIntentCallback) + invocation.getArguments()[1]; + callback.onGesturePendingIntentRetrieved(pendingIntent); + return null; + }) + .when(mQuickAccessWalletClient) + .getGestureTargetActivityPendingIntent(any(), any()); + } + + private void setUpWalletFallbackPendingIntent(PendingIntent pendingIntent) { + doAnswer( + invocation -> { + QuickAccessWalletClient.WalletPendingIntentCallback callback = + (QuickAccessWalletClient.WalletPendingIntentCallback) + invocation.getArguments()[1]; + callback.onWalletPendingIntentRetrieved(pendingIntent); + return null; + }) + .when(mQuickAccessWalletClient) + .getWalletPendingIntent(any(), any()); + } + + private void enableWalletGesture() { + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + withDoubleTapPowerGestureEnableSettingValue(true); + withDoubleTapPowerEnabledConfigValue(true); + + mGestureLauncherService.updateWalletDoubleTapPowerEnabled(); + withUserSetupCompleteValue(true); + } + + private void enableEmergencyGesture() { + withEmergencyGestureEnabledConfigValue(true); + withEmergencyGestureEnabledSettingValue(true); + mGestureLauncherService.updateEmergencyGestureEnabled(); + withUserSetupCompleteValue(true); + } + + private void enableCameraGesture() { + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(0); + } + mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + withUserSetupCompleteValue(true); + } + + private void sendPowerKeyDownToGestureLauncherServiceAndAssertValues( + long eventTime, boolean expectedIntercept, boolean expectedOutLaunchedValue) { + KeyEvent keyEvent = + new KeyEvent( + IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); + boolean interactive = true; + MutableBoolean outLaunched = new MutableBoolean(true); + boolean intercepted = + mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, outLaunched); + assertEquals(intercepted, expectedIntercept); + assertEquals(outLaunched.value, expectedOutLaunchedValue); + } } diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java index 5127b2d11e44..c6df146dd42a 100644 --- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java @@ -22,6 +22,8 @@ import static com.android.compatibility.common.util.PollingCheck.waitFor; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; @@ -921,6 +923,41 @@ public final class DeviceStateManagerServiceTest { }); } + @Test + public void shouldShowRdmEduDialog1() { + // RDM V1 Cases + assertTrue(DeviceStateManagerService.shouldShowRdmEduDialog( + false /* hasControlDeviceStatePermission */, + false /* requestingRdmOuterDefault */, + false /* isDeviceClosed (no-op) */)); + + assertFalse(DeviceStateManagerService.shouldShowRdmEduDialog( + true /* hasControlDeviceStatePermission */, + false /* requestingRdmOuterDefault */, + true /* isDeviceClosed (no-op) */)); + + // RDM V2 Cases + // hasControlDeviceStatePermission = false + assertFalse(DeviceStateManagerService.shouldShowRdmEduDialog( + false /* hasControlDeviceStatePermission */, + true /* requestingRdmOuterDefault */, + false /* isDeviceClosed */)); + assertTrue(DeviceStateManagerService.shouldShowRdmEduDialog( + false /* hasControlDeviceStatePermission */, + true /* requestingRdmOuterDefault */, + true /* isDeviceClosed */)); + + // hasControlDeviceStatePermission = true + assertFalse(DeviceStateManagerService.shouldShowRdmEduDialog( + true /* hasControlDeviceStatePermission */, + true /* requestingRdmOuterDefault */, + false /* isDeviceClosed */)); + assertFalse(DeviceStateManagerService.shouldShowRdmEduDialog( + true /* hasControlDeviceStatePermission */, + true /* requestingRdmOuterDefault */, + true /* isDeviceClosed */)); + } + /** * Common code to verify the handling of FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP flag. * diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java index 05a1482b9be6..1cc4db91b1d3 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java @@ -18,15 +18,22 @@ package com.android.server.policy; import static android.view.KeyEvent.KEYCODE_POWER; import static android.view.KeyEvent.KEYCODE_VOLUME_UP; +import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS; +import static com.android.server.policy.PhoneWindowManager.POWER_MULTI_PRESS_TIMEOUT_MILLIS; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_SLEEP; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_GO_TO_SLEEP; +import static org.junit.Assert.assertEquals; + +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.view.Display; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; /** @@ -39,8 +46,12 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { @Before public void setUp() { setUpPhoneWindowManager(); + mPhoneWindowManager.overrideStatusBarManagerInternal(); } + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + /** * Power single press to turn screen on/off. */ @@ -50,6 +61,8 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { sendKey(KEYCODE_POWER); mPhoneWindowManager.assertPowerSleep(); + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + // turn screen on when begin from non-interactive. mPhoneWindowManager.overrideDisplayState(Display.STATE_OFF); sendKey(KEYCODE_POWER); @@ -90,7 +103,7 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { mPhoneWindowManager.overrideCanStartDreaming(false); sendKey(KEYCODE_POWER); sendKey(KEYCODE_POWER); - mPhoneWindowManager.assertCameraLaunch(); + mPhoneWindowManager.assertDoublePowerLaunch(); mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); } @@ -101,7 +114,7 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { public void testPowerDoublePress() { sendKey(KEYCODE_POWER); sendKey(KEYCODE_POWER); - mPhoneWindowManager.assertCameraLaunch(); + mPhoneWindowManager.assertDoublePowerLaunch(); } /** @@ -111,12 +124,14 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { public void testPowerLongPress() { // Show assistant. mPhoneWindowManager.overrideLongPressOnPower(LONG_PRESS_POWER_ASSISTANT); - sendKey(KEYCODE_POWER, true); + sendKey(KEYCODE_POWER, SingleKeyGestureDetector.sDefaultLongPressTimeout); mPhoneWindowManager.assertSearchManagerLaunchAssist(); + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + // Show global actions. mPhoneWindowManager.overrideLongPressOnPower(LONG_PRESS_POWER_GLOBAL_ACTIONS); - sendKey(KEYCODE_POWER, true); + sendKey(KEYCODE_POWER, SingleKeyGestureDetector.sDefaultLongPressTimeout); mPhoneWindowManager.assertShowGlobalActionsCalled(); } @@ -141,4 +156,139 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { sendKey(KEYCODE_POWER); mPhoneWindowManager.assertNoPowerSleep(); } + + /** + * Double press of power when the window handles the power key events. The + * system double power gesture launch should not be performed. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerDoublePress_windowHasOverridePermissionAndKeysHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); + + mPhoneWindowManager.assertNoDoublePowerLaunch(); + } + + /** + * Double press of power when the window doesn't handle the power key events. + * The system default gesture launch should be performed and the app should receive both events. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerDoublePress_windowHasOverridePermissionAndKeysUnHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> false); + + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); + mPhoneWindowManager.assertDoublePowerLaunch(); + assertEquals(getDownKeysDispatched(), 2); + assertEquals(getUpKeysDispatched(), 2); + } + + /** + * Triple press of power when the window handles the power key double press gesture. + * The system default gesture launch should not be performed, and the app only receives the + * first two presses. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerTriplePress_windowHasOverridePermissionAndKeysHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); + mPhoneWindowManager.assertNoDoublePowerLaunch(); + assertEquals(getDownKeysDispatched(), 2); + assertEquals(getUpKeysDispatched(), 2); + } + + /** + * Tests a single press, followed by a double press when the window can handle the power key. + * The app should receive all 3 events. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerTriplePressWithDelay_windowHasOverridePermissionAndKeysHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + + sendKey(KEYCODE_POWER); + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertNoDoublePowerLaunch(); + assertEquals(getDownKeysDispatched(), 3); + assertEquals(getUpKeysDispatched(), 3); + } + + /** + * Tests single press when window doesn't handle the power key. Phone should go to sleep. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerSinglePress_windowHasOverridePermissionAndKeyUnhandledByApp() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> false); + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); + + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertPowerSleep(); + } + + /** + * Tests single press when the window handles the power key. Phone should go to sleep after a + * delay of {POWER_MULTI_PRESS_TIMEOUT_MILLIS} + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerSinglePress_windowHasOverridePermissionAndKeyHandledByApp() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + mPhoneWindowManager.overrideDisplayState(Display.STATE_ON); + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); + + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + + mPhoneWindowManager.assertPowerSleep(); + } + + + /** + * Tests 5x press when the window handles the power key. Emergency gesture should still be + * launched. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerFiveTimesPress_windowHasOverridePermissionAndKeyHandledByApp() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + mPhoneWindowManager.overrideDisplayState(Display.STATE_ON); + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); + + for (int i = 0; i < 5; ++i) { + sendKey(KEYCODE_POWER); + mPhoneWindowManager.moveTimeForward(100); + } + + mPhoneWindowManager.assertEmergencyLaunch(); + assertEquals(getDownKeysDispatched(), 2); + assertEquals(getUpKeysDispatched(), 2); + } } diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java index 9e47a008592c..2329a0728baf 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java +++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java @@ -85,7 +85,11 @@ class ShortcutKeyTestBase { private Resources mResources; private PackageManager mPackageManager; TestPhoneWindowManager mPhoneWindowManager; - DispatchedKeyHandler mDispatchedKeyHandler = event -> false; + + DispatchedKeyHandler mDispatchedKeyHandler; + private int mDownKeysDispatched; + private int mUpKeysDispatched; + Context mContext; /** Modifier key to meta state */ @@ -116,6 +120,9 @@ class ShortcutKeyTestBase { XmlResourceParser testBookmarks = mResources.getXml( com.android.frameworks.wmtests.R.xml.bookmarks); doReturn(testBookmarks).when(mResources).getXml(com.android.internal.R.xml.bookmarks); + mDispatchedKeyHandler = event -> false; + mDownKeysDispatched = 0; + mUpKeysDispatched = 0; try { // Keep packageName / className in sync with @@ -229,6 +236,10 @@ class ShortcutKeyTestBase { sendKeyCombination(new int[]{keyCode}, 0 /*durationMillis*/, longPress, DEFAULT_DISPLAY); } + void sendKey(int keyCode, long durationMillis) { + sendKeyCombination(new int[]{keyCode}, durationMillis, false, DEFAULT_DISPLAY); + } + boolean sendKeyGestureEventStart(int gestureType) { return mPhoneWindowManager.sendKeyGestureEvent( new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction( @@ -278,6 +289,14 @@ class ShortcutKeyTestBase { doReturn(expectedBehavior).when(mResources).getInteger(eq(resId)); } + int getDownKeysDispatched() { + return mDownKeysDispatched; + } + + int getUpKeysDispatched() { + return mUpKeysDispatched; + } + private void interceptKey(KeyEvent keyEvent) { int actions = mPhoneWindowManager.interceptKeyBeforeQueueing(keyEvent); if ((actions & ACTION_PASS_TO_USER) != 0) { @@ -285,6 +304,11 @@ class ShortcutKeyTestBase { if (!mDispatchedKeyHandler.onKeyDispatched(keyEvent)) { mPhoneWindowManager.interceptUnhandledKey(keyEvent); } + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + ++mDownKeysDispatched; + } else { + ++mUpKeysDispatched; + } } } mPhoneWindowManager.dispatchAllPendingEvents(); diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 9db76d47fed7..f06b45e94f77 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -22,6 +22,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.STATE_ON; import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE; +import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong; @@ -45,10 +46,14 @@ import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_SHUT import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM; import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.after; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.description; import static org.mockito.Mockito.mockingDetails; import static org.mockito.Mockito.timeout; @@ -85,7 +90,9 @@ import android.os.VibratorInfo; import android.os.test.TestLooper; import android.provider.Settings; import android.service.dreams.DreamManagerInternal; +import android.service.quickaccesswallet.QuickAccessWalletClient; import android.telecom.TelecomManager; +import android.util.MutableBoolean; import android.view.Display; import android.view.InputEvent; import android.view.KeyCharacterMap; @@ -95,9 +102,12 @@ import android.view.autofill.AutofillManagerInternal; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.accessibility.AccessibilityShortcutController; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.KeyInterceptionInfo; import com.android.server.GestureLauncherService; import com.android.server.LocalServices; +import com.android.server.SystemService; import com.android.server.input.InputManagerInternal; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.pm.UserManagerInternal; @@ -120,6 +130,7 @@ import org.mockito.MockSettings; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.util.List; import java.util.function.Supplier; @@ -132,6 +143,8 @@ class TestPhoneWindowManager { private PhoneWindowManager mPhoneWindowManager; private Context mContext; + private GestureLauncherService mGestureLauncherService; + @Mock private WindowManagerInternal mWindowManagerInternal; @Mock private ActivityManagerInternal mActivityManagerInternal; @@ -163,7 +176,9 @@ class TestPhoneWindowManager { @Mock private DisplayRotation mDisplayRotation; @Mock private DisplayPolicy mDisplayPolicy; @Mock private WindowManagerPolicy.ScreenOnListener mScreenOnListener; - @Mock private GestureLauncherService mGestureLauncherService; + @Mock private QuickAccessWalletClient mQuickAccessWalletClient; + @Mock private MetricsLogger mMetricsLogger; + @Mock private UiEventLogger mUiEventLogger; @Mock private GlobalActions mGlobalActions; @Mock private AccessibilityShortcutController mAccessibilityShortcutController; @@ -192,6 +207,8 @@ class TestPhoneWindowManager { private int mKeyEventPolicyFlags = FLAG_INTERACTIVE; + private int mProcessPowerKeyDownCount = 0; + private class TestTalkbackShortcutController extends TalkbackShortcutController { TestTalkbackShortcutController(Context context) { super(context); @@ -260,6 +277,8 @@ class TestPhoneWindowManager { MockitoAnnotations.initMocks(this); mHandler = new Handler(mTestLooper.getLooper()); mContext = mockingDetails(context).isSpy() ? context : spy(context); + mGestureLauncherService = spy(new GestureLauncherService(mContext, mMetricsLogger, + mQuickAccessWalletClient, mUiEventLogger)); setUp(supportSettingsUpdate); mTestLooper.dispatchAll(); } @@ -272,6 +291,7 @@ class TestPhoneWindowManager { mMockitoSession = mockitoSession() .mockStatic(LocalServices.class, spyStubOnly) .mockStatic(KeyCharacterMap.class) + .mockStatic(GestureLauncherService.class) .strictness(Strictness.LENIENT) .startMocking(); @@ -294,6 +314,16 @@ class TestPhoneWindowManager { () -> LocalServices.getService(eq(PowerManagerInternal.class))); doReturn(mDisplayManagerInternal).when( () -> LocalServices.getService(eq(DisplayManagerInternal.class))); + doReturn(true).when( + () -> GestureLauncherService.isCameraDoubleTapPowerSettingEnabled(any(), anyInt()) + ); + doReturn(true).when( + () -> GestureLauncherService.isEmergencyGestureSettingEnabled(any(), anyInt()) + ); + doReturn(true).when( + () -> GestureLauncherService.isGestureLauncherEnabled(any()) + ); + mGestureLauncherService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START); doReturn(mGestureLauncherService).when( () -> LocalServices.getService(eq(GestureLauncherService.class))); doReturn(mUserManagerInternal).when( @@ -375,7 +405,8 @@ class TestPhoneWindowManager { doNothing().when(mContext).startActivityAsUser(any(), any()); doNothing().when(mContext).startActivityAsUser(any(), any(), any()); - KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0); + KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0, + /* inputFeatureFlags = */ 0); doReturn(interceptionInfo) .when(mWindowManagerInternal).getKeyInterceptionInfoFromToken(any()); @@ -393,6 +424,8 @@ class TestPhoneWindowManager { eq(TEST_BROWSER_ROLE_PACKAGE_NAME)); doReturn(mSmsIntent).when(mPackageManager).getLaunchIntentForPackage( eq(TEST_SMS_ROLE_PACKAGE_NAME)); + mProcessPowerKeyDownCount = 0; + captureProcessPowerKeyDownCount(); Mockito.reset(mContext); } @@ -638,6 +671,12 @@ class TestPhoneWindowManager { .when(mButtonOverridePermissionChecker).canAppOverrideSystemKey(any(), anyInt()); } + void overrideCanWindowOverridePowerKey(boolean granted) { + doReturn(granted) + .when(mButtonOverridePermissionChecker).canWindowOverridePowerKey(any(), anyInt(), + anyInt()); + } + void overrideKeyEventPolicyFlags(int flags) { mKeyEventPolicyFlags = flags; } @@ -713,13 +752,59 @@ class TestPhoneWindowManager { verify(mPowerManager, never()).goToSleep(anyLong(), anyInt(), anyInt()); } - void assertCameraLaunch() { + void assertDoublePowerLaunch() { + ArgumentCaptor<MutableBoolean> valueCaptor = ArgumentCaptor.forClass(MutableBoolean.class); + mTestLooper.dispatchAll(); - // GestureLauncherService should receive interceptPowerKeyDown twice. - verify(mGestureLauncherService, times(2)) - .interceptPowerKeyDown(any(), anyBoolean(), any()); + verify(mGestureLauncherService, atLeast(2)) + .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); + verify(mGestureLauncherService, atMost(4)) + .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); + + if (overridePowerKeyBehaviorInFocusedWindow()) { + assertTrue(mProcessPowerKeyDownCount >= 2 && mProcessPowerKeyDownCount <= 4); + } + + List<Boolean> capturedValues = valueCaptor.getAllValues().stream() + .map(mutableBoolean -> mutableBoolean.value) + .toList(); + + assertTrue(capturedValues.contains(true)); } + void assertNoDoublePowerLaunch() { + ArgumentCaptor<MutableBoolean> valueCaptor = ArgumentCaptor.forClass(MutableBoolean.class); + + mTestLooper.dispatchAll(); + verify(mGestureLauncherService, atLeast(0)) + .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); + + List<Boolean> capturedValues = valueCaptor.getAllValues().stream() + .map(mutableBoolean -> mutableBoolean.value) + .toList(); + + assertTrue(capturedValues.stream().noneMatch(value -> value)); + } + + void assertEmergencyLaunch() { + ArgumentCaptor<MutableBoolean> valueCaptor = ArgumentCaptor.forClass(MutableBoolean.class); + + mTestLooper.dispatchAll(); + verify(mGestureLauncherService, atLeast(1)) + .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); + + if (overridePowerKeyBehaviorInFocusedWindow()) { + assertEquals(mProcessPowerKeyDownCount, 5); + } + + List<Boolean> capturedValues = valueCaptor.getAllValues().stream() + .map(mutableBoolean -> mutableBoolean.value) + .toList(); + + assertTrue(capturedValues.getLast()); + } + + void assertSearchManagerLaunchAssist() { mTestLooper.dispatchAll(); verify(mSearchManager).launchAssist(any()); @@ -929,4 +1014,12 @@ class TestPhoneWindowManager { verify(mInputManagerInternal) .handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType)); } + + private void captureProcessPowerKeyDownCount() { + doAnswer((Answer<Void>) invocation -> { + invocation.callRealMethod(); + mProcessPowerKeyDownCount++; + return null; + }).when(mGestureLauncherService).processPowerKeyDown(any()); + } } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 698821bc8890..3f8e0669ba7b 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -2455,6 +2455,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public String getMeid() { + if (Flags.cleanupCdma()) return null; return getMeid(getSlotIndex()); } @@ -2500,6 +2501,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public String getMeid(int slotIndex) { + if (Flags.cleanupCdma()) return null; ITelephony telephony = getITelephony(); if (telephony == null) return null; @@ -2531,6 +2533,7 @@ public class TelephonyManager { @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) @Nullable public String getManufacturerCode() { + if (Flags.cleanupCdma()) return null; return getManufacturerCode(getSlotIndex()); } @@ -2549,6 +2552,7 @@ public class TelephonyManager { @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) @Nullable public String getManufacturerCode(int slotIndex) { + if (Flags.cleanupCdma()) return null; ITelephony telephony = getITelephony(); if (telephony == null) return null; @@ -6942,6 +6946,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public @EriIconIndex int getCdmaEnhancedRoamingIndicatorDisplayNumber() { + if (Flags.cleanupCdma()) return -1; return getCdmaEriIconIndex(getSubId()); } @@ -6954,6 +6959,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @UnsupportedAppUsage public @EriIconIndex int getCdmaEriIconIndex(int subId) { + if (Flags.cleanupCdma()) return -1; try { ITelephony telephony = getITelephony(); if (telephony == null) @@ -6980,6 +6986,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @UnsupportedAppUsage public @EriIconMode int getCdmaEriIconMode(int subId) { + if (Flags.cleanupCdma()) return -1; try { ITelephony telephony = getITelephony(); if (telephony == null) @@ -7003,6 +7010,7 @@ public class TelephonyManager { @Deprecated @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getCdmaEriText() { + if (Flags.cleanupCdma()) return null; return getCdmaEriText(getSubId()); } @@ -7016,6 +7024,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) @UnsupportedAppUsage public String getCdmaEriText(int subId) { + if (Flags.cleanupCdma()) return null; try { ITelephony telephony = getITelephony(); if (telephony == null) @@ -8295,6 +8304,7 @@ public class TelephonyManager { @Deprecated @UnsupportedAppUsage public String nvReadItem(int itemID) { + if (Flags.cleanupCdma()) return ""; try { ITelephony telephony = getITelephony(); if (telephony != null) @@ -8324,6 +8334,7 @@ public class TelephonyManager { */ @Deprecated public boolean nvWriteItem(int itemID, String itemValue) { + if (Flags.cleanupCdma()) return false; try { ITelephony telephony = getITelephony(); if (telephony != null) @@ -8352,6 +8363,7 @@ public class TelephonyManager { */ @Deprecated public boolean nvWriteCdmaPrl(byte[] preferredRoamingList) { + if (Flags.cleanupCdma()) return false; try { ITelephony telephony = getITelephony(); if (telephony != null) @@ -10712,6 +10724,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public String getCdmaMdn() { + if (Flags.cleanupCdma()) return null; return getCdmaMdn(getSubId()); } @@ -10727,6 +10740,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public String getCdmaMdn(int subId) { + if (Flags.cleanupCdma()) return null; try { ITelephony telephony = getITelephony(); if (telephony == null) @@ -10751,6 +10765,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public String getCdmaMin() { + if (Flags.cleanupCdma()) return null; return getCdmaMin(getSubId()); } @@ -10766,6 +10781,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public String getCdmaMin(int subId) { + if (Flags.cleanupCdma()) return null; try { ITelephony telephony = getITelephony(); if (telephony == null) @@ -12059,6 +12075,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public @CdmaRoamingMode int getCdmaRoamingMode() { + if (Flags.cleanupCdma()) return CDMA_ROAMING_MODE_RADIO_DEFAULT; int mode = CDMA_ROAMING_MODE_RADIO_DEFAULT; try { ITelephony telephony = getITelephony(); @@ -12106,6 +12123,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public void setCdmaRoamingMode(@CdmaRoamingMode int mode) { + if (Flags.cleanupCdma()) return; if (getPhoneType() != PHONE_TYPE_CDMA) { throw new IllegalStateException("Phone does not support CDMA."); } @@ -12191,6 +12209,7 @@ public class TelephonyManager { @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public @CdmaSubscription int getCdmaSubscriptionMode() { + if (Flags.cleanupCdma()) return CDMA_SUBSCRIPTION_UNKNOWN; int mode = CDMA_SUBSCRIPTION_RUIM_SIM; try { ITelephony telephony = getITelephony(); @@ -12234,6 +12253,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public void setCdmaSubscriptionMode(@CdmaSubscription int mode) { + if (Flags.cleanupCdma()) return; if (getPhoneType() != PHONE_TYPE_CDMA) { throw new IllegalStateException("Phone does not support CDMA."); } @@ -13961,6 +13981,7 @@ public class TelephonyManager { @SystemApi @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CDMA) public String getCdmaPrlVersion() { + if (Flags.cleanupCdma()) return null; return getCdmaPrlVersion(getSubId()); } @@ -13976,6 +13997,7 @@ public class TelephonyManager { */ @Deprecated public String getCdmaPrlVersion(int subId) { + if (Flags.cleanupCdma()) return null; try { ITelephony service = getITelephony(); if (service != null) { diff --git a/tests/AppJankTest/Android.bp b/tests/AppJankTest/Android.bp index acf8dc9aca47..c3cda6a41cbb 100644 --- a/tests/AppJankTest/Android.bp +++ b/tests/AppJankTest/Android.bp @@ -30,6 +30,7 @@ android_test { "androidx.test.core", "platform-test-annotations", "flag-junit", + "androidx.test.uiautomator_uiautomator", ], platform_apis: true, test_suites: ["device-tests"], diff --git a/tests/AppJankTest/AndroidManifest.xml b/tests/AppJankTest/AndroidManifest.xml index abed1798c47c..861a79c6f0ed 100644 --- a/tests/AppJankTest/AndroidManifest.xml +++ b/tests/AppJankTest/AndroidManifest.xml @@ -19,22 +19,31 @@ package="android.app.jank.tests"> <application android:appCategory="news"> - <uses-library android:name="android.test.runner" /> + <activity android:name=".JankTrackerActivity" + android:exported="true" + android:label="JankTrackerActivity" + android:launchMode="singleTop"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/> + </intent-filter> + </activity> <activity android:name=".EmptyActivity" - android:label="EmptyActivity" - android:exported="true"> + android:exported="true" + android:label="EmptyActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/> </intent-filter> </activity> + <uses-library android:name="android.test.runner"/> </application> <!-- self-instrumenting test package. --> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" - android:targetPackage="android.app.jank.tests" - android:label="Core tests of App Jank Tracking"> + android:label="Core tests of App Jank Tracking" + android:targetPackage="android.app.jank.tests"> </instrumentation> </manifest>
\ No newline at end of file diff --git a/tests/AppJankTest/res/layout/jank_tracker_activity_layout.xml b/tests/AppJankTest/res/layout/jank_tracker_activity_layout.xml new file mode 100644 index 000000000000..65def7f2d5b6 --- /dev/null +++ b/tests/AppJankTest/res/layout/jank_tracker_activity_layout.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + + +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + + <LinearLayout + android:id="@+id/linear_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <EditText + android:id="@+id/edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textEnableTextConversionSuggestions" + android:text="Edit Text"/> + <TextView android:id="@+id/text_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Text View" + /> + <android.app.jank.tests.TestWidget + android:id="@+id/jank_tracker_widget" + android:layout_width="match_parent" + android:layout_height="wrap_content" + /> + </LinearLayout> +</ScrollView>
\ No newline at end of file diff --git a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java new file mode 100644 index 000000000000..34f0c191ecf5 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.jank.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import android.app.Activity; +import android.app.Instrumentation; +import android.app.jank.AppJankStats; +import android.app.jank.Flags; +import android.app.jank.JankDataProcessor; +import android.app.jank.JankTracker; +import android.app.jank.StateTracker; +import android.content.Intent; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.widget.EditText; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.Until; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This file contains tests that verify the proper functionality of the Jank Tracking feature. + * All tests should obtain references to necessary objects through View type interfaces, rather + * than direct instantiation. When operating outside of a testing environment, the expected + * behavior is to retrieve the necessary objects using View type interfaces. This approach ensures + * that calls are correctly routed down to the activity level. Any modifications to the call + * routing should result in test failures, which might happen with direct instantiations. + */ +@RunWith(AndroidJUnit4.class) +public class IntegrationTests { + public static final int WAIT_FOR_TIMEOUT_MS = 5000; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private Activity mEmptyActivity; + + public UiDevice mDevice; + private Instrumentation mInstrumentation; + private ActivityTestRule<JankTrackerActivity> mJankTrackerActivityRule = + new ActivityTestRule<>( + JankTrackerActivity.class, + false, + false); + + private ActivityTestRule<EmptyActivity> mEmptyActivityRule = + new ActivityTestRule<>(EmptyActivity.class, false , true); + + @Before + public void setUp() { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mDevice = UiDevice.getInstance(mInstrumentation); + } + + + /** + * Get a JankTracker object from a view and confirm it's not null. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void getJankTacker_confirmNotNull() { + Activity jankTrackerActivity = mJankTrackerActivityRule.launchActivity(null); + EditText editText = jankTrackerActivity.findViewById(R.id.edit_text); + + mDevice.wait(Until.findObject(By.text("Edit Text")), WAIT_FOR_TIMEOUT_MS); + + JankTracker jankTracker = editText.getJankTracker(); + assertTrue(jankTracker != null); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void reportJankStats_confirmPendingStatsIncreases() { + Activity jankTrackerActivity = mJankTrackerActivityRule.launchActivity(null); + EditText editText = jankTrackerActivity.findViewById(R.id.edit_text); + JankTracker jankTracker = editText.getJankTracker(); + + HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = + jankTracker.getPendingJankStats(); + assertEquals(0, pendingStats.size()); + + editText.reportAppJankStats(JankUtils.getAppJankStats()); + + // reportAppJankStats performs the work on a background thread, check periodically to see + // if the work is complete. + for (int i = 0; i < 10; i++) { + try { + Thread.sleep(100); + if (jankTracker.getPendingJankStats().size() > 0) { + break; + } + } catch (InterruptedException exception) { + //do nothing and continue + } + } + + pendingStats = jankTracker.getPendingJankStats(); + + assertEquals(1, pendingStats.size()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void simulateWidgetStateChanges_confirmStateChangesAreTracked() { + JankTrackerActivity jankTrackerActivity = + mJankTrackerActivityRule.launchActivity(null); + TestWidget testWidget = jankTrackerActivity.findViewById(R.id.jank_tracker_widget); + JankTracker jankTracker = testWidget.getJankTracker(); + jankTracker.forceListenerRegistration(); + + ArrayList<StateTracker.StateData> uiStates = new ArrayList<>(); + // Get the current UI states, at this point only the activity name should be in the UI + // states list. + jankTracker.getAllUiStates(uiStates); + + assertEquals(1, uiStates.size()); + + // This should add a UI state to be tracked. + testWidget.simulateAnimationStarting(); + uiStates.clear(); + jankTracker.getAllUiStates(uiStates); + + assertEquals(2, uiStates.size()); + + // Stop the animation + testWidget.simulateAnimationEnding(); + uiStates.clear(); + jankTracker.getAllUiStates(uiStates); + + assertEquals(2, uiStates.size()); + + // Confirm the Animation state has a VsyncIdEnd that is not default, indicating the end + // of that state. + for (int i = 0; i < uiStates.size(); i++) { + StateTracker.StateData stateData = uiStates.get(i); + if (stateData.mWidgetCategory.equals(AppJankStats.ANIMATION)) { + assertNotEquals(Long.MAX_VALUE, stateData.mVsyncIdEnd); + } + } + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void jankTrackingPaused_whenActivityNoLongerVisible() { + JankTrackerActivity jankTrackerActivity = + mJankTrackerActivityRule.launchActivity(null); + TestWidget testWidget = jankTrackerActivity.findViewById(R.id.jank_tracker_widget); + JankTracker jankTracker = testWidget.getJankTracker(); + jankTracker.forceListenerRegistration(); + + assertTrue(jankTracker.shouldTrack()); + + // Send jankTrackerActivity to the background + mDevice.pressHome(); + mDevice.waitForIdle(WAIT_FOR_TIMEOUT_MS); + + assertFalse(jankTracker.shouldTrack()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void jankTrackingResumed_whenActivityBecomesVisibleAgain() { + mEmptyActivityRule.launchActivity(null); + mEmptyActivity = mEmptyActivityRule.getActivity(); + JankTrackerActivity jankTrackerActivity = + mJankTrackerActivityRule.launchActivity(null); + TestWidget testWidget = jankTrackerActivity.findViewById(R.id.jank_tracker_widget); + JankTracker jankTracker = testWidget.getJankTracker(); + jankTracker.forceListenerRegistration(); + + // Send jankTrackerActivity to the background + mDevice.pressHome(); + mDevice.waitForIdle(WAIT_FOR_TIMEOUT_MS); + + assertFalse(jankTracker.shouldTrack()); + + Intent resumeJankTracker = new Intent(mInstrumentation.getContext(), + JankTrackerActivity.class); + resumeJankTracker.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + mEmptyActivity.startActivity(resumeJankTracker); + mDevice.wait(Until.findObject(By.text("Edit Text")), WAIT_FOR_TIMEOUT_MS); + + assertTrue(jankTracker.shouldTrack()); + } +} diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java index 4d495adf727b..30c568be7716 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java +++ b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertEquals; import android.app.jank.AppJankStats; import android.app.jank.Flags; -import android.app.jank.FrameOverrunHistogram; import android.app.jank.JankDataProcessor; import android.app.jank.StateTracker; import android.platform.test.annotations.RequiresFlagsEnabled; @@ -165,7 +164,7 @@ public class JankDataProcessorTest { assertEquals(pendingStats.size(), 0); - AppJankStats jankStats = getAppJankStats(); + AppJankStats jankStats = JankUtils.getAppJankStats(); mJankDataProcessor.mergeJankStats(jankStats, sActivityName); pendingStats = mJankDataProcessor.getPendingJankStats(); @@ -182,14 +181,14 @@ public class JankDataProcessorTest { @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void mergeAppJankStats_confirmStatsWithMatchingStatesAreCombinedIntoOnePendingStat() { - AppJankStats jankStats = getAppJankStats(); + AppJankStats jankStats = JankUtils.getAppJankStats(); mJankDataProcessor.mergeJankStats(jankStats, sActivityName); HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = mJankDataProcessor.getPendingJankStats(); assertEquals(pendingStats.size(), 1); - AppJankStats secondJankStat = getAppJankStats(); + AppJankStats secondJankStat = JankUtils.getAppJankStats(); mJankDataProcessor.mergeJankStats(secondJankStat, sActivityName); pendingStats = mJankDataProcessor.getPendingJankStats(); @@ -200,7 +199,7 @@ public class JankDataProcessorTest { @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void mergeAppJankStats_whenStatsWithMatchingStatesMerge_confirmFrameCountsAdded() { - AppJankStats jankStats = getAppJankStats(); + AppJankStats jankStats = JankUtils.getAppJankStats(); mJankDataProcessor.mergeJankStats(jankStats, sActivityName); mJankDataProcessor.mergeJankStats(jankStats, sActivityName); @@ -345,27 +344,4 @@ public class JankDataProcessorTest { return mockData; } - - private AppJankStats getAppJankStats() { - AppJankStats jankStats = new AppJankStats( - /*App Uid*/APP_ID, - /*Widget Id*/"test widget id", - /*Widget Category*/AppJankStats.SCROLL, - /*Widget State*/AppJankStats.SCROLLING, - /*Total Frames*/100, - /*Janky Frames*/25, - getOverrunHistogram() - ); - return jankStats; - } - - private FrameOverrunHistogram getOverrunHistogram() { - FrameOverrunHistogram overrunHistogram = new FrameOverrunHistogram(); - overrunHistogram.addFrameOverrunMillis(-2); - overrunHistogram.addFrameOverrunMillis(1); - overrunHistogram.addFrameOverrunMillis(5); - overrunHistogram.addFrameOverrunMillis(25); - return overrunHistogram; - } - } diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java new file mode 100644 index 000000000000..80ab6ad3e587 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.jank.tests; + +import android.app.Activity; +import android.os.Bundle; + + +public class JankTrackerActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.jank_tracker_activity_layout); + } +} + + diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java new file mode 100644 index 000000000000..0b4d97ed20d6 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.jank.tests; + +import android.app.jank.AppJankStats; +import android.app.jank.FrameOverrunHistogram; + +public class JankUtils { + private static final int APP_ID = 25; + + /** + * Returns a mock AppJankStats object to be used in tests. + */ + public static AppJankStats getAppJankStats() { + AppJankStats jankStats = new AppJankStats( + /*App Uid*/APP_ID, + /*Widget Id*/"test widget id", + /*Widget Category*/AppJankStats.SCROLL, + /*Widget State*/AppJankStats.SCROLLING, + /*Total Frames*/100, + /*Janky Frames*/25, + getOverrunHistogram() + ); + return jankStats; + } + + /** + * Returns a mock histogram to be used with an AppJankStats object. + */ + public static FrameOverrunHistogram getOverrunHistogram() { + FrameOverrunHistogram overrunHistogram = new FrameOverrunHistogram(); + overrunHistogram.addFrameOverrunMillis(-2); + overrunHistogram.addFrameOverrunMillis(1); + overrunHistogram.addFrameOverrunMillis(5); + overrunHistogram.addFrameOverrunMillis(25); + return overrunHistogram; + } +} diff --git a/tests/AppJankTest/src/android/app/jank/tests/TestWidget.java b/tests/AppJankTest/src/android/app/jank/tests/TestWidget.java new file mode 100644 index 000000000000..5fff46038ead --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/TestWidget.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.jank.tests; + +import android.app.jank.AppJankStats; +import android.app.jank.JankTracker; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +public class TestWidget extends View { + + private JankTracker mJankTracker; + + /** + * Create JankTrackerView + */ + public TestWidget(Context context) { + super(context); + } + + /** + * Create JankTrackerView, needed by system when inflating views defined in a layout file. + */ + public TestWidget(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + /** + * Mock starting an animation. + */ + public void simulateAnimationStarting() { + if (jankTrackerCreated()) { + mJankTracker.addUiState(AppJankStats.ANIMATION, + Integer.toString(this.getId()), AppJankStats.ANIMATING); + } + } + + /** + * Mock ending an animation. + */ + public void simulateAnimationEnding() { + if (jankTrackerCreated()) { + mJankTracker.removeUiState(AppJankStats.ANIMATION, + Integer.toString(this.getId()), AppJankStats.ANIMATING); + } + } + + private boolean jankTrackerCreated() { + if (mJankTracker == null) { + mJankTracker = getJankTracker(); + } + return mJankTracker != null; + } +} diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 43844f6514e8..038c6d7754b5 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -541,7 +541,8 @@ class InputManagerServiceTests { 0 }, "title", - /* uid = */0 + /* uid = */0, + /* inputFeatureFlags = */ 0 ) whenever(windowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info) } diff --git a/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java index ad032fb2fba6..15a580c9e8f7 100644 --- a/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java +++ b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java @@ -20,6 +20,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; @@ -186,6 +187,22 @@ public class BroadcastStickyCacheTest { } @Test + public void getIntent_queryActionTwiceWithNullResult_verifyRegisterReceiverIsCalledOnce() + throws RemoteException { + setActivityManagerMock(null); + final Intent intent = queryIntent(new IntentFilter(Intent.ACTION_DEVICE_STORAGE_FULL)); + final Intent cachedIntent = queryIntent( + new IntentFilter(Intent.ACTION_DEVICE_STORAGE_FULL)); + + assertNull(intent); + assertNull(cachedIntent); + + verify(mActivityManagerMock, times(1)).registerReceiverWithFeature( + eq(mIApplicationThreadMock), anyString(), anyString(), anyString(), any(), + any(), anyString(), anyInt(), anyInt()); + } + + @Test public void getIntent_querySameActionWithDifferentFilter_verifyRegisterReceiverCalledTwice() throws RemoteException { setActivityManagerMock(Intent.ACTION_DEVICE_STORAGE_LOW); @@ -226,6 +243,6 @@ public class BroadcastStickyCacheTest { when(ActivityManager.getService()).thenReturn(mActivityManagerMock); when(mActivityManagerMock.registerReceiverWithFeature(any(), anyString(), anyString(), anyString(), any(), any(), anyString(), anyInt(), - anyInt())).thenReturn(new Intent(action)); + anyInt())).thenReturn(action != null ? new Intent(action) : null); } } |