diff options
55 files changed, 1678 insertions, 590 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 51c6f6a5eecc..45c9becdcfd4 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -13655,9 +13655,9 @@ package android.credentials { public final class CredentialManager { method public void clearCredentialState(@NonNull android.credentials.ClearCredentialStateRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException>); - method public void createCredential(@NonNull android.credentials.CreateCredentialRequest, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.CreateCredentialResponse,android.credentials.CreateCredentialException>); - method public void getCredential(@NonNull android.credentials.GetCredentialRequest, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>); - method public void getCredential(@NonNull android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>); + method public void createCredential(@NonNull android.content.Context, @NonNull android.credentials.CreateCredentialRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.CreateCredentialResponse,android.credentials.CreateCredentialException>); + method public void getCredential(@NonNull android.content.Context, @NonNull android.credentials.GetCredentialRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>); + method public void getCredential(@NonNull android.content.Context, @NonNull android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>); method public boolean isEnabledCredentialProviderService(@NonNull android.content.ComponentName); method public void prepareGetCredential(@NonNull android.credentials.GetCredentialRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.PrepareGetCredentialResponse,android.credentials.GetCredentialException>); method public void registerCredentialDescription(@NonNull android.credentials.RegisterCredentialDescriptionRequest); @@ -60403,6 +60403,7 @@ package android.widget { method public void setLineBreakStyle(int); method public void setLineBreakWordStyle(int); method public void setLineHeight(@IntRange(from=0) @Px int); + method public void setLineHeight(int, @FloatRange(from=0) float); method public void setLineSpacing(float, float); method public void setLines(int); method public final void setLinkTextColor(@ColorInt int); diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index d23d3cd87fdb..6357798a6cab 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -37,8 +37,6 @@ import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; -import com.android.internal.util.Preconditions; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; @@ -61,7 +59,8 @@ public class BroadcastOptions extends ComponentOptions { private long mRequireCompatChangeId = CHANGE_INVALID; private long mIdForResponseEvent; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; - private @Nullable String mDeliveryGroupMatchingKey; + private @Nullable String mDeliveryGroupMatchingNamespaceFragment; + private @Nullable String mDeliveryGroupMatchingKeyFragment; private @Nullable BundleMerger mDeliveryGroupExtrasMerger; private @Nullable IntentFilter mDeliveryGroupMatchingFilter; private @DeferralPolicy int mDeferralPolicy; @@ -196,7 +195,13 @@ public class BroadcastOptions extends ComponentOptions { "android:broadcast.deliveryGroupPolicy"; /** - * Corresponds to {@link #setDeliveryGroupMatchingKey(String, String)}. + * Corresponds to namespace fragment of {@link #setDeliveryGroupMatchingKey(String, String)}. + */ + private static final String KEY_DELIVERY_GROUP_NAMESPACE = + "android:broadcast.deliveryGroupMatchingNamespace"; + + /** + * Corresponds to key fragment of {@link #setDeliveryGroupMatchingKey(String, String)}. */ private static final String KEY_DELIVERY_GROUP_KEY = "android:broadcast.deliveryGroupMatchingKey"; @@ -337,7 +342,8 @@ public class BroadcastOptions extends ComponentOptions { mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT); mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, DELIVERY_GROUP_POLICY_ALL); - mDeliveryGroupMatchingKey = opts.getString(KEY_DELIVERY_GROUP_KEY); + mDeliveryGroupMatchingNamespaceFragment = opts.getString(KEY_DELIVERY_GROUP_NAMESPACE); + mDeliveryGroupMatchingKeyFragment = opts.getString(KEY_DELIVERY_GROUP_KEY); mDeliveryGroupExtrasMerger = opts.getParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER, BundleMerger.class); mDeliveryGroupMatchingFilter = opts.getParcelable(KEY_DELIVERY_GROUP_MATCHING_FILTER, @@ -851,11 +857,8 @@ public class BroadcastOptions extends ComponentOptions { @NonNull public BroadcastOptions setDeliveryGroupMatchingKey(@NonNull String namespace, @NonNull String key) { - Preconditions.checkArgument(!namespace.contains(":"), - "namespace should not contain ':'"); - Preconditions.checkArgument(!key.contains(":"), - "key should not contain ':'"); - mDeliveryGroupMatchingKey = namespace + ":" + key; + mDeliveryGroupMatchingNamespaceFragment = Objects.requireNonNull(namespace); + mDeliveryGroupMatchingKeyFragment = Objects.requireNonNull(key); return this; } @@ -868,7 +871,38 @@ public class BroadcastOptions extends ComponentOptions { */ @Nullable public String getDeliveryGroupMatchingKey() { - return mDeliveryGroupMatchingKey; + if (mDeliveryGroupMatchingNamespaceFragment == null + || mDeliveryGroupMatchingKeyFragment == null) { + return null; + } + return String.join(":", mDeliveryGroupMatchingNamespaceFragment, + mDeliveryGroupMatchingKeyFragment); + } + + /** + * Return the namespace fragment that is used to identify the delivery group that this + * broadcast belongs to. + * + * @return the delivery group namespace fragment that was previously set using + * {@link #setDeliveryGroupMatchingKey(String, String)}. + * @hide + */ + @Nullable + public String getDeliveryGroupMatchingNamespaceFragment() { + return mDeliveryGroupMatchingNamespaceFragment; + } + + /** + * Return the key fragment that is used to identify the delivery group that this + * broadcast belongs to. + * + * @return the delivery group key fragment that was previously set using + * {@link #setDeliveryGroupMatchingKey(String, String)}. + * @hide + */ + @Nullable + public String getDeliveryGroupMatchingKeyFragment() { + return mDeliveryGroupMatchingKeyFragment; } /** @@ -876,7 +910,8 @@ public class BroadcastOptions extends ComponentOptions { * {@link #setDeliveryGroupMatchingKey(String, String)}. */ public void clearDeliveryGroupMatchingKey() { - mDeliveryGroupMatchingKey = null; + mDeliveryGroupMatchingNamespaceFragment = null; + mDeliveryGroupMatchingKeyFragment = null; } /** @@ -1094,8 +1129,11 @@ public class BroadcastOptions extends ComponentOptions { if (mDeliveryGroupPolicy != DELIVERY_GROUP_POLICY_ALL) { b.putInt(KEY_DELIVERY_GROUP_POLICY, mDeliveryGroupPolicy); } - if (mDeliveryGroupMatchingKey != null) { - b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupMatchingKey); + if (mDeliveryGroupMatchingNamespaceFragment != null) { + b.putString(KEY_DELIVERY_GROUP_NAMESPACE, mDeliveryGroupMatchingNamespaceFragment); + } + if (mDeliveryGroupMatchingKeyFragment != null) { + b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupMatchingKeyFragment); } if (mDeliveryGroupPolicy == DELIVERY_GROUP_POLICY_MERGED) { if (mDeliveryGroupExtrasMerger != null) { diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index 5579d2263d06..00ce17adfda6 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -25,11 +25,11 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.TestApi; -import android.app.Activity; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.IntentSender; +import android.os.Binder; import android.os.CancellationSignal; import android.os.ICancellationSignal; import android.os.OutcomeReceiver; @@ -126,20 +126,21 @@ public final class CredentialManager { * need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN} * to use this functionality * + * @param context the context used to launch any UI needed; use an activity context to make sure + * the UI will be launched within the same task stack * @param request the request specifying type(s) of credentials to get from the user - * @param activity the activity used to launch any UI needed * @param cancellationSignal an optional signal that allows for cancelling this call * @param executor the callback will take place on this {@link Executor} * @param callback the callback invoked when the request succeeds or fails */ public void getCredential( + @NonNull Context context, @NonNull GetCredentialRequest request, - @NonNull Activity activity, @Nullable CancellationSignal cancellationSignal, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) { requireNonNull(request, "request must not be null"); - requireNonNull(activity, "activity must not be null"); + requireNonNull(context, "context must not be null"); requireNonNull(executor, "executor must not be null"); requireNonNull(callback, "callback must not be null"); @@ -153,7 +154,7 @@ public final class CredentialManager { cancelRemote = mService.executeGetCredential( request, - new GetCredentialTransport(activity, executor, callback), + new GetCredentialTransport(context, executor, callback), mContext.getOpPackageName()); } catch (RemoteException e) { e.rethrowFromSystemServer(); @@ -175,21 +176,22 @@ public final class CredentialManager { * request through the {@link #prepareGetCredential( * GetCredentialRequest, CancellationSignal, Executor, OutcomeReceiver)} API. * + * @param context the context used to launch any UI needed; use an activity context to make sure + * the UI will be launched within the same task stack * @param pendingGetCredentialHandle the handle representing the pending operation to resume - * @param activity the activity used to launch any UI needed * @param cancellationSignal an optional signal that allows for cancelling this call * @param executor the callback will take place on this {@link Executor} * @param callback the callback invoked when the request succeeds or fails */ public void getCredential( + @NonNull Context context, @NonNull PrepareGetCredentialResponse.PendingGetCredentialHandle - pendingGetCredentialHandle, - @NonNull Activity activity, + pendingGetCredentialHandle, @Nullable CancellationSignal cancellationSignal, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) { requireNonNull(pendingGetCredentialHandle, "pendingGetCredentialHandle must not be null"); - requireNonNull(activity, "activity must not be null"); + requireNonNull(context, "context must not be null"); requireNonNull(executor, "executor must not be null"); requireNonNull(callback, "callback must not be null"); @@ -198,7 +200,7 @@ public final class CredentialManager { return; } - pendingGetCredentialHandle.show(activity, cancellationSignal, executor, callback); + pendingGetCredentialHandle.show(context, cancellationSignal, executor, callback); } /** @@ -207,9 +209,9 @@ public final class CredentialManager { * * <p>This API doesn't invoke any UI. It only performs the preparation work so that you can * later launch the remaining get-credential operation (involves UIs) through the {@link - * #getCredential(PrepareGetCredentialResponse.PendingGetCredentialHandle, Activity, + * #getCredential(PrepareGetCredentialResponse.PendingGetCredentialHandle, Context, * CancellationSignal, Executor, OutcomeReceiver)} API which incurs less latency compared to - * the {@link #getCredential(GetCredentialRequest, Activity, CancellationSignal, Executor, + * the {@link #getCredential(GetCredentialRequest, Context, CancellationSignal, Executor, * OutcomeReceiver)} API that executes the whole operation in one call. * * @param request the request specifying type(s) of credentials to get from the user @@ -262,21 +264,22 @@ public final class CredentialManager { * need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN} * to use this functionality * + * @param context the context used to launch any UI needed; use an activity context to make sure + * the UI will be launched within the same task stack * @param request the request specifying type(s) of credentials to get from the user - * @param activity the activity used to launch any UI needed * @param cancellationSignal an optional signal that allows for cancelling this call * @param executor the callback will take place on this {@link Executor} * @param callback the callback invoked when the request succeeds or fails */ public void createCredential( + @NonNull Context context, @NonNull CreateCredentialRequest request, - @NonNull Activity activity, @Nullable CancellationSignal cancellationSignal, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback) { requireNonNull(request, "request must not be null"); - requireNonNull(activity, "activity must not be null"); + requireNonNull(context, "context must not be null"); requireNonNull(executor, "executor must not be null"); requireNonNull(callback, "callback must not be null"); @@ -290,7 +293,7 @@ public final class CredentialManager { cancelRemote = mService.executeCreateCredential( request, - new CreateCredentialTransport(activity, executor, callback), + new CreateCredentialTransport(context, executor, callback), mContext.getOpPackageName()); } catch (RemoteException e) { e.rethrowFromSystemServer(); @@ -547,14 +550,24 @@ public final class CredentialManager { @Override public void onResponse(PrepareGetCredentialResponseInternal response) { - mExecutor.execute(() -> mCallback.onResult( - new PrepareGetCredentialResponse(response, mGetCredentialTransport))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onResult( + new PrepareGetCredentialResponse(response, mGetCredentialTransport))); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new GetCredentialException(errorType, message))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute( + () -> mCallback.onError(new GetCredentialException(errorType, message))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } @@ -587,7 +600,12 @@ public final class CredentialManager { @Override public void onResponse(GetCredentialResponse response) { if (mCallback != null) { - mCallback.onResponse(response); + final long identity = Binder.clearCallingIdentity(); + try { + mCallback.onResponse(response); + } finally { + Binder.restoreCallingIdentity(identity); + } } else { Log.d(TAG, "Unexpected onResponse call before the show invocation"); } @@ -596,7 +614,12 @@ public final class CredentialManager { @Override public void onError(String errorType, String message) { if (mCallback != null) { - mCallback.onError(errorType, message); + final long identity = Binder.clearCallingIdentity(); + try { + mCallback.onError(errorType, message); + } finally { + Binder.restoreCallingIdentity(identity); + } } else { Log.d(TAG, "Unexpected onError call before the show invocation"); } @@ -606,15 +629,15 @@ public final class CredentialManager { private static class GetCredentialTransport extends IGetCredentialCallback.Stub { // TODO: listen for cancellation to release callback. - private final Activity mActivity; + private final Context mContext; private final Executor mExecutor; private final OutcomeReceiver<GetCredentialResponse, GetCredentialException> mCallback; private GetCredentialTransport( - Activity activity, + Context context, Executor executor, OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) { - mActivity = activity; + mContext = context; mExecutor = executor; mCallback = callback; } @@ -622,42 +645,57 @@ public final class CredentialManager { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - mActivity.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e( TAG, "startIntentSender() failed for intent:" + pendingIntent.getIntentSender(), e); - mExecutor.execute(() -> mCallback.onError( - new GetCredentialException(GetCredentialException.TYPE_UNKNOWN))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onError( + new GetCredentialException(GetCredentialException.TYPE_UNKNOWN))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } @Override public void onResponse(GetCredentialResponse response) { - mExecutor.execute(() -> mCallback.onResult(response)); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onResult(response)); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new GetCredentialException(errorType, message))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute( + () -> mCallback.onError(new GetCredentialException(errorType, message))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } private static class CreateCredentialTransport extends ICreateCredentialCallback.Stub { // TODO: listen for cancellation to release callback. - private final Activity mActivity; + private final Context mContext; private final Executor mExecutor; private final OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> mCallback; private CreateCredentialTransport( - Activity activity, + Context context, Executor executor, OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback) { - mActivity = activity; + mContext = context; mExecutor = executor; mCallback = callback; } @@ -665,26 +703,41 @@ public final class CredentialManager { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - mActivity.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e( TAG, "startIntentSender() failed for intent:" + pendingIntent.getIntentSender(), e); - mExecutor.execute(() -> mCallback.onError( - new CreateCredentialException(CreateCredentialException.TYPE_UNKNOWN))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onError( + new CreateCredentialException(CreateCredentialException.TYPE_UNKNOWN))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } @Override public void onResponse(CreateCredentialResponse response) { - mExecutor.execute(() -> mCallback.onResult(response)); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onResult(response)); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new CreateCredentialException(errorType, message))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute( + () -> mCallback.onError(new CreateCredentialException(errorType, message))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } @@ -702,13 +755,24 @@ public final class CredentialManager { @Override public void onSuccess() { - mCallback.onResult(null); + final long identity = Binder.clearCallingIdentity(); + try { + mCallback.onResult(null); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new ClearCredentialStateException(errorType, message))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute( + () -> mCallback.onError( + new ClearCredentialStateException(errorType, message))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } @@ -725,18 +789,34 @@ public final class CredentialManager { } public void onResponse(Void result) { - mExecutor.execute(() -> mCallback.onResult(result)); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onResult(result)); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onResponse() { - mExecutor.execute(() -> mCallback.onResult(null)); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onResult(null)); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new SetEnabledProvidersException(errorType, message))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute( + () -> mCallback.onError( + new SetEnabledProvidersException(errorType, message))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } } diff --git a/core/java/android/credentials/PrepareGetCredentialResponse.java b/core/java/android/credentials/PrepareGetCredentialResponse.java index 81e906859cb8..056b18a51b6d 100644 --- a/core/java/android/credentials/PrepareGetCredentialResponse.java +++ b/core/java/android/credentials/PrepareGetCredentialResponse.java @@ -24,6 +24,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.app.Activity; import android.app.PendingIntent; +import android.content.Context; import android.content.IntentSender; import android.os.CancellationSignal; import android.os.OutcomeReceiver; @@ -67,7 +68,7 @@ public final class PrepareGetCredentialResponse { } /** @hide */ - void show(@NonNull Activity activity, @Nullable CancellationSignal cancellationSignal, + void show(@NonNull Context context, @Nullable CancellationSignal cancellationSignal, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) { if (mPendingIntent == null) { @@ -80,7 +81,7 @@ public final class PrepareGetCredentialResponse { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - activity.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + context.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent for show()", e); executor.execute(() -> callback.onError( @@ -101,7 +102,7 @@ public final class PrepareGetCredentialResponse { }); try { - activity.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0); + context.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent for show()", e); executor.execute(() -> callback.onError( diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index fd80981fe4b8..593f3dbb99de 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -4604,7 +4604,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) { + @NonNull + private DisplayMetrics getDisplayMetricsOrSystem() { Context c = getContext(); Resources r; @@ -4614,8 +4615,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener r = c.getResources(); } + return r.getDisplayMetrics(); + } + + private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) { mTextSizeUnit = unit; - setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()), + setRawTextSize(TypedValue.applyDimension(unit, size, getDisplayMetricsOrSystem()), shouldRequestLayout); } @@ -6197,10 +6202,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ @android.view.RemotableViewMethod public void setLineHeight(@Px @IntRange(from = 0) int lineHeight) { - Preconditions.checkArgumentNonnegative(lineHeight); + setLineHeightPx(lineHeight); + } + + private void setLineHeightPx(@Px @FloatRange(from = 0) float lineHeight) { + Preconditions.checkArgumentNonnegative((int) lineHeight); final int fontHeight = getPaint().getFontMetricsInt(null); // Make sure we don't setLineSpacing if it's not needed to avoid unnecessary redraw. + // TODO(b/274974975): should this also check if lineSpacing needs to change? if (lineHeight != fontHeight) { // Set lineSpacingExtra by the difference of lineSpacing with lineHeight setLineSpacing(lineHeight - fontHeight, 1f); @@ -6208,6 +6218,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Sets an explicit line height to a given unit and value for this TextView. This is equivalent + * to the vertical distance between subsequent baselines in the TextView. See {@link + * TypedValue} for the possible dimension units. + * + * @param unit The desired dimension unit. SP units are strongly recommended so that line height + * stays proportional to the text size when fonts are scaled up for accessibility. + * @param lineHeight The desired line height in the given units. + * + * @see #setLineSpacing(float, float) + * @see #getLineSpacingExtra() + * + * @attr ref android.R.styleable#TextView_lineHeight + */ + @android.view.RemotableViewMethod + public void setLineHeight(int unit, @FloatRange(from = 0) float lineHeight) { + setLineHeightPx( + TypedValue.applyDimension(unit, lineHeight, getDisplayMetricsOrSystem())); + } + + /** * Set Highlights * * @param highlights A highlight object. Call with null for reset. diff --git a/core/java/com/android/internal/expresslog/Histogram.java b/core/java/com/android/internal/expresslog/Histogram.java index 65fbb03bf967..2fe784a5a855 100644 --- a/core/java/com/android/internal/expresslog/Histogram.java +++ b/core/java/com/android/internal/expresslog/Histogram.java @@ -54,6 +54,19 @@ public final class Histogram { /*count*/ 1, binIndex); } + /** + * Logs increment sample count for automatically calculated bin + * + * @param uid used as a dimension for the count metric + * @param sample value + * @hide + */ + public void logSampleWithUid(int uid, float sample) { + final int binIndex = mBinOptions.getBinForSample(sample); + FrameworkStatsLog.write(FrameworkStatsLog.EXPRESS_UID_HISTOGRAM_SAMPLE_REPORTED, + mMetricIdHash, /*count*/ 1, binIndex, uid); + } + /** Used by Histogram to map data sample to corresponding bin */ public interface BinOptions { /** diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp index 0c3ff6c28ea7..410b44161cf6 100644 --- a/core/jni/android_view_DisplayEventReceiver.cpp +++ b/core/jni/android_view_DisplayEventReceiver.cpp @@ -18,19 +18,17 @@ //#define LOG_NDEBUG 0 -#include <nativehelper/JNIHelp.h> - -#include <inttypes.h> - #include <android_runtime/AndroidRuntime.h> +#include <android_runtime/Log.h> #include <gui/DisplayEventDispatcher.h> +#include <inttypes.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedLocalRef.h> #include <utils/Log.h> #include <utils/Looper.h> #include <utils/threads.h> -#include "android_os_MessageQueue.h" - -#include <nativehelper/ScopedLocalRef.h> +#include "android_os_MessageQueue.h" #include "core_jni_helpers.h" namespace android { @@ -133,6 +131,12 @@ static jobject createJavaVsyncEventData(JNIEnv* env, VsyncEventData vsyncEventDa gDisplayEventReceiverClassInfo .frameTimelineClassInfo.clazz, /*initial element*/ NULL)); + if (!frameTimelineObjs.get() || env->ExceptionCheck()) { + ALOGW("%s: Failed to create FrameTimeline array", __func__); + LOGW_EX(env); + env->ExceptionClear(); + return NULL; + } for (int i = 0; i < VsyncEventData::kFrameTimelinesLength; i++) { VsyncEventData::FrameTimeline frameTimeline = vsyncEventData.frameTimelines[i]; ScopedLocalRef<jobject> @@ -144,6 +148,12 @@ static jobject createJavaVsyncEventData(JNIEnv* env, VsyncEventData vsyncEventDa frameTimeline.vsyncId, frameTimeline.expectedPresentationTime, frameTimeline.deadlineTimestamp)); + if (!frameTimelineObj.get() || env->ExceptionCheck()) { + ALOGW("%s: Failed to create FrameTimeline object", __func__); + LOGW_EX(env); + env->ExceptionClear(); + return NULL; + } env->SetObjectArrayElement(frameTimelineObjs.get(), i, frameTimelineObj.get()); } return env->NewObject(gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.clazz, diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp index 49f47c56e6a6..2e9f1790a4a5 100644 --- a/core/jni/android_view_MotionEvent.cpp +++ b/core/jni/android_view_MotionEvent.cpp @@ -316,8 +316,9 @@ static void pointerPropertiesToNative(JNIEnv* env, jobject pointerPropertiesObj, outPointerProperties->clear(); outPointerProperties->id = env->GetIntField(pointerPropertiesObj, gPointerPropertiesClassInfo.id); - outPointerProperties->toolType = env->GetIntField(pointerPropertiesObj, + const int32_t toolType = env->GetIntField(pointerPropertiesObj, gPointerPropertiesClassInfo.toolType); + outPointerProperties->toolType = static_cast<ToolType>(toolType); } static void pointerPropertiesFromNative(JNIEnv* env, const PointerProperties* pointerProperties, @@ -325,7 +326,7 @@ static void pointerPropertiesFromNative(JNIEnv* env, const PointerProperties* po env->SetIntField(outPointerPropertiesObj, gPointerPropertiesClassInfo.id, pointerProperties->id); env->SetIntField(outPointerPropertiesObj, gPointerPropertiesClassInfo.toolType, - pointerProperties->toolType); + static_cast<int32_t>(pointerProperties->toolType)); } @@ -535,7 +536,7 @@ static jint android_view_MotionEvent_nativeGetToolType(JNIEnv* env, jclass clazz if (!validatePointerIndex(env, pointerIndex, *event)) { return -1; } - return event->getToolType(pointerIndex); + return static_cast<jint>(event->getToolType(pointerIndex)); } static jlong android_view_MotionEvent_nativeGetEventTimeNanos(JNIEnv* env, jclass clazz, diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 5a35ca74e5dc..239747a37ec6 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3345,6 +3345,11 @@ <!-- The duration in which a recent task is considered in session and should be visible. --> <integer name="config_activeTaskDurationHours">6</integer> + <!-- Whether this device prefers to show snapshot or splash screen on back predict target. + When set true, there will create windowless starting surface for the preview target, so it + won't affect activity's lifecycle. This should only be disabled on low-ram device. --> + <bool name="config_predictShowStartingSurface">true</bool> + <!-- default window ShowCircularMask property --> <bool name="config_windowShowCircularMask">false</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 5c734df4ebf3..a09cee797878 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -393,6 +393,7 @@ <java-symbol type="integer" name="config_maxNumVisibleRecentTasks" /> <java-symbol type="integer" name="config_activeTaskDurationHours" /> <java-symbol type="bool" name="config_windowShowCircularMask" /> + <java-symbol type="bool" name="config_predictShowStartingSurface" /> <java-symbol type="bool" name="config_windowEnableCircularEmulatorDisplayOverlay" /> <java-symbol type="bool" name="config_supportMicNearUltrasound" /> <java-symbol type="bool" name="config_supportSpeakerNearUltrasound" /> diff --git a/core/tests/coretests/src/android/credentials/CredentialManagerTest.java b/core/tests/coretests/src/android/credentials/CredentialManagerTest.java index c7e026123487..b0d5240c61ba 100644 --- a/core/tests/coretests/src/android/credentials/CredentialManagerTest.java +++ b/core/tests/coretests/src/android/credentials/CredentialManagerTest.java @@ -144,7 +144,7 @@ public class CredentialManagerTest { public void testGetCredential_nullRequest() { GetCredentialRequest nullRequest = null; assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(nullRequest, mMockActivity, null, mExecutor, + () -> mCredentialManager.getCredential(mMockActivity, nullRequest, null, mExecutor, result -> { })); } @@ -152,7 +152,7 @@ public class CredentialManagerTest { @Test public void testGetCredential_nullActivity() { assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(mGetRequest, null, null, mExecutor, + () -> mCredentialManager.getCredential(null, mGetRequest, null, mExecutor, result -> { })); } @@ -160,7 +160,7 @@ public class CredentialManagerTest { @Test public void testGetCredential_nullExecutor() { assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(mGetRequest, mMockActivity, null, null, + () -> mCredentialManager.getCredential(mMockActivity, mGetRequest, null, null, result -> { })); } @@ -168,7 +168,7 @@ public class CredentialManagerTest { @Test public void testGetCredential_nullCallback() { assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(mGetRequest, mMockActivity, null, null, + () -> mCredentialManager.getCredential(mMockActivity, mGetRequest, null, null, null)); } @@ -184,7 +184,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeGetCredential(any(), callbackCaptor.capture(), any())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.getCredential(mGetRequest, mMockActivity, null, mExecutor, callback); + mCredentialManager.getCredential(mMockActivity, mGetRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).executeGetCredential(any(), any(), eq(mPackageName)); callbackCaptor.getValue().onError(GetCredentialException.TYPE_NO_CREDENTIAL, @@ -200,7 +200,7 @@ public class CredentialManagerTest { final CancellationSignal cancellation = new CancellationSignal(); cancellation.cancel(); - mCredentialManager.getCredential(mGetRequest, mMockActivity, cancellation, mExecutor, + mCredentialManager.getCredential(mMockActivity, mGetRequest, cancellation, mExecutor, result -> { }); @@ -218,7 +218,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeGetCredential(any(), any(), any())).thenReturn( serviceSignal); - mCredentialManager.getCredential(mGetRequest, mMockActivity, cancellation, mExecutor, + mCredentialManager.getCredential(mMockActivity, mGetRequest, cancellation, mExecutor, callback); verify(mMockCredentialManagerService).executeGetCredential(any(), any(), eq(mPackageName)); @@ -241,7 +241,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeGetCredential(any(), callbackCaptor.capture(), any())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.getCredential(mGetRequest, mMockActivity, null, mExecutor, callback); + mCredentialManager.getCredential(mMockActivity, mGetRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).executeGetCredential(any(), any(), eq(mPackageName)); callbackCaptor.getValue().onResponse(new GetCredentialResponse(cred)); @@ -253,7 +253,7 @@ public class CredentialManagerTest { @Test public void testCreateCredential_nullRequest() { assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(null, mMockActivity, null, mExecutor, + () -> mCredentialManager.createCredential(mMockActivity, null, null, mExecutor, result -> { })); } @@ -261,7 +261,7 @@ public class CredentialManagerTest { @Test public void testCreateCredential_nullActivity() { assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(mCreateRequest, null, null, mExecutor, + () -> mCredentialManager.createCredential(null, mCreateRequest, null, mExecutor, result -> { })); } @@ -269,7 +269,7 @@ public class CredentialManagerTest { @Test public void testCreateCredential_nullExecutor() { assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(mCreateRequest, mMockActivity, null, null, + () -> mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, null, result -> { })); } @@ -277,7 +277,7 @@ public class CredentialManagerTest { @Test public void testCreateCredential_nullCallback() { assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(mCreateRequest, mMockActivity, null, + () -> mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, mExecutor, null)); } @@ -286,7 +286,7 @@ public class CredentialManagerTest { final CancellationSignal cancellation = new CancellationSignal(); cancellation.cancel(); - mCredentialManager.createCredential(mCreateRequest, mMockActivity, cancellation, mExecutor, + mCredentialManager.createCredential(mMockActivity, mCreateRequest, cancellation, mExecutor, result -> { }); @@ -304,7 +304,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeCreateCredential(any(), any(), any())).thenReturn( serviceSignal); - mCredentialManager.createCredential(mCreateRequest, mMockActivity, cancellation, mExecutor, + mCredentialManager.createCredential(mMockActivity, mCreateRequest, cancellation, mExecutor, callback); verify(mMockCredentialManagerService).executeCreateCredential(any(), any(), @@ -326,7 +326,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeCreateCredential(any(), callbackCaptor.capture(), any())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.createCredential(mCreateRequest, mMockActivity, null, mExecutor, + mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).executeCreateCredential(any(), any(), eq(mPackageName)); @@ -353,7 +353,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeCreateCredential(any(), callbackCaptor.capture(), any())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.createCredential(mCreateRequest, mMockActivity, null, mExecutor, + mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).executeCreateCredential(any(), any(), eq(mPackageName)); diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 51f99ec637da..22462eb11a8f 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -38,8 +38,11 @@ import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; import android.net.Uri; import android.os.Build; +import android.os.SystemProperties; import android.os.Trace; import android.system.ErrnoException; import android.system.Os; @@ -928,6 +931,8 @@ public final class ImageDecoder implements AutoCloseable { case "image/x-pentax-pef": case "image/x-samsung-srw": return true; + case "image/avif": + return isP010SupportedForAV1(); default: return false; } @@ -2063,6 +2068,49 @@ public final class ImageDecoder implements AutoCloseable { return decodeBitmapImpl(src, null); } + private static boolean sIsP010SupportedForAV1 = false; + private static boolean sIsP010SupportedForAV1Initialized = false; + + /** + * Checks if the device supports decoding 10-bit AV1. + */ + private static boolean isP010SupportedForAV1() { + if (sIsP010SupportedForAV1Initialized) { + return sIsP010SupportedForAV1; + } + + sIsP010SupportedForAV1Initialized = true; + + if (hasHardwareDecoder("video/av01")) { + sIsP010SupportedForAV1 = true; + return true; + } + + sIsP010SupportedForAV1 = SystemProperties.getInt("ro.product.first_api_level", 0) >= + Build.VERSION_CODES.S; + return sIsP010SupportedForAV1; + } + + /** + * Checks if the device has hardware decoder for the target mime type. + */ + private static boolean hasHardwareDecoder(String mime) { + final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (MediaCodecInfo info : sMCL.getCodecInfos()) { + if (info.isEncoder() == false && info.isHardwareAccelerated()) { + try { + if (info.getCapabilitiesForType(mime) != null) { + return true; + } + } catch (IllegalArgumentException e) { + // mime is not supported + return false; + } + } + } + return false; + } + /** * Private method called by JNI. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 349ff36ce8ce..6f993aebdc65 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -384,8 +384,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private void onMove() { - if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get() - || mActiveCallback == null) { + if (!mBackGestureStarted || mBackNavigationInfo == null || mActiveCallback == null) { return; } @@ -424,9 +423,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } try { - if (mEnableAnimations.get()) { - callback.onBackStarted(backEvent); - } + callback.onBackStarted(backEvent); } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackStarted error: ", e); } @@ -448,9 +445,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } try { - if (mEnableAnimations.get()) { - callback.onBackCancelled(); - } + callback.onBackCancelled(); } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackCancelled error: ", e); } @@ -462,19 +457,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } try { - if (mEnableAnimations.get()) { - callback.onBackProgressed(backEvent); - } + callback.onBackProgressed(backEvent); } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackProgressed error: ", e); } } - private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) { - // TODO(b/258698745): Only dispatch to animation callbacks. - return mEnableAnimations.get(); - } - /** * Sets to true when the back gesture has passed the triggering threshold, false otherwise. */ diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt new file mode 100644 index 000000000000..083cfd294f96 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.RequiresDevice +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule +import com.android.server.wm.flicker.testapp.ActivityOptions +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test the dragging of a PIP window. + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipDragTest(flicker: FlickerTest) : PipTransition(flicker) { + private var isDraggedLeft: Boolean = true + override val transition: FlickerBuilder.() -> Unit + get() = { + val stringExtras = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true") + + setup { + tapl.setEnableRotation(true) + // Launch the PIP activity and wait for it to enter PiP mode + RemoveAllTasksButHomeRule.removeAllTasksButHome() + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) + + // determine the direction of dragging to test for + isDraggedLeft = pipApp.isCloserToRightEdge(wmHelper) + } + teardown { + // release the primary pointer after dragging without release + pipApp.releasePipAfterDragging() + + pipApp.exit(wmHelper) + tapl.setEnableRotation(false) + } + transitions { + pipApp.dragPipWindowAwayFromEdgeWithoutRelease(wmHelper, 50) + } + } + + @Postsubmit + @Test + fun pipLayerMovesAwayFromEdge() { + flicker.assertLayers { + val pipLayerList = layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + if (isDraggedLeft) { + previous.visibleRegion.isToTheRight(current.visibleRegion.region) + } else { + current.visibleRegion.isToTheRight(previous.visibleRegion.region) + } + } + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 169b9bd4dea7..806bffebd4cb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -253,8 +253,6 @@ public class BackAnimationControllerTest extends ShellTestCase { triggerBackGesture(); - verify(mAppCallback, never()).onBackStarted(any()); - verify(mAppCallback, never()).onBackProgressed(backEventCaptor.capture()); verify(mAppCallback, times(1)).onBackInvoked(); verify(mAnimatorCallback, never()).onBackStarted(any()); diff --git a/native/android/input.cpp b/native/android/input.cpp index f1c30889c4db..432e21cb5c08 100644 --- a/native/android/input.cpp +++ b/native/android/input.cpp @@ -149,7 +149,8 @@ int32_t AMotionEvent_getPointerId(const AInputEvent* motion_event, size_t pointe } int32_t AMotionEvent_getToolType(const AInputEvent* motion_event, size_t pointer_index) { - return static_cast<const MotionEvent*>(motion_event)->getToolType(pointer_index); + const MotionEvent& motion = static_cast<const MotionEvent&>(*motion_event); + return static_cast<int32_t>(motion.getToolType(pointer_index)); } float AMotionEvent_getRawX(const AInputEvent* motion_event, size_t pointer_index) { diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index d92e65c3c581..ff570524ca0e 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -947,18 +947,6 @@ android:visibleToInstantApps="true"> </activity> - <activity android:name=".user.UserSwitcherActivity" - android:label="@string/accessibility_multi_user_switch_switcher" - android:theme="@style/Theme.UserSwitcherActivity" - android:excludeFromRecents="true" - android:showWhenLocked="true" - android:showForAllUsers="true" - android:finishOnTaskLaunch="true" - android:lockTaskMode="always" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden" - android:visibleToInstantApps="true"> - </activity> - <receiver android:name=".controls.management.ControlsRequestReceiver" android:exported="true"> <intent-filter> diff --git a/packages/SystemUI/docs/user-switching.md b/packages/SystemUI/docs/user-switching.md index b9509eb41c3c..01cba426f782 100644 --- a/packages/SystemUI/docs/user-switching.md +++ b/packages/SystemUI/docs/user-switching.md @@ -6,7 +6,7 @@ Multiple users and the ability to switch between them is controlled by Settings ### Quick Settings -In the QS footer, an icon becomes available for users to tap on. The view and its onClick actions are handled by [MultiUserSwitchController][2]. Multiple visual implementations are currently in use; one for phones/foldables ([UserSwitchDialogController][6]) and one for tablets ([UserSwitcherActivity][5]). +In the QS footer, an icon becomes available for users to tap on. The view and its onClick actions are handled by [MultiUserSwitchController][2]. Multiple visual implementations are currently in use; one for phones/foldables ([UserSwitchDialogController][6]) and one for tablets ([UserSwitcherFullscreenDialog][5]). ### Bouncer @@ -29,7 +29,7 @@ All visual implementations should derive their logic and use the adapter specifi ## Visual Components -### [UserSwitcherActivity][5] +### [UserSwitcherFullscreenDialog][5] A fullscreen user switching activity, supporting add guest/user actions if configured. @@ -41,5 +41,5 @@ Renders user switching as a dialog over the current surface, and supports add gu [2]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserController.java [3]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java [4]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java -[5]: /frameworks/base/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt +[5]: /frameworks/base/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt [6]: /frameworks/base/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 5b6a83c24e3d..0080517a722b 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -137,6 +137,12 @@ interface ClockAnimations { fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {} /** + * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview, + * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize + */ + fun onPickerCarouselSwiping(swipingFraction: Float, previewRatio: Float) {} + + /** * Whether this clock has a custom position update animation. If true, the keyguard will call * `onPositionUpdated` to notify the clock of a position update animation. If false, a default * animation will be used (e.g. a simple translation). diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 2fb1592dfe15..a3655c31fde9 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -844,12 +844,10 @@ <item name="wallpaperTextColor">@*android:color/primary_text_material_dark</item> </style> - <style name="Theme.UserSwitcherActivity" parent="@android:style/Theme.DeviceDefault.NoActionBar"> + <style name="Theme.UserSwitcherFullscreenDialog" parent="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"> <item name="android:statusBarColor">@color/user_switcher_fullscreen_bg</item> <item name="android:windowBackground">@color/user_switcher_fullscreen_bg</item> <item name="android:navigationBarColor">@color/user_switcher_fullscreen_bg</item> - <!-- Setting a placeholder will avoid using the SystemUI icon on the splash screen --> - <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_blank</item> </style> <style name="Theme.CreateUser" parent="@android:style/Theme.DeviceDefault.NoActionBar"> diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 79a51d6670c4..7ffb59422bc4 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -701,6 +701,6 @@ object Flags { // TODO(b/272805037): Tracking Bug @JvmField - val ADVANCED_VPN_ENABLED = unreleasedFlag(2800, name = "AdvancedVpn__enable_feature", - namespace = "vpn", teamfood = true) + val ADVANCED_VPN_ENABLED = releasedFlag(2800, name = "AdvancedVpn__enable_feature", + namespace = "vpn") } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt index 84abf57cacf2..d5129a612b04 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt @@ -22,10 +22,10 @@ import android.content.Context import android.content.IntentFilter import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback -import android.os.Looper import android.os.UserHandle import android.util.Log import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.biometrics.AuthController @@ -35,8 +35,8 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.TAG import com.android.systemui.keyguard.shared.model.DevicePosture import com.android.systemui.user.data.repository.UserRepository import java.io.PrintWriter @@ -45,10 +45,12 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn @@ -93,8 +95,16 @@ interface BiometricSettingsRepository { * restricted to specific postures using [R.integer.config_face_auth_supported_posture] */ val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> + + /** + * Whether the user manually locked down the device. This doesn't include device policy manager + * lockdown. + */ + val isCurrentUserInLockdown: Flow<Boolean> } +const val TAG = "BiometricsRepositoryImpl" + @SysUISingleton class BiometricSettingsRepositoryImpl @Inject @@ -103,19 +113,25 @@ constructor( lockPatternUtils: LockPatternUtils, broadcastDispatcher: BroadcastDispatcher, authController: AuthController, - userRepository: UserRepository, + private val userRepository: UserRepository, devicePolicyManager: DevicePolicyManager, @Application scope: CoroutineScope, @Background backgroundDispatcher: CoroutineDispatcher, biometricManager: BiometricManager?, - @Main looper: Looper, devicePostureRepository: DevicePostureRepository, dumpManager: DumpManager, ) : BiometricSettingsRepository, Dumpable { override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> + private val strongAuthTracker = StrongAuthTracker(userRepository, context) + + override val isCurrentUserInLockdown: Flow<Boolean> = + strongAuthTracker.currentUserAuthFlags.map { it.isInUserLockdown } + init { + Log.d(TAG, "Registering StrongAuthTracker") + lockPatternUtils.registerStrongAuthTracker(strongAuthTracker) dumpManager.registerDumpable(this) val configFaceAuthSupportedPosture = DevicePosture.toPosture( @@ -251,38 +267,14 @@ constructor( .stateIn(scope, SharingStarted.Eagerly, false) override val isStrongBiometricAllowed: StateFlow<Boolean> = - selectedUserId - .flatMapLatest { currUserId -> - conflatedCallbackFlow { - val callback = - object : LockPatternUtils.StrongAuthTracker(context, looper) { - override fun onStrongAuthRequiredChanged(userId: Int) { - if (currUserId != userId) { - return - } - - trySendWithFailureLogging( - isBiometricAllowedForUser(true, currUserId), - TAG - ) - } - - override fun onIsNonStrongBiometricAllowedChanged(userId: Int) { - // no-op - } - } - lockPatternUtils.registerStrongAuthTracker(callback) - awaitClose { lockPatternUtils.unregisterStrongAuthTracker(callback) } - } - } - .stateIn( - scope, - started = SharingStarted.Eagerly, - initialValue = - lockPatternUtils.isBiometricAllowedForUser( - userRepository.getSelectedUserInfo().id - ) + strongAuthTracker.isStrongBiometricAllowed.stateIn( + scope, + SharingStarted.Eagerly, + strongAuthTracker.isBiometricAllowedForUser( + true, + userRepository.getSelectedUserInfo().id ) + ) override val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> = selectedUserId @@ -300,9 +292,44 @@ constructor( userRepository.getSelectedUserInfo().id ) ) +} - companion object { - private const val TAG = "BiometricsRepositoryImpl" +private class StrongAuthTracker(private val userRepository: UserRepository, context: Context?) : + LockPatternUtils.StrongAuthTracker(context) { + + private val _authFlags = + MutableStateFlow( + StrongAuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId)) + ) + + val currentUserAuthFlags: Flow<StrongAuthenticationFlags> = + userRepository.selectedUserInfo + .map { it.id } + .distinctUntilChanged() + .flatMapLatest { currUserId -> + _authFlags + .filter { it.userId == currUserId } + .onEach { Log.d(TAG, "currentUser authFlags changed, new value: $it") } + .onStart { + emit( + StrongAuthenticationFlags( + currentUserId, + getStrongAuthForUser(currentUserId) + ) + ) + } + } + + val isStrongBiometricAllowed: Flow<Boolean> = + currentUserAuthFlags.map { isBiometricAllowedForUser(true, it.userId) } + + private val currentUserId + get() = userRepository.getSelectedUserInfo().id + + override fun onStrongAuthRequiredChanged(userId: Int) { + val newFlags = getStrongAuthForUser(userId) + _authFlags.value = StrongAuthenticationFlags(userId, newFlags) + Log.d(TAG, "onStrongAuthRequiredChanged for userId: $userId, flag value: $newFlags") } } @@ -314,3 +341,11 @@ private fun DevicePolicyManager.isFingerprintDisabled(userId: Int): Boolean = private fun DevicePolicyManager.isNotActive(userId: Int, policy: Int): Boolean = (getKeyguardDisabledFeatures(null, userId) and policy) == 0 + +private data class StrongAuthenticationFlags(val userId: Int, val flag: Int) { + val isInUserLockdown = containsFlag(flag, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN) +} + +private fun containsFlag(haystack: Int, needle: Int): Boolean { + return haystack and needle != 0 +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt index 8387c1dd60a5..b394a079fb00 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt @@ -89,7 +89,7 @@ interface FooterActionsInteractor { fun showSettings(expandable: Expandable) /** Show the user switcher. */ - fun showUserSwitcher(context: Context, expandable: Expandable) + fun showUserSwitcher(expandable: Expandable) } @SysUISingleton @@ -177,7 +177,7 @@ constructor( ) } - override fun showUserSwitcher(context: Context, expandable: Expandable) { - userInteractor.showUserSwitcher(context, expandable) + override fun showUserSwitcher(expandable: Expandable) { + userInteractor.showUserSwitcher(expandable) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index f170ac1d9d4e..3a9098ab49d3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -230,7 +230,7 @@ class FooterActionsViewModel( return } - footerActionsInteractor.showUserSwitcher(context, expandable) + footerActionsInteractor.showUserSwitcher(expandable) } private fun onSettingsButtonClicked(expandable: Expandable) { diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index f7c8bac1b478..b2bf9727b534 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -16,7 +16,6 @@ package com.android.systemui.user; -import android.app.Activity; import android.os.UserHandle; import com.android.settingslib.users.EditUserInfoController; @@ -24,11 +23,8 @@ import com.android.systemui.user.data.repository.UserRepositoryModule; import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule; import com.android.systemui.user.ui.dialog.UserDialogModule; -import dagger.Binds; import dagger.Module; import dagger.Provides; -import dagger.multibindings.ClassKey; -import dagger.multibindings.IntoMap; /** * Dagger module for User related classes. @@ -49,12 +45,6 @@ public abstract class UserModule { return new EditUserInfoController(FILE_PROVIDER_AUTHORITY); } - /** Provides UserSwitcherActivity */ - @Binds - @IntoMap - @ClassKey(UserSwitcherActivity.class) - public abstract Activity provideUserSwitcherActivity(UserSwitcherActivity activity); - /** * Provides the {@link UserHandle} for the user associated with this System UI process. * diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt deleted file mode 100644 index 52b7fb63c1a2..000000000000 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.user - -import android.os.Bundle -import android.view.WindowInsets.Type -import android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE -import androidx.activity.ComponentActivity -import androidx.lifecycle.ViewModelProvider -import com.android.systemui.R -import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.user.ui.binder.UserSwitcherViewBinder -import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel -import dagger.Lazy -import javax.inject.Inject - -/** Support a fullscreen user switcher */ -open class UserSwitcherActivity -@Inject -constructor( - private val falsingCollector: FalsingCollector, - private val viewModelFactory: Lazy<UserSwitcherViewModel.Factory>, -) : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.user_switcher_fullscreen) - window.decorView.windowInsetsController?.let { controller -> - controller.systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - controller.hide(Type.systemBars()) - } - val viewModel = - ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java] - UserSwitcherViewBinder.bind( - view = requireViewById(R.id.user_switcher_root), - viewModel = viewModel, - lifecycleOwner = this, - layoutInflater = layoutInflater, - falsingCollector = falsingCollector, - onFinish = this::finish, - ) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt new file mode 100644 index 000000000000..72786efc416d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.user + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.WindowInsets +import android.view.WindowInsetsController +import com.android.systemui.R +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.user.ui.binder.UserSwitcherViewBinder +import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel + +class UserSwitchFullscreenDialog( + context: Context, + private val falsingCollector: FalsingCollector, + private val userSwitcherViewModel: UserSwitcherViewModel, +) : SystemUIDialog(context, R.style.Theme_UserSwitcherFullscreenDialog) { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setShowForAllUsers(true) + setCanceledOnTouchOutside(true) + + window?.decorView?.windowInsetsController?.let { controller -> + controller.systemBarsBehavior = + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + controller.hide(WindowInsets.Type.systemBars()) + } + + val view = + LayoutInflater.from(this.context).inflate(R.layout.user_switcher_fullscreen, null) + setContentView(view) + + UserSwitcherViewBinder.bind( + view = requireViewById(R.id.user_switcher_root), + viewModel = userSwitcherViewModel, + layoutInflater = layoutInflater, + falsingCollector = falsingCollector, + onFinish = this::dismiss, + ) + } + + override fun getWidth(): Int { + val displayMetrics = context.resources.displayMetrics.apply { + context.display.getRealMetrics(this) + } + return displayMetrics.widthPixels + } + + override fun getHeight() = MATCH_PARENT + +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 94dd1b309436..0ec1a214660c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -49,7 +49,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.telephony.domain.interactor.TelephonyInteractor -import com.android.systemui.user.UserSwitcherActivity import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.user.data.source.UserRecord @@ -513,24 +512,12 @@ constructor( } } - fun showUserSwitcher(context: Context, expandable: Expandable) { - if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { + fun showUserSwitcher(expandable: Expandable) { + if (featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { + showDialog(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) + } else { showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable)) - return } - - val intent = - Intent(context, UserSwitcherActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - } - - activityStarter.startActivity( - intent, - true /* dismissShade */, - expandable.activityLaunchController(), - true /* showOverlockscreenwhenlocked */, - UserHandle.SYSTEM, - ) } private fun showDialog(request: ShowDialogRequestModel) { diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt index 14cc3e783fed..de73cdbb6026 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt @@ -50,4 +50,8 @@ sealed class ShowDialogRequestModel( data class ShowUserSwitcherDialog( override val expandable: Expandable?, ) : ShowDialogRequestModel() + + data class ShowUserSwitcherFullscreenDialog( + override val expandable: Expandable?, + ) : ShowDialogRequestModel() } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt index e13710786fbb..7236e0fd134a 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt @@ -31,19 +31,18 @@ import android.widget.TextView import androidx.constraintlayout.helper.widget.Flow as FlowWidget import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.user.UserSwitcherPopupMenu import com.android.systemui.user.UserSwitcherRootView import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.util.children -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -56,7 +55,6 @@ object UserSwitcherViewBinder { fun bind( view: ViewGroup, viewModel: UserSwitcherViewModel, - lifecycleOwner: LifecycleOwner, layoutInflater: LayoutInflater, falsingCollector: FalsingCollector, onFinish: () -> Unit, @@ -79,88 +77,92 @@ object UserSwitcherViewBinder { addButton.setOnClickListener { viewModel.onOpenMenuButtonClicked() } cancelButton.setOnClickListener { viewModel.onCancelButtonClicked() } - lifecycleOwner.lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - viewModel.isFinishRequested - .filter { it } - .collect { - onFinish() - viewModel.onFinished() - } + view.repeatWhenAttached { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + viewModel.isFinishRequested + .filter { it } + .collect { + //finish requested, we want to dismiss popupmenu at the same time + popupMenu?.dismiss() + onFinish() + viewModel.onFinished() + } + } } } - } - lifecycleOwner.lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { viewModel.isOpenMenuButtonVisible.collect { addButton.isVisible = it } } - - launch { - viewModel.isMenuVisible.collect { isVisible -> - if (isVisible && popupMenu?.isShowing != true) { - popupMenu?.dismiss() - // Use post to make sure we show the popup menu *after* the activity is - // ready to show one to avoid a WindowManager$BadTokenException. - view.post { - popupMenu = - createAndShowPopupMenu( - context = view.context, - anchorView = addButton, - adapter = popupMenuAdapter, - onDismissed = viewModel::onMenuClosed, - ) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { viewModel.isOpenMenuButtonVisible.collect { addButton.isVisible = it } } + + launch { + viewModel.isMenuVisible.collect { isVisible -> + if (isVisible && popupMenu?.isShowing != true) { + popupMenu?.dismiss() + // Use post to make sure we show the popup menu *after* the activity is + // ready to show one to avoid a WindowManager$BadTokenException. + view.post { + popupMenu = + createAndShowPopupMenu( + context = view.context, + anchorView = addButton, + adapter = popupMenuAdapter, + onDismissed = viewModel::onMenuClosed, + ) + } + } else if (!isVisible && popupMenu?.isShowing == true) { + popupMenu?.dismiss() + popupMenu = null } - } else if (!isVisible && popupMenu?.isShowing == true) { - popupMenu?.dismiss() - popupMenu = null } } - } - launch { - viewModel.menu.collect { menuViewModels -> - popupMenuAdapter.setItems(menuViewModels) + launch { + viewModel.menu.collect { menuViewModels -> + popupMenuAdapter.setItems(menuViewModels) + } } - } - launch { - viewModel.maximumUserColumns.collect { maximumColumns -> - flowWidget.setMaxElementsWrap(maximumColumns) + launch { + viewModel.maximumUserColumns.collect { maximumColumns -> + flowWidget.setMaxElementsWrap(maximumColumns) + } } - } - launch { - viewModel.users.collect { users -> - val viewPool = - gridContainerView.children - .filter { it.tag == USER_VIEW_TAG } - .toMutableList() - viewPool.forEach { - gridContainerView.removeView(it) - flowWidget.removeView(it) - } - users.forEach { userViewModel -> - val userView = - if (viewPool.isNotEmpty()) { - viewPool.removeAt(0) - } else { - val inflatedView = - layoutInflater.inflate( - R.layout.user_switcher_fullscreen_item, - view, - false, - ) - inflatedView.tag = USER_VIEW_TAG - inflatedView - } - userView.id = View.generateViewId() - gridContainerView.addView(userView) - flowWidget.addView(userView) - UserViewBinder.bind( - view = userView, - viewModel = userViewModel, - ) + launch { + viewModel.users.collect { users -> + val viewPool = + gridContainerView.children + .filter { it.tag == USER_VIEW_TAG } + .toMutableList() + viewPool.forEach { + gridContainerView.removeView(it) + flowWidget.removeView(it) + } + users.forEach { userViewModel -> + val userView = + if (viewPool.isNotEmpty()) { + viewPool.removeAt(0) + } else { + val inflatedView = + layoutInflater.inflate( + R.layout.user_switcher_fullscreen_item, + view, + false, + ) + inflatedView.tag = USER_VIEW_TAG + inflatedView + } + userView.id = View.generateViewId() + gridContainerView.addView(userView) + flowWidget.addView(userView) + UserViewBinder.bind( + view = userView, + viewModel = userViewModel, + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index 79721b370c21..0930cb8a3d7a 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -26,13 +26,16 @@ import com.android.systemui.CoreStartable import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.tiles.UserDetailView +import com.android.systemui.user.UserSwitchFullscreenDialog import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import dagger.Lazy import javax.inject.Inject import javax.inject.Provider @@ -54,6 +57,8 @@ constructor( private val userDetailAdapterProvider: Provider<UserDetailView.Adapter>, private val eventLogger: Lazy<UiEventLogger>, private val activityStarter: Lazy<ActivityStarter>, + private val falsingCollector: Lazy<FalsingCollector>, + private val userSwitcherViewModel: Lazy<UserSwitcherViewModel>, ) : CoreStartable { private var currentDialog: Dialog? = null @@ -124,6 +129,15 @@ constructor( INTERACTION_JANK_EXIT_GUEST_MODE_TAG, ), ) + is ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog -> + Pair( + UserSwitchFullscreenDialog( + context = context.get(), + falsingCollector = falsingCollector.get(), + userSwitcherViewModel = userSwitcherViewModel.get(), + ), + null, /* dialogCuj */ + ) } currentDialog = dialog diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt index 3300e8e5b2a5..78edad7c3af2 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt @@ -55,5 +55,5 @@ constructor( interactor.selectedUser.mapLatest { userModel -> userModel.image } /** Action to execute on click. Should launch the user switcher */ - val onClick: (Expandable) -> Unit = { interactor.showUserSwitcher(context, it) } + val onClick: (Expandable) -> Unit = { interactor.showUserSwitcher(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 37115ad53880..afd72e7ed1be 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -17,12 +17,10 @@ package com.android.systemui.user.ui.viewmodel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import com.android.systemui.R import com.android.systemui.common.shared.model.Text import com.android.systemui.common.ui.drawable.CircularDrawable -import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper @@ -36,12 +34,13 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map /** Models UI state for the user switcher feature. */ +@SysUISingleton class UserSwitcherViewModel -private constructor( +@Inject +constructor( private val userInteractor: UserInteractor, private val guestUserInteractor: GuestUserInteractor, - private val powerInteractor: PowerInteractor, -) : ViewModel() { +) { /** On-device users. */ val users: Flow<List<UserViewModel>> = @@ -112,34 +111,15 @@ private constructor( } } - private fun createFinishRequestedFlow(): Flow<Boolean> { - var mostRecentSelectedUserId: Int? = null - var mostRecentIsInteractive: Boolean? = null - - return combine( - // When the user is switched, we should finish. - userInteractor.selectedUser - .map { it.id } - .map { - val selectedUserChanged = - mostRecentSelectedUserId != null && mostRecentSelectedUserId != it - mostRecentSelectedUserId = it - selectedUserChanged - }, - // When the screen turns off, we should finish. - powerInteractor.isInteractive.map { - val screenTurnedOff = mostRecentIsInteractive == true && !it - mostRecentIsInteractive = it - screenTurnedOff - }, + private fun createFinishRequestedFlow(): Flow<Boolean> = + combine( // When the cancel button is clicked, we should finish. hasCancelButtonBeenClicked, // If an executed action told us to finish, we should finish, isFinishRequiredDueToExecutedAction, - ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish -> - selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish + ) { cancelButtonClicked, executedActionFinish -> + cancelButtonClicked || executedActionFinish } - } private fun toViewModel( model: UserModel, @@ -210,22 +190,4 @@ private constructor( { userInteractor.selectUser(model.id) } } } - - class Factory - @Inject - constructor( - private val userInteractor: UserInteractor, - private val guestUserInteractor: GuestUserInteractor, - private val powerInteractor: PowerInteractor, - ) : ViewModelProvider.Factory { - override fun <T : ViewModel> create(modelClass: Class<T>): T { - @Suppress("UNCHECKED_CAST") - return UserSwitcherViewModel( - userInteractor = userInteractor, - guestUserInteractor = guestUserInteractor, - powerInteractor = powerInteractor, - ) - as T - } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt index 5dc04f7efa63..fb7d379c0627 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt @@ -30,6 +30,8 @@ import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController @@ -42,7 +44,6 @@ import com.android.systemui.keyguard.data.repository.BiometricType.UNDER_DISPLAY import com.android.systemui.keyguard.shared.model.DevicePosture import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -78,6 +79,8 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var biometricManager: BiometricManager + @Captor + private lateinit var strongAuthTracker: ArgumentCaptor<LockPatternUtils.StrongAuthTracker> @Captor private lateinit var authControllerCallback: ArgumentCaptor<AuthController.Callback> @Captor private lateinit var biometricManagerCallback: @@ -112,12 +115,12 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, scope = testScope.backgroundScope, backgroundDispatcher = testDispatcher, - looper = testableLooper!!.looper, - dumpManager = dumpManager, biometricManager = biometricManager, devicePostureRepository = devicePostureRepository, + dumpManager = dumpManager, ) testScope.runCurrent() + verify(lockPatternUtils).registerStrongAuthTracker(strongAuthTracker.capture()) } @Test @@ -147,21 +150,18 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { val strongBiometricAllowed = collectLastValue(underTest.isStrongBiometricAllowed) runCurrent() - val captor = argumentCaptor<LockPatternUtils.StrongAuthTracker>() - verify(lockPatternUtils).registerStrongAuthTracker(captor.capture()) - - captor.value.stub.onStrongAuthRequiredChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) - testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) assertThat(strongBiometricAllowed()).isTrue() - captor.value.stub.onStrongAuthRequiredChanged( - STRONG_AUTH_REQUIRED_AFTER_BOOT, - PRIMARY_USER_ID - ) - testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_BOOT, PRIMARY_USER_ID) assertThat(strongBiometricAllowed()).isFalse() } + private fun onStrongAuthChanged(flags: Int, userId: Int) { + strongAuthTracker.value.stub.onStrongAuthRequiredChanged(flags, userId) + testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + } + @Test fun fingerprintDisabledByDpmChange() = testScope.runTest { @@ -351,6 +351,30 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { assertThat(isFaceAuthSupported()).isTrue() } + @Test + fun userInLockdownUsesStrongAuthFlagsToDetermineValue() = + testScope.runTest { + createBiometricSettingsRepository() + + val isUserInLockdown = collectLastValue(underTest.isCurrentUserInLockdown) + // has default value. + assertThat(isUserInLockdown()).isFalse() + + // change strong auth flags for another user. + // Combine with one more flag to check if we do the bitwise and + val inLockdown = + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN or STRONG_AUTH_REQUIRED_AFTER_TIMEOUT + onStrongAuthChanged(inLockdown, ANOTHER_USER_ID) + + // Still false. + assertThat(isUserInLockdown()).isFalse() + + // change strong auth flags for current user. + onStrongAuthChanged(inLockdown, PRIMARY_USER_ID) + + assertThat(isUserInLockdown()).isTrue() + } + private fun enrollmentChange(biometricType: BiometricType, userId: Int, enabled: Boolean) { authControllerCallback.value.onEnrollmentsChanged(biometricType, userId, enabled) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index 8d74c82da6b0..adba53823d2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.user.domain.interactor import android.app.ActivityManager import android.app.admin.DevicePolicyManager -import android.content.ComponentName import android.content.Intent import android.content.pm.UserInfo import android.graphics.Bitmap @@ -49,7 +48,6 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor -import com.android.systemui.user.UserSwitcherActivity import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.source.UserRecord @@ -58,11 +56,9 @@ import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel import com.android.systemui.user.utils.MultiUserActionsEvent import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertNotNull @@ -842,7 +838,7 @@ class UserInteractorTest : SysuiTestCase() { fun `show user switcher - full screen disabled - shows dialog switcher`() = testScope.runTest { val expandable = mock<Expandable>() - underTest.showUserSwitcher(context, expandable) + underTest.showUserSwitcher(expandable) val dialogRequest = collectLastValue(underTest.dialogShowRequests) @@ -855,30 +851,22 @@ class UserInteractorTest : SysuiTestCase() { } @Test - fun `show user switcher - full screen enabled - launches activity`() { - featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) - - val expandable = mock<Expandable>() - underTest.showUserSwitcher(context, expandable) - - // Dialog is shown. - val intentCaptor = argumentCaptor<Intent>() - verify(activityStarter) - .startActivity( - intentCaptor.capture(), - /* dismissShade= */ eq(true), - /* ActivityLaunchAnimator.Controller= */ nullable(), - /* showOverLockscreenWhenLocked= */ eq(true), - eq(UserHandle.SYSTEM), - ) - assertThat(intentCaptor.value.component) - .isEqualTo( - ComponentName( - context, - UserSwitcherActivity::class.java, - ) - ) - } + fun `show user switcher - full screen enabled - launches full screen dialog`() = + testScope.runTest { + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + + val expandable = mock<Expandable>() + underTest.showUserSwitcher(expandable) + + val dialogRequest = collectLastValue(underTest.dialogShowRequests) + + // Dialog is shown. + assertThat(dialogRequest()) + .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) + + underTest.onDialogShown() + assertThat(dialogRequest()).isNull() + } @Test fun `users - secondary user - managed profile is not included`() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index 7780a4372976..a342dadcb775 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -34,8 +34,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerReposito import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.power.data.repository.FakePowerRepository -import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository @@ -88,7 +86,6 @@ class UserSwitcherViewModelTest : SysuiTestCase() { private lateinit var userRepository: FakeUserRepository private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var powerRepository: FakePowerRepository private lateinit var testDispatcher: TestDispatcher private lateinit var testScope: TestScope @@ -116,7 +113,6 @@ class UserSwitcherViewModelTest : SysuiTestCase() { } keyguardRepository = FakeKeyguardRepository() - powerRepository = FakePowerRepository() val refreshUsersScheduler = RefreshUsersScheduler( applicationScope = testScope.backgroundScope, @@ -145,7 +141,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { set(Flags.FACE_AUTH_REFACTOR, true) } underTest = - UserSwitcherViewModel.Factory( + UserSwitcherViewModel( userInteractor = UserInteractor( applicationContext = context, @@ -174,13 +170,8 @@ class UserSwitcherViewModelTest : SysuiTestCase() { guestUserInteractor = guestUserInteractor, uiEventLogger = uiEventLogger, ), - powerInteractor = - PowerInteractor( - repository = powerRepository, - ), guestUserInteractor = guestUserInteractor, ) - .create(UserSwitcherViewModel::class.java) } @Test @@ -327,46 +318,12 @@ class UserSwitcherViewModelTest : SysuiTestCase() { } @Test - fun `isFinishRequested - finishes when user is switched`() = - testScope.runTest { - val userInfos = setUsers(count = 2) - val isFinishRequested = mutableListOf<Boolean>() - val job = - launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } - assertThat(isFinishRequested.last()).isFalse() - - userRepository.setSelectedUserInfo(userInfos[1]) - - assertThat(isFinishRequested.last()).isTrue() - - job.cancel() - } - - @Test - fun `isFinishRequested - finishes when the screen turns off`() = - testScope.runTest { - setUsers(count = 2) - powerRepository.setInteractive(true) - val isFinishRequested = mutableListOf<Boolean>() - val job = - launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } - assertThat(isFinishRequested.last()).isFalse() - - powerRepository.setInteractive(false) - - assertThat(isFinishRequested.last()).isTrue() - - job.cancel() - } - - @Test fun `isFinishRequested - finishes when cancel button is clicked`() = testScope.runTest { setUsers(count = 2) - powerRepository.setInteractive(true) val isFinishRequested = mutableListOf<Boolean>() val job = - launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } assertThat(isFinishRequested.last()).isFalse() underTest.onCancelButtonClicked() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt index d4b1701892c7..d8b3270d3aff 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt @@ -46,6 +46,10 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> get() = flowOf(true) + private val _isCurrentUserInLockdown = MutableStateFlow(false) + override val isCurrentUserInLockdown: Flow<Boolean> + get() = _isCurrentUserInLockdown + fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) { _isFingerprintEnrolled.value = isFingerprintEnrolled } diff --git a/services/autofill/java/com/android/server/autofill/SaveEventLogger.java b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java new file mode 100644 index 000000000000..4b7d5bdf0002 --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.autofill; + +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_DATASET_MATCH; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_FIELD_VALIDATION_FAILED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_HAS_EMPTY_REQUIRED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NONE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NO_SAVE_INFO; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NO_VALUE_CHANGED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_OPTIONAL_ID_CHANGE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_REQUIRED_ID_CHANGE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_TRIGGER_ID_SET; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_UNKNOWN; +import static com.android.server.autofill.Helper.sVerbose; + +import android.annotation.IntDef; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.internal.util.FrameworkStatsLog; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Optional; + +/** + * Helper class to log Autofill Save event stats. + */ +public final class SaveEventLogger { + private static final String TAG = "SaveEventLogger"; + + /** + * Reasons why presentation was not shown. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillSaveEventReported.SaveUiShownReason}. + */ + @IntDef(prefix = {"SAVE_UI_SHOWN_REASON"}, value = { + SAVE_UI_SHOWN_REASON_UNKNOWN, + SAVE_UI_SHOWN_REASON_REQUIRED_ID_CHANGE, + SAVE_UI_SHOWN_REASON_OPTIONAL_ID_CHANGE, + SAVE_UI_SHOWN_REASON_TRIGGER_ID_SET + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SaveUiShownReason { + } + + /** + * Reasons why presentation was not shown. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillSaveEventReported.SaveUiNotShownReason}. + */ + @IntDef(prefix = {"SAVE_UI_NOT_SHOWN_REASON"}, value = { + NO_SAVE_REASON_UNKNOWN, + NO_SAVE_REASON_NONE, + NO_SAVE_REASON_NO_SAVE_INFO, + NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG, + NO_SAVE_REASON_HAS_EMPTY_REQUIRED, + NO_SAVE_REASON_NO_VALUE_CHANGED, + NO_SAVE_REASON_FIELD_VALIDATION_FAILED, + NO_SAVE_REASON_DATASET_MATCH + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SaveUiNotShownReason { + } + + public static final int SAVE_UI_SHOWN_REASON_UNKNOWN = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_UNKNOWN; + public static final int SAVE_UI_SHOWN_REASON_REQUIRED_ID_CHANGE = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_REQUIRED_ID_CHANGE; + public static final int SAVE_UI_SHOWN_REASON_OPTIONAL_ID_CHANGE = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_OPTIONAL_ID_CHANGE; + public static final int SAVE_UI_SHOWN_REASON_TRIGGER_ID_SET = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_TRIGGER_ID_SET; + + public static final int NO_SAVE_REASON_UNKNOWN = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_UNKNOWN; + public static final int NO_SAVE_REASON_NONE = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NONE; + public static final int NO_SAVE_REASON_NO_SAVE_INFO = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NO_SAVE_INFO; + public static final int NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG; + public static final int NO_SAVE_REASON_HAS_EMPTY_REQUIRED = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_HAS_EMPTY_REQUIRED; + public static final int NO_SAVE_REASON_NO_VALUE_CHANGED = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NO_VALUE_CHANGED; + public static final int NO_SAVE_REASON_FIELD_VALIDATION_FAILED = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_FIELD_VALIDATION_FAILED; + public static final int NO_SAVE_REASON_DATASET_MATCH = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_DATASET_MATCH; + + private final int mSessionId; + private Optional<SaveEventInternal> mEventInternal; + + private SaveEventLogger(int sessionId) { + mSessionId = sessionId; + mEventInternal = Optional.of(new SaveEventInternal()); + } + + /** + * A factory constructor to create FillRequestEventLogger. + */ + public static SaveEventLogger forSessionId(int sessionId) { + return new SaveEventLogger(sessionId); + } + + /** + * Set request_id as long as mEventInternal presents. + */ + public void maybeSetRequestId(int requestId) { + mEventInternal.ifPresent(event -> event.mRequestId = requestId); + } + + /** + * Set app_package_uid as long as mEventInternal presents. + */ + public void maybeSetAppPackageUid(int val) { + mEventInternal.ifPresent(event -> { + event.mAppPackageUid = val; + }); + } + + /** + * Set save_ui_trigger_ids as long as mEventInternal presents. + */ + public void maybeSetSaveUiTriggerIds(int val) { + mEventInternal.ifPresent(event -> { + event.mSaveUiTriggerIds = val; + }); + } + + /** + * Set flag as long as mEventInternal presents. + */ + public void maybeSetFlag(int val) { + mEventInternal.ifPresent(event -> { + event.mFlag = val; + }); + } + + /** + * Set is_new_field as long as mEventInternal presents. + */ + public void maybeSetIsNewField(boolean val) { + mEventInternal.ifPresent(event -> { + event.mIsNewField = val; + }); + } + + /** + * Set save_ui_shown_reason as long as mEventInternal presents. + */ + public void maybeSetSaveUiShownReason(@SaveUiShownReason int reason) { + mEventInternal.ifPresent(event -> { + event.mSaveUiShownReason = reason; + }); + } + + /** + * Set save_ui_not_shown_reason as long as mEventInternal presents. + */ + public void maybeSetSaveUiNotShownReason(@SaveUiNotShownReason int reason) { + mEventInternal.ifPresent(event -> { + event.mSaveUiNotShownReason = reason; + }); + } + + /** + * Set save_button_clicked as long as mEventInternal presents. + */ + public void maybeSetSaveButtonClicked(boolean val) { + mEventInternal.ifPresent(event -> { + event.mSaveButtonClicked = val; + }); + } + + /** + * Set cancel_button_clicked as long as mEventInternal presents. + */ + public void maybeSetCancelButtonClicked(boolean val) { + mEventInternal.ifPresent(event -> { + event.mCancelButtonClicked = val; + }); + } + + /** + * Set dialog_dismissed as long as mEventInternal presents. + */ + public void maybeSetDialogDismissed(boolean val) { + mEventInternal.ifPresent(event -> { + event.mDialogDismissed = val; + }); + } + + /** + * Set is_saved as long as mEventInternal presents. + */ + public void maybeSetIsSaved(boolean val) { + mEventInternal.ifPresent(event -> { + event.mIsSaved = val; + }); + } + + /** + * Set latency_save_ui_display_millis as long as mEventInternal presents. + */ + public void maybeSetLatencySaveUiDisplayMillis(long timestamp) { + mEventInternal.ifPresent(event -> { + event.mLatencySaveUiDisplayMillis = timestamp; + }); + } + + /** + * Set latency_save_request_millis as long as mEventInternal presents. + */ + public void maybeSetLatencySaveRequestMillis(long timestamp) { + mEventInternal.ifPresent(event -> { + event.mLatencySaveRequestMillis = timestamp; + }); + } + + /** + * Set latency_save_finish_millis as long as mEventInternal presents. + */ + public void maybeSetLatencySaveFinishMillis(long timestamp) { + mEventInternal.ifPresent(event -> { + event.mLatencySaveFinishMillis = timestamp; + }); + } + + /** + * Log an AUTOFILL_SAVE_EVENT_REPORTED event. + */ + public void logAndEndEvent() { + if (!mEventInternal.isPresent()) { + Slog.w(TAG, "Shouldn't be logging AutofillSaveEventReported again for same " + + "event"); + return; + } + SaveEventInternal event = mEventInternal.get(); + if (sVerbose) { + Slog.v(TAG, "Log AutofillSaveEventReported:" + + " requestId=" + event.mRequestId + + " sessionId=" + mSessionId + + " mAppPackageUid=" + event.mAppPackageUid + + " mSaveUiTriggerIds=" + event.mSaveUiTriggerIds + + " mFlag=" + event.mFlag + + " mIsNewField=" + event.mIsNewField + + " mSaveUiShownReason=" + event.mSaveUiShownReason + + " mSaveUiNotShownReason=" + event.mSaveUiNotShownReason + + " mSaveButtonClicked=" + event.mSaveButtonClicked + + " mCancelButtonClicked=" + event.mCancelButtonClicked + + " mDialogDismissed=" + event.mDialogDismissed + + " mIsSaved=" + event.mIsSaved + + " mLatencySaveUiDisplayMillis=" + event.mLatencySaveUiDisplayMillis + + " mLatencySaveRequestMillis=" + event.mLatencySaveRequestMillis + + " mLatencySaveFinishMillis=" + event.mLatencySaveFinishMillis); + } + FrameworkStatsLog.write( + AUTOFILL_SAVE_EVENT_REPORTED, + event.mRequestId, + mSessionId, + event.mAppPackageUid, + event.mSaveUiTriggerIds, + event.mFlag, + event.mIsNewField, + event.mSaveUiShownReason, + event.mSaveUiNotShownReason, + event.mSaveButtonClicked, + event.mCancelButtonClicked, + event.mDialogDismissed, + event.mIsSaved, + event.mLatencySaveUiDisplayMillis, + event.mLatencySaveRequestMillis, + event.mLatencySaveFinishMillis); + mEventInternal = Optional.empty(); + } + + private static final class SaveEventInternal { + int mRequestId; + int mAppPackageUid = -1; + int mSaveUiTriggerIds = -1; + long mFlag = -1; + boolean mIsNewField = false; + int mSaveUiShownReason = SAVE_UI_SHOWN_REASON_UNKNOWN; + int mSaveUiNotShownReason = NO_SAVE_REASON_UNKNOWN; + boolean mSaveButtonClicked = false; + boolean mCancelButtonClicked = false; + boolean mDialogDismissed = false; + boolean mIsSaved = false; + long mLatencySaveUiDisplayMillis = 0; + long mLatencySaveRequestMillis = 0; + long mLatencySaveFinishMillis = 0; + + SaveEventInternal() { + } + } +} diff --git a/services/autofill/java/com/android/server/autofill/SessionCommittedEventLogger.java b/services/autofill/java/com/android/server/autofill/SessionCommittedEventLogger.java new file mode 100644 index 000000000000..92d72ac828f3 --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/SessionCommittedEventLogger.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.autofill; + +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_ACTIVITY_FINISHED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CHANGED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CLICKED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_COMMITTED; +import static com.android.server.autofill.Helper.sVerbose; + +import android.annotation.IntDef; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.internal.util.FrameworkStatsLog; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Optional; + +/** + * Helper class to log Autofill session committed event stats. + */ +public final class SessionCommittedEventLogger { + private static final String TAG = "SessionCommittedEventLogger"; + + /** + * Reasons why presentation was not shown. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillSessionCommitted.AutofillCommitReason}. + */ + @IntDef(prefix = {"COMMIT_REASON"}, value = { + COMMIT_REASON_UNKNOWN, + COMMIT_REASON_ACTIVITY_FINISHED, + COMMIT_REASON_VIEW_COMMITTED, + COMMIT_REASON_VIEW_CLICKED, + COMMIT_REASON_VIEW_CHANGED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CommitReason { + } + + public static final int COMMIT_REASON_UNKNOWN = + AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_UNKNOWN; + public static final int COMMIT_REASON_ACTIVITY_FINISHED = + AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_ACTIVITY_FINISHED; + public static final int COMMIT_REASON_VIEW_COMMITTED = + AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_COMMITTED; + public static final int COMMIT_REASON_VIEW_CLICKED = + AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CLICKED; + public static final int COMMIT_REASON_VIEW_CHANGED = + AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CHANGED; + + private final int mSessionId; + private Optional<SessionCommittedEventInternal> mEventInternal; + + private SessionCommittedEventLogger(int sessionId) { + mSessionId = sessionId; + mEventInternal = Optional.of(new SessionCommittedEventInternal()); + } + + /** + * A factory constructor to create SessionCommittedEventLogger. + */ + public static SessionCommittedEventLogger forSessionId(int sessionId) { + return new SessionCommittedEventLogger(sessionId); + } + + /** + * Set component_package_uid as long as mEventInternal presents. + */ + public void maybeSetComponentPackageUid(int val) { + mEventInternal.ifPresent(event -> { + event.mComponentPackageUid = val; + }); + } + + /** + * Set request_count as long as mEventInternal presents. + */ + public void maybeSetRequestCount(int val) { + mEventInternal.ifPresent(event -> { + event.mRequestCount = val; + }); + } + + /** + * Set commit_reason as long as mEventInternal presents. + */ + public void maybeSetCommitReason(@CommitReason int val) { + mEventInternal.ifPresent(event -> { + event.mCommitReason = val; + }); + } + + /** + * Set session_duration_millis as long as mEventInternal presents. + */ + public void maybeSetSessionDurationMillis(long timestamp) { + mEventInternal.ifPresent(event -> { + event.mSessionDurationMillis = timestamp; + }); + } + + /** + * Log an AUTOFILL_SESSION_COMMITTED event. + */ + public void logAndEndEvent() { + if (!mEventInternal.isPresent()) { + Slog.w(TAG, "Shouldn't be logging AutofillSessionCommitted again for same session."); + return; + } + SessionCommittedEventInternal event = mEventInternal.get(); + if (sVerbose) { + Slog.v(TAG, "Log AutofillSessionCommitted:" + + " sessionId=" + mSessionId + + " mComponentPackageUid=" + event.mComponentPackageUid + + " mRequestCount=" + event.mRequestCount + + " mCommitReason=" + event.mCommitReason + + " mSessionDurationMillis=" + event.mSessionDurationMillis); + } + FrameworkStatsLog.write( + AUTOFILL_SESSION_COMMITTED, + mSessionId, + event.mComponentPackageUid, + event.mRequestCount, + event.mCommitReason, + event.mSessionDurationMillis); + mEventInternal = Optional.empty(); + } + + private static final class SessionCommittedEventInternal { + int mComponentPackageUid = -1; + int mRequestCount = 0; + int mCommitReason = COMMIT_REASON_UNKNOWN; + long mSessionDurationMillis = 0; + + SessionCommittedEventInternal() { + } + } +} diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 6bd3c7953e01..e6ef3b468d2d 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -999,23 +999,50 @@ final class BroadcastRecord extends Binder { private static boolean matchesDeliveryGroup(@NonNull BroadcastRecord newRecord, @NonNull BroadcastRecord oldRecord) { - final String newMatchingKey = getDeliveryGroupMatchingKey(newRecord); - final String oldMatchingKey = getDeliveryGroupMatchingKey(oldRecord); final IntentFilter newMatchingFilter = getDeliveryGroupMatchingFilter(newRecord); // If neither delivery group key nor matching filter is specified, then use // Intent.filterEquals() to identify the delivery group. - if (newMatchingKey == null && oldMatchingKey == null && newMatchingFilter == null) { + if (isMatchingKeyNull(newRecord) && isMatchingKeyNull(oldRecord) + && newMatchingFilter == null) { return newRecord.intent.filterEquals(oldRecord.intent); } if (newMatchingFilter != null && !newMatchingFilter.asPredicate().test(oldRecord.intent)) { return false; } - return Objects.equals(newMatchingKey, oldMatchingKey); + return areMatchingKeysEqual(newRecord, oldRecord); + } + + private static boolean isMatchingKeyNull(@NonNull BroadcastRecord record) { + final String namespace = getDeliveryGroupMatchingNamespaceFragment(record); + final String key = getDeliveryGroupMatchingKeyFragment(record); + // If either namespace or key part is null, then treat the entire matching key as null. + return namespace == null || key == null; + } + + private static boolean areMatchingKeysEqual(@NonNull BroadcastRecord newRecord, + @NonNull BroadcastRecord oldRecord) { + final String newNamespaceFragment = getDeliveryGroupMatchingNamespaceFragment(newRecord); + final String oldNamespaceFragment = getDeliveryGroupMatchingNamespaceFragment(oldRecord); + if (!Objects.equals(newNamespaceFragment, oldNamespaceFragment)) { + return false; + } + + final String newKeyFragment = getDeliveryGroupMatchingKeyFragment(newRecord); + final String oldKeyFragment = getDeliveryGroupMatchingKeyFragment(oldRecord); + return Objects.equals(newKeyFragment, oldKeyFragment); + } + + @Nullable + private static String getDeliveryGroupMatchingNamespaceFragment( + @NonNull BroadcastRecord record) { + return record.options == null + ? null : record.options.getDeliveryGroupMatchingNamespaceFragment(); } @Nullable - private static String getDeliveryGroupMatchingKey(@NonNull BroadcastRecord record) { - return record.options == null ? null : record.options.getDeliveryGroupMatchingKey(); + private static String getDeliveryGroupMatchingKeyFragment(@NonNull BroadcastRecord record) { + return record.options == null + ? null : record.options.getDeliveryGroupMatchingKeyFragment(); } @Nullable diff --git a/services/core/java/com/android/server/locales/AppLocaleChangedAtomRecord.java b/services/core/java/com/android/server/locales/AppLocaleChangedAtomRecord.java index 2be2ef8c35af..7a70db22106e 100644 --- a/services/core/java/com/android/server/locales/AppLocaleChangedAtomRecord.java +++ b/services/core/java/com/android/server/locales/AppLocaleChangedAtomRecord.java @@ -20,24 +20,32 @@ import static android.os.Process.INVALID_UID; import com.android.internal.util.FrameworkStatsLog; +import java.util.Locale; + /** * Holds data used to report the ApplicationLocalesChanged atom. */ public final class AppLocaleChangedAtomRecord { + private static final String DEFAULT_PREFIX = "default-"; final int mCallingUid; int mTargetUid = INVALID_UID; - String mNewLocales = ""; - String mPrevLocales = ""; + String mNewLocales = DEFAULT_PREFIX; + String mPrevLocales = DEFAULT_PREFIX; int mStatus = FrameworkStatsLog .APPLICATION_LOCALES_CHANGED__STATUS__STATUS_UNSPECIFIED; int mCaller = FrameworkStatsLog .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_UNKNOWN; AppLocaleChangedAtomRecord(int callingUid) { this.mCallingUid = callingUid; + Locale defaultLocale = Locale.getDefault(); + if (defaultLocale != null) { + this.mNewLocales = DEFAULT_PREFIX + defaultLocale.toLanguageTag(); + this.mPrevLocales = DEFAULT_PREFIX + defaultLocale.toLanguageTag(); + } } void setNewLocales(String newLocales) { - this.mNewLocales = newLocales; + this.mNewLocales = convertEmptyLocales(newLocales); } void setTargetUid(int targetUid) { @@ -45,7 +53,7 @@ public final class AppLocaleChangedAtomRecord { } void setPrevLocales(String prevLocales) { - this.mPrevLocales = prevLocales; + this.mPrevLocales = convertEmptyLocales(prevLocales); } void setStatus(int status) { @@ -55,4 +63,16 @@ public final class AppLocaleChangedAtomRecord { void setCaller(int caller) { this.mCaller = caller; } + + private String convertEmptyLocales(String locales) { + String target = locales; + if ("".equals(locales)) { + Locale defaultLocale = Locale.getDefault(); + if (defaultLocale != null) { + target = DEFAULT_PREFIX + defaultLocale.toLanguageTag(); + } + } + + return target; + } } diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java index 6cd2ed41e94c..0049213cbf55 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java +++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java @@ -44,6 +44,7 @@ import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -377,7 +378,8 @@ class LocaleManagerBackupHelper { // Restore the locale immediately try { mLocaleManagerService.setApplicationLocales(pkgName, userId, - LocaleList.forLanguageTags(localesInfo.mLocales), localesInfo.mSetFromDelegate); + LocaleList.forLanguageTags(localesInfo.mLocales), localesInfo.mSetFromDelegate, + FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); if (DEBUG) { Slog.d(TAG, "Restored locales=" + localesInfo.mLocales + " fromDelegate=" + localesInfo.mSetFromDelegate + " for package=" + pkgName); @@ -662,7 +664,9 @@ class LocaleManagerBackupHelper { try { LocaleConfig localeConfig = new LocaleConfig( mContext.createPackageContextAsUser(packageName, 0, UserHandle.of(userId))); - mLocaleManagerService.removeUnsupportedAppLocales(packageName, userId, localeConfig); + mLocaleManagerService.removeUnsupportedAppLocales(packageName, userId, localeConfig, + FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APP_UPDATE_LOCALES_CHANGE); } catch (PackageManager.NameNotFoundException e) { Slog.e(TAG, "Can not found the package name : " + packageName + " / " + e); } diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java index e3a555bd2f6a..43e346a5bfa3 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerService.java +++ b/services/core/java/com/android/server/locales/LocaleManagerService.java @@ -182,8 +182,11 @@ public class LocaleManagerService extends SystemService { @Override public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales, boolean fromDelegate) throws RemoteException { + int caller = fromDelegate + ? FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DELEGATE + : FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS; LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales, - fromDelegate); + fromDelegate, caller); } @Override @@ -226,13 +229,14 @@ public class LocaleManagerService extends SystemService { * Sets the current UI locales for a specified app. */ public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId, - @NonNull LocaleList locales, boolean fromDelegate) + @NonNull LocaleList locales, boolean fromDelegate, int caller) throws RemoteException, IllegalArgumentException { AppLocaleChangedAtomRecord atomRecordForMetrics = new AppLocaleChangedAtomRecord(Binder.getCallingUid()); try { requireNonNull(appPackageName); requireNonNull(locales); + atomRecordForMetrics.setCaller(caller); atomRecordForMetrics.setNewLocales(locales.toLanguageTags()); //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user. userId = mActivityManagerInternal.handleIncomingUser( @@ -273,8 +277,8 @@ public class LocaleManagerService extends SystemService { + " and user " + userId); } - atomRecordForMetrics.setPrevLocales(getApplicationLocalesUnchecked(appPackageName, userId) - .toLanguageTags()); + atomRecordForMetrics.setPrevLocales( + getApplicationLocalesUnchecked(appPackageName, userId).toLanguageTags()); final ActivityTaskManagerInternal.PackageConfigurationUpdater updater = mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName, userId); @@ -619,7 +623,10 @@ public class LocaleManagerService extends SystemService { Slog.d(TAG, "remove the override LocaleConfig"); file.delete(); } - removeUnsupportedAppLocales(appPackageName, userId, resLocaleConfig); + removeUnsupportedAppLocales(appPackageName, userId, resLocaleConfig, + FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE + ); atomRecord.setOverrideRemoved(true); atomRecord.setStatus(FrameworkStatsLog .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS); @@ -661,7 +668,10 @@ public class LocaleManagerService extends SystemService { } atomicFile.finishWrite(stream); // Clear per-app locales if they are not in the override LocaleConfig. - removeUnsupportedAppLocales(appPackageName, userId, overrideLocaleConfig); + removeUnsupportedAppLocales(appPackageName, userId, overrideLocaleConfig, + FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE + ); if (overrideLocaleConfig.isSameLocaleConfig(resLocaleConfig)) { Slog.d(TAG, "setOverrideLocaleConfig, same as the app's LocaleConfig"); atomRecord.setSameAsResConfig(true); @@ -678,9 +688,12 @@ public class LocaleManagerService extends SystemService { /** * Checks if the per-app locales are in the LocaleConfig. Per-app locales missing from the * LocaleConfig will be removed. + * + * <p><b>Note:</b> Check whether to remove the per-app locales when the app is upgraded or + * the LocaleConfig is overridden. */ void removeUnsupportedAppLocales(String appPackageName, int userId, - LocaleConfig localeConfig) { + LocaleConfig localeConfig, int caller) { LocaleList appLocales = getApplicationLocalesUnchecked(appPackageName, userId); // Remove the per-app locales from the locale list if they don't exist in the LocaleConfig. boolean resetAppLocales = false; @@ -707,7 +720,7 @@ public class LocaleManagerService extends SystemService { try { setApplicationLocales(appPackageName, userId, new LocaleList(newAppLocales.toArray(locales)), - mBackupHelper.areLocalesSetFromDelegate(userId, appPackageName)); + mBackupHelper.areLocalesSetFromDelegate(userId, appPackageName), caller); } catch (RemoteException | IllegalArgumentException e) { Slog.e(TAG, "Could not set locales for " + appPackageName, e); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 2ea59b3fd6fd..6944b2918d84 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5266,10 +5266,20 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mTransitionController.collect(this); } else { inFinishingTransition = mTransitionController.inFinishingTransition(this); - if (!inFinishingTransition) { + if (!inFinishingTransition && !mDisplayContent.isSleeping()) { Slog.e(TAG, "setVisibility=" + visible + " while transition is not collecting or finishing " + this + " caller=" + Debug.getCallers(8)); + // Force showing the parents because they may be hidden by previous transition. + if (visible) { + final Transaction t = getSyncTransaction(); + for (WindowContainer<?> p = getParent(); p != null && p != mDisplayContent; + p = p.getParent()) { + if (p.mSurfaceControl != null) { + t.show(p.mSurfaceControl); + } + } + } } } } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index fb79c0677a2f..0d1f2ce8d63f 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -32,6 +32,7 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; import android.content.res.ResourceId; import android.graphics.Point; import android.graphics.Rect; @@ -75,7 +76,7 @@ class BackNavigationController { private Runnable mPendingAnimation; private final NavigationMonitor mNavigationMonitor = new NavigationMonitor(); - private AnimationHandler mAnimationHandler; + AnimationHandler mAnimationHandler; private final ArrayList<WindowContainer> mTmpOpenApps = new ArrayList<>(); private final ArrayList<WindowContainer> mTmpCloseApps = new ArrayList<>(); @@ -651,7 +652,8 @@ class BackNavigationController { /** * Create and handling animations status for an open/close animation targets. */ - private static class AnimationHandler { + static class AnimationHandler { + private final boolean mShowWindowlessSurface; private final WindowManagerService mWindowManagerService; private BackWindowAnimationAdaptor mCloseAdaptor; private BackWindowAnimationAdaptor mOpenAdaptor; @@ -670,19 +672,37 @@ class BackNavigationController { AnimationHandler(WindowManagerService wms) { mWindowManagerService = wms; + final Context context = wms.mContext; + mShowWindowlessSurface = context.getResources().getBoolean( + com.android.internal.R.bool.config_predictShowStartingSurface); } private static final int UNKNOWN = 0; private static final int TASK_SWITCH = 1; private static final int ACTIVITY_SWITCH = 2; + private static boolean isActivitySwitch(WindowContainer close, WindowContainer open) { + if (close.asActivityRecord() == null || open.asActivityRecord() == null + || (close.asActivityRecord().getTask() + != open.asActivityRecord().getTask())) { + return false; + } + return true; + } + + private static boolean isTaskSwitch(WindowContainer close, WindowContainer open) { + if (close.asTask() == null || open.asTask() == null + || (close.asTask() == open.asTask())) { + return false; + } + return true; + } + private void initiate(WindowContainer close, WindowContainer open) { WindowContainer closeTarget; - if (close.asActivityRecord() != null && open.asActivityRecord() != null - && (close.asActivityRecord().getTask() == open.asActivityRecord().getTask())) { + if (isActivitySwitch(close, open)) { mSwitchType = ACTIVITY_SWITCH; closeTarget = close.asActivityRecord(); - } else if (close.asTask() != null && open.asTask() != null - && close.asTask() != open.asTask()) { + } else if (isTaskSwitch(close, open)) { mSwitchType = TASK_SWITCH; closeTarget = close.asTask().getTopNonFinishingActivity(); } else { @@ -699,7 +719,8 @@ class BackNavigationController { } } - boolean composeAnimations(@NonNull WindowContainer close, @NonNull WindowContainer open) { + private boolean composeAnimations(@NonNull WindowContainer close, + @NonNull WindowContainer open) { clearBackAnimateTarget(null /* cleanupTransaction */); if (close == null || open == null) { Slog.e(TAG, "reset animation with null target close: " @@ -757,15 +778,15 @@ class BackNavigationController { } boolean isTarget(WindowContainer wc, boolean open) { - if (open) { - return wc == mOpenAdaptor.mTarget || mOpenAdaptor.mTarget.hasChild(wc); + if (!mComposed) { + return false; } - + final WindowContainer target = open ? mOpenAdaptor.mTarget : mCloseAdaptor.mTarget; if (mSwitchType == TASK_SWITCH) { - return wc == mCloseAdaptor.mTarget - || (wc.asTask() != null && wc.hasChild(mCloseAdaptor.mTarget)); + return wc == target + || (wc.asTask() != null && wc.hasChild(target)); } else if (mSwitchType == ACTIVITY_SWITCH) { - return wc == mCloseAdaptor.mTarget; + return wc == target || (wc.asTaskFragment() != null && wc.hasChild(target)); } return false; } @@ -983,21 +1004,20 @@ class BackNavigationController { case BackNavigationInfo.TYPE_CROSS_ACTIVITY: return new ScheduleAnimationBuilder(backType, adapter) .setComposeTarget(currentActivity, previousActivity) - .setOpeningSnapshot(getActivitySnapshot(previousActivity)); + .setIsLaunchBehind(false); case BackNavigationInfo.TYPE_CROSS_TASK: return new ScheduleAnimationBuilder(backType, adapter) .setComposeTarget(currentTask, previousTask) - .setOpeningSnapshot(getTaskSnapshot(previousTask)); + .setIsLaunchBehind(false); } return null; } - private class ScheduleAnimationBuilder { + class ScheduleAnimationBuilder { final int mType; final BackAnimationAdapter mBackAnimationAdapter; WindowContainer mCloseTarget; WindowContainer mOpenTarget; - TaskSnapshot mOpenSnapshot; boolean mIsLaunchBehind; ScheduleAnimationBuilder(int type, BackAnimationAdapter backAnimationAdapter) { @@ -1011,11 +1031,6 @@ class BackNavigationController { return this; } - ScheduleAnimationBuilder setOpeningSnapshot(TaskSnapshot snapshot) { - mOpenSnapshot = snapshot; - return this; - } - ScheduleAnimationBuilder setIsLaunchBehind(boolean launchBehind) { mIsLaunchBehind = launchBehind; return this; @@ -1026,17 +1041,32 @@ class BackNavigationController { || wc.hasChild(mOpenTarget) || wc.hasChild(mCloseTarget); } + /** + * Apply preview strategy on the opening target + * @param open The opening target. + * @param visibleOpenActivity The visible activity in opening target. + * @return If the preview strategy is launch behind, returns the Activity that has + * launchBehind set, or null otherwise. + */ + private ActivityRecord applyPreviewStrategy(WindowContainer open, + ActivityRecord visibleOpenActivity) { + if (isSupportWindowlessSurface() && mShowWindowlessSurface && !mIsLaunchBehind) { + createStartingSurface(getSnapshot(open)); + return null; + } + setLaunchBehind(visibleOpenActivity); + return visibleOpenActivity; + } + Runnable build() { if (mOpenTarget == null || mCloseTarget == null) { return null; } - final boolean shouldLaunchBehind = mIsLaunchBehind || !isSupportWindowlessSurface(); - final ActivityRecord launchBehindActivity = !shouldLaunchBehind ? null - : mOpenTarget.asTask() != null + final ActivityRecord openActivity = mOpenTarget.asTask() != null ? mOpenTarget.asTask().getTopNonFinishingActivity() : mOpenTarget.asActivityRecord() != null ? mOpenTarget.asActivityRecord() : null; - if (shouldLaunchBehind && launchBehindActivity == null) { + if (openActivity == null) { Slog.e(TAG, "No opening activity"); return null; } @@ -1044,11 +1074,8 @@ class BackNavigationController { if (!composeAnimations(mCloseTarget, mOpenTarget)) { return null; } - if (launchBehindActivity != null) { - setLaunchBehind(launchBehindActivity); - } else { - createStartingSurface(mOpenSnapshot); - } + final ActivityRecord launchBehindActivity = + applyPreviewStrategy(mOpenTarget, openActivity); final IBackAnimationFinishedCallback callback = makeAnimationFinishedCallback( launchBehindActivity != null ? triggerBack -> { @@ -1171,25 +1198,22 @@ class BackNavigationController { mPendingAnimationBuilder = null; } - private static TaskSnapshot getActivitySnapshot(@NonNull ActivityRecord r) { + static TaskSnapshot getSnapshot(@NonNull WindowContainer w) { if (!isScreenshotEnabled()) { return null; } - // Check if we have a screenshot of the previous activity, indexed by its - // component name. - // TODO return TaskSnapshot when feature complete. -// final HardwareBuffer hw = r.getTask().getSnapshotForActivityRecord(r); - return null; - } + if (w.asTask() != null) { + final Task task = w.asTask(); + return task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot( + task.mTaskId, task.mUserId, false /* restoreFromDisk */, + false /* isLowResolution */); + } - private static TaskSnapshot getTaskSnapshot(Task task) { - if (!isScreenshotEnabled()) { + if (w.asActivityRecord() != null) { + // TODO (b/259497289) return TaskSnapshot when feature complete. return null; } - // Don't read from disk!! - return task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot( - task.mTaskId, task.mUserId, false /* restoreFromDisk */, - false /* isLowResolution */); + return null; } void setWindowManager(WindowManagerService wm) { diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 2f5634362e68..07daa4b22ac9 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2349,12 +2349,23 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } // Prepare transition before resume top activity, so it can be collected. - if (!displayShouldSleep && display.isDefaultDisplay - && !display.getDisplayPolicy().isAwake() - && display.mTransitionController.isShellTransitionsEnabled() + if (!displayShouldSleep && display.mTransitionController.isShellTransitionsEnabled() && !display.mTransitionController.isCollecting()) { - display.mTransitionController.requestTransitionIfNeeded(TRANSIT_WAKE, - 0 /* flags */, null /* trigger */, display); + int transit = TRANSIT_NONE; + if (!display.getDisplayPolicy().isAwake()) { + // Note that currently this only happens on default display because non-default + // display is always awake. + transit = TRANSIT_WAKE; + } else if (display.isKeyguardOccluded()) { + // The display was awake so this is resuming activity for occluding keyguard. + transit = WindowManager.TRANSIT_KEYGUARD_OCCLUDE; + } + if (transit != TRANSIT_NONE) { + display.mTransitionController.requestStartTransition( + display.mTransitionController.createTransition(transit), + null /* startTask */, null /* remoteTransition */, + null /* displayChange */); + } } // Set the sleeping state of the root tasks on the display. display.forAllRootTasks(rootTask -> { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 2cfd2af89c80..8fa7592e6da0 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2363,6 +2363,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (isTranslucent(wc)) { flags |= FLAG_TRANSLUCENT; } + if (wc.mWmService.mAtmService.mBackNavigationController.isMonitorTransitionTarget(wc)) { + flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; + } final Task task = wc.asTask(); if (task != null) { final ActivityRecord topActivity = task.getTopNonFinishingActivity(); @@ -2371,19 +2374,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { && topActivity.mStartingData.hasImeSurface()) { flags |= FLAG_WILL_IME_SHOWN; } - if (topActivity.mAtmService.mBackNavigationController - .isMonitorTransitionTarget(topActivity)) { - flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; - } - if (topActivity != null && topActivity.mLaunchTaskBehind) { + if (topActivity.mLaunchTaskBehind) { Slog.e(TAG, "Unexpected launch-task-behind operation in shell transition"); flags |= FLAG_TASK_LAUNCHING_BEHIND; } - } else { - if (task.mAtmService.mBackNavigationController - .isMonitorTransitionTarget(task)) { - flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; - } } if (task.voiceSession != null) { flags |= FLAG_IS_VOICE_INTERACTION; @@ -2397,10 +2391,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { flags |= FLAG_IS_VOICE_INTERACTION; } flags |= record.mTransitionChangeFlags; - if (record.mAtmService.mBackNavigationController - .isMonitorTransitionTarget(record)) { - flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; - } } final TaskFragment taskFragment = wc.asTaskFragment(); if (taskFragment != null && task == null) { diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SdCardEjectionTests.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SdCardEjectionTests.kt index 9f9e6a310d0b..d8139623f393 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SdCardEjectionTests.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SdCardEjectionTests.kt @@ -57,7 +57,8 @@ class SdCardEjectionTests : BaseHostJUnit4Test() { @Parameterized.Parameters(name = "reboot={0}") @JvmStatic - fun parameters() = arrayOf(false, true) + // TODO(b/275403538): re-enable non-reboot scenarios with better tracking of APK removal + fun parameters() = arrayOf(/*false, */true) data class Volume( val diskId: String, @@ -200,15 +201,6 @@ class SdCardEjectionTests : BaseHostJUnit4Test() { // TODO: There must be a better way to prevent it from auto-mounting. removeVirtualDisk() device.reboot() - } else { - // Because PackageManager unmount scan is asynchronous, need to retry until the package - // has been unloaded. This only has to be done in the non-reboot case. Reboot will - // clear the data structure by its nature. - retryUntilSuccess { - // The compiler section will print the state of the physical APK - HostUtils.packageSection(device, pkgName, sectionName = "Compiler stats") - .any { it.contains("Unable to find package: $pkgName") } - } } } diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Generic/Android.bp b/services/tests/PackageManagerServiceTests/host/test-apps/Generic/Android.bp index 1cc7ccc01283..5cc3371a1a6e 100644 --- a/services/tests/PackageManagerServiceTests/host/test-apps/Generic/Android.bp +++ b/services/tests/PackageManagerServiceTests/host/test-apps/Generic/Android.bp @@ -24,30 +24,45 @@ package { android_test_helper_app { name: "PackageManagerTestAppStub", manifest: "AndroidManifestVersion1.xml", - srcs: [] + srcs: [], } android_test_helper_app { name: "PackageManagerTestAppVersion1", - manifest: "AndroidManifestVersion1.xml" + manifest: "AndroidManifestVersion1.xml", + srcs: [ + "src/**/*.kt", + ], } android_test_helper_app { name: "PackageManagerTestAppVersion2", - manifest: "AndroidManifestVersion2.xml" + manifest: "AndroidManifestVersion2.xml", + srcs: [ + "src/**/*.kt", + ], } android_test_helper_app { name: "PackageManagerTestAppVersion3", - manifest: "AndroidManifestVersion3.xml" + manifest: "AndroidManifestVersion3.xml", + srcs: [ + "src/**/*.kt", + ], } android_test_helper_app { name: "PackageManagerTestAppVersion4", - manifest: "AndroidManifestVersion4.xml" + manifest: "AndroidManifestVersion4.xml", + srcs: [ + "src/**/*.kt", + ], } android_test_helper_app { name: "PackageManagerTestAppOriginalOverride", - manifest: "AndroidManifestOriginalOverride.xml" + manifest: "AndroidManifestOriginalOverride.xml", + srcs: [ + "src/**/*.kt", + ], } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 390119c968cd..36d191b466ba 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -1008,6 +1008,42 @@ public final class BroadcastQueueModernImplTest { dropboxEntryBroadcast2.first, expectedMergedBroadcast.first)); } + @Test + public void testDeliveryGroupPolicy_sameAction_differentMatchingCriteria() { + final Intent closeSystemDialogs1 = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + final BroadcastOptions optionsCloseSystemDialog1 = BroadcastOptions.makeBasic() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + + final Intent closeSystemDialogs2 = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) + .putExtra("reason", "testing"); + final BroadcastOptions optionsCloseSystemDialog2 = BroadcastOptions.makeBasic() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .setDeliveryGroupMatchingKey(Intent.ACTION_CLOSE_SYSTEM_DIALOGS, "testing"); + + // Halt all processing so that we get a consistent view + mHandlerThread.getLooper().getQueue().postSyncBarrier(); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs1, optionsCloseSystemDialog1)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs2, optionsCloseSystemDialog2)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs1, optionsCloseSystemDialog1)); + // Verify that only the older broadcast with no extras was removed. + final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + verifyPendingRecords(queue, List.of(closeSystemDialogs2, closeSystemDialogs1)); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs2, optionsCloseSystemDialog2)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs1, optionsCloseSystemDialog1)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs2, optionsCloseSystemDialog2)); + // Verify that only the older broadcast with no extras was removed. + verifyPendingRecords(queue, List.of(closeSystemDialogs1, closeSystemDialogs2)); + } + private Pair<Intent, BroadcastOptions> createDropboxBroadcast(String tag, long timestampMs, int droppedCount) { final Intent dropboxEntryAdded = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED); diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java index 13371cce5fb5..40ecaf1770a9 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java @@ -54,6 +54,7 @@ import android.util.Xml; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.content.PackageMonitor; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -264,7 +265,8 @@ public class LocaleManagerBackupRestoreTest { // Locales were restored verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME, - DEFAULT_USER_ID, DEFAULT_LOCALES, false); + DEFAULT_USER_ID, DEFAULT_LOCALES, false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); checkStageDataDoesNotExist(DEFAULT_USER_ID); } @@ -280,7 +282,8 @@ public class LocaleManagerBackupRestoreTest { // Locales were restored verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME, - DEFAULT_USER_ID, DEFAULT_LOCALES, false); + DEFAULT_USER_ID, DEFAULT_LOCALES, false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); checkStageDataDoesNotExist(DEFAULT_USER_ID); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, false, @@ -303,7 +306,8 @@ public class LocaleManagerBackupRestoreTest { // Locales were restored verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME, - DEFAULT_USER_ID, DEFAULT_LOCALES, true); + DEFAULT_USER_ID, DEFAULT_LOCALES, true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); checkStageDataDoesNotExist(DEFAULT_USER_ID); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, true, @@ -327,7 +331,8 @@ public class LocaleManagerBackupRestoreTest { // Locales were restored verify(mMockLocaleManagerService, times(1)).setApplicationLocales( - DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, DEFAULT_LOCALES, true); + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, DEFAULT_LOCALES, true, + FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); checkStageDataDoesNotExist(DEFAULT_USER_ID); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, true, @@ -369,7 +374,8 @@ public class LocaleManagerBackupRestoreTest { mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID); verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID, - LocaleList.forLanguageTags(langTagsA), true); + LocaleList.forLanguageTags(langTagsA), true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); pkgLocalesMap.remove(pkgNameA); @@ -422,11 +428,12 @@ public class LocaleManagerBackupRestoreTest { // Restore locales only for myAppB. verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameA), anyInt(), - any(), anyBoolean()); + any(), anyBoolean(), anyInt()); verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID, - LocaleList.forLanguageTags(langTagsB), true); + LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameC), anyInt(), - any(), anyBoolean()); + any(), anyBoolean(), anyInt()); // App C is staged. pkgLocalesMap.remove(pkgNameA); @@ -484,7 +491,8 @@ public class LocaleManagerBackupRestoreTest { mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID); verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID, - LocaleList.forLanguageTags(langTagsA), false); + LocaleList.forLanguageTags(langTagsA), false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameA, false, false); @@ -499,7 +507,8 @@ public class LocaleManagerBackupRestoreTest { mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID); verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID, - LocaleList.forLanguageTags(langTagsB), true); + LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameB, true, false); @@ -606,7 +615,8 @@ public class LocaleManagerBackupRestoreTest { mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID); verify(mMockLocaleManagerService, times(1)).setApplicationLocales( - pkgNameA, DEFAULT_USER_ID, LocaleList.forLanguageTags(langTagsA), false); + pkgNameA, DEFAULT_USER_ID, LocaleList.forLanguageTags(langTagsA), false, + FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); pkgLocalesMap.remove(pkgNameA); @@ -620,7 +630,7 @@ public class LocaleManagerBackupRestoreTest { mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID); verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameB), anyInt(), - any(), anyBoolean()); + any(), anyBoolean(), anyInt()); checkStageDataDoesNotExist(DEFAULT_USER_ID); } @@ -734,7 +744,7 @@ public class LocaleManagerBackupRestoreTest { */ private void verifyNothingRestored() throws Exception { verify(mMockLocaleManagerService, times(0)).setApplicationLocales(anyString(), anyInt(), - any(), anyBoolean()); + any(), anyBoolean(), anyInt()); } private static void verifyPayloadForAppLocales(Map<String, LocalesInfo> expectedPkgLocalesMap, diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java index 07fda309f03e..550204b99323 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java @@ -52,6 +52,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.content.PackageMonitor; +import com.android.internal.util.FrameworkStatsLog; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal.PackageConfig; @@ -136,7 +137,8 @@ public class LocaleManagerServiceTest { try { mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, - LocaleList.getEmptyLocaleList(), false); + LocaleList.getEmptyLocaleList(), false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS); fail("Expected SecurityException"); } finally { verify(mMockContext).enforceCallingOrSelfPermission( @@ -151,7 +153,8 @@ public class LocaleManagerServiceTest { public void testSetApplicationLocales_nullPackageName_fails() throws Exception { try { mLocaleManagerService.setApplicationLocales(/* appPackageName = */ null, - DEFAULT_USER_ID, LocaleList.getEmptyLocaleList(), false); + DEFAULT_USER_ID, LocaleList.getEmptyLocaleList(), false, + FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS); fail("Expected NullPointerException"); } finally { verify(mMockBackupHelper, times(0)).notifyBackupManager(); @@ -165,7 +168,8 @@ public class LocaleManagerServiceTest { try { mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, - /* locales = */ null, false); + /* locales = */ null, false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS); fail("Expected NullPointerException"); } finally { verify(mMockBackupHelper, times(0)).notifyBackupManager(); @@ -183,7 +187,8 @@ public class LocaleManagerServiceTest { setUpPassingPermissionCheckFor(Manifest.permission.CHANGE_CONFIGURATION); mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, - DEFAULT_LOCALES, true); + DEFAULT_LOCALES, true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DELEGATE); assertEquals(DEFAULT_LOCALES, mFakePackageConfigurationUpdater.getStoredLocales()); verify(mMockBackupHelper, times(1)).notifyBackupManager(); @@ -196,7 +201,8 @@ public class LocaleManagerServiceTest { .when(mMockPackageManager).getPackageUidAsUser(anyString(), any(), anyInt()); mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, - DEFAULT_LOCALES, false); + DEFAULT_LOCALES, false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS); assertEquals(DEFAULT_LOCALES, mFakePackageConfigurationUpdater.getStoredLocales()); verify(mMockBackupHelper, times(1)).notifyBackupManager(); @@ -208,7 +214,8 @@ public class LocaleManagerServiceTest { .when(mMockPackageManager).getPackageUidAsUser(anyString(), any(), anyInt()); try { mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, - LocaleList.getEmptyLocaleList(), false); + LocaleList.getEmptyLocaleList(), false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS); fail("Expected IllegalArgumentException"); } finally { assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales()); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index b80c3e84198b..d0628f12336c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -26,25 +26,31 @@ import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.window.BackNavigationInfo.typeToString; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityOptions; import android.content.Context; +import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; +import android.content.res.Resources; import android.os.Bundle; import android.os.RemoteCallback; import android.os.RemoteException; @@ -58,6 +64,7 @@ import android.window.IOnBackInvokedCallback; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedCallbackInfo; import android.window.OnBackInvokedDispatcher; +import android.window.TaskSnapshot; import android.window.WindowOnBackInvokedDispatcher; import com.android.server.LocalServices; @@ -66,6 +73,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -408,6 +417,25 @@ public class BackNavigationControllerTests extends WindowTestsBase { 0, navigationObserver.getCount()); } + + /** + * Test with + * config_predictShowStartingSurface = true + */ + @Test + public void testEnableWindowlessSurface() { + testPrepareAnimation(true); + } + + /** + * Test with + * config_predictShowStartingSurface = false + */ + @Test + public void testDisableWindowlessSurface() { + testPrepareAnimation(false); + } + private IOnBackInvokedCallback withSystemCallback(Task task) { IOnBackInvokedCallback callback = createOnBackInvokedCallback(); task.getTopMostActivity().getTopChild().setOnBackInvokedCallbackInfo( @@ -492,6 +520,55 @@ public class BackNavigationControllerTests extends WindowTestsBase { doReturn(true).when(kc).isDisplayOccluded(anyInt()); } + private void testPrepareAnimation(boolean preferWindowlessSurface) { + final TaskSnapshot taskSnapshot = mock(TaskSnapshot.class); + final ContextWrapper contextSpy = Mockito.spy(new ContextWrapper(mWm.mContext)); + final Resources resourcesSpy = Mockito.spy(contextSpy.getResources()); + + when(contextSpy.getResources()).thenReturn(resourcesSpy); + + MockitoSession mockitoSession = mockitoSession().mockStatic(BackNavigationController.class) + .strictness(Strictness.LENIENT).startMocking(); + doReturn(taskSnapshot).when(() -> BackNavigationController.getSnapshot(any())); + when(resourcesSpy.getBoolean( + com.android.internal.R.bool.config_predictShowStartingSurface)) + .thenReturn(preferWindowlessSurface); + + final BackNavigationController.AnimationHandler animationHandler = + Mockito.spy(new BackNavigationController.AnimationHandler(mWm)); + doReturn(true).when(animationHandler).isSupportWindowlessSurface(); + testWithConfig(animationHandler, preferWindowlessSurface); + mockitoSession.finishMocking(); + } + + private void testWithConfig(BackNavigationController.AnimationHandler animationHandler, + boolean preferWindowlessSurface) { + final Task task = createTask(mDefaultDisplay); + final ActivityRecord bottomActivity = createActivityRecord(task); + final ActivityRecord homeActivity = mRootHomeTask.getTopNonFinishingActivity(); + + final BackNavigationController.AnimationHandler.ScheduleAnimationBuilder toHomeBuilder = + animationHandler.prepareAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME, + mBackAnimationAdapter, task, mRootHomeTask, bottomActivity, homeActivity); + assertTrue(toHomeBuilder.mIsLaunchBehind); + toHomeBuilder.build(); + verify(animationHandler, never()).createStartingSurface(any()); + + // Back to ACTIVITY and TASK have the same logic, just with different target. + final ActivityRecord topActivity = createActivityRecord(task); + final BackNavigationController.AnimationHandler.ScheduleAnimationBuilder toActivityBuilder = + animationHandler.prepareAnimation( + BackNavigationInfo.TYPE_CROSS_ACTIVITY, mBackAnimationAdapter, task, task, + topActivity, bottomActivity); + assertFalse(toActivityBuilder.mIsLaunchBehind); + toActivityBuilder.build(); + if (preferWindowlessSurface) { + verify(animationHandler).createStartingSurface(any()); + } else { + verify(animationHandler, never()).createStartingSurface(any()); + } + } + @NonNull private Task createTopTaskWithActivity() { Task task = createTask(mDefaultDisplay); diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java index 858cd7672fe3..a8f1b3de564e 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java @@ -26,6 +26,8 @@ import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; +import androidx.annotation.Nullable; + /** * Injects gestures given an {@link Instrumentation} object. */ @@ -36,6 +38,13 @@ public class GestureHelper { private final UiAutomation mUiAutomation; /** + * Primary pointer should be cached here for separate release + */ + @Nullable private PointerProperties mPrimaryPtrProp; + @Nullable private PointerCoords mPrimaryPtrCoord; + private long mPrimaryPtrDownTime; + + /** * A pair of floating point values. */ public static class Tuple { @@ -53,6 +62,52 @@ public class GestureHelper { } /** + * Injects a series of {@link MotionEvent}s to simulate a drag gesture without pointer release. + * + * Simulates a drag gesture without releasing the primary pointer. The primary pointer info + * will be cached for potential release later on by {@code releasePrimaryPointer()} + * + * @param startPoint initial coordinates of the primary pointer + * @param endPoint final coordinates of the primary pointer + * @param steps number of steps to take to animate dragging + * @return true if gesture is injected successfully + */ + public boolean dragWithoutRelease(@NonNull Tuple startPoint, + @NonNull Tuple endPoint, int steps) { + PointerProperties ptrProp = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER); + PointerCoords ptrCoord = getPointerCoord(startPoint.x, startPoint.y, 1, 1); + + PointerProperties[] ptrProps = new PointerProperties[] { ptrProp }; + PointerCoords[] ptrCoords = new PointerCoords[] { ptrCoord }; + + long downTime = SystemClock.uptimeMillis(); + + if (!primaryPointerDown(ptrProp, ptrCoord, downTime)) { + return false; + } + + // cache the primary pointer info for later potential release + mPrimaryPtrProp = ptrProp; + mPrimaryPtrCoord = ptrCoord; + mPrimaryPtrDownTime = downTime; + + return movePointers(ptrProps, ptrCoords, new Tuple[] { endPoint }, downTime, steps); + } + + /** + * Release primary pointer if previous gesture has cached the primary pointer info. + * + * @return true if the release was injected successfully + */ + public boolean releasePrimaryPointer() { + if (mPrimaryPtrProp != null && mPrimaryPtrCoord != null) { + return primaryPointerUp(mPrimaryPtrProp, mPrimaryPtrCoord, mPrimaryPtrDownTime); + } + + return false; + } + + /** * Injects a series of {@link MotionEvent} objects to simulate a pinch gesture. * * @param startPoint1 initial coordinates of the first pointer diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt index de57d06a5619..e497ae4779a7 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt @@ -58,6 +58,43 @@ open class PipAppHelper(instrumentation: Instrumentation) : } /** + * Drags the PIP window to the provided final coordinates without releasing the pointer. + */ + fun dragPipWindowAwayFromEdgeWithoutRelease( + wmHelper: WindowManagerStateHelper, + steps: Int + ) { + val initWindowRect = getWindowRect(wmHelper).clone() + + // initial pointer at the center of the window + val initialCoord = GestureHelper.Tuple(initWindowRect.centerX().toFloat(), + initWindowRect.centerY().toFloat()) + + // the offset to the right (or left) of the window center to drag the window to + val offset = 50 + + // the actual final x coordinate with the offset included; + // if the pip window is closer to the right edge of the display the offset is negative + // otherwise the offset is positive + val endX = initWindowRect.centerX() + + offset * (if (isCloserToRightEdge(wmHelper)) -1 else 1) + val finalCoord = GestureHelper.Tuple(endX.toFloat(), initWindowRect.centerY().toFloat()) + + // drag to the final coordinate + gestureHelper.dragWithoutRelease(initialCoord, finalCoord, steps) + } + + /** + * Releases the primary pointer. + * + * Injects the release of the primary pointer if the primary pointer info was cached after + * another gesture was injected without pointer release. + */ + fun releasePipAfterDragging() { + gestureHelper.releasePrimaryPointer() + } + + /** * Drags the PIP window away from the screen edge while not crossing the display center. * * @throws IllegalStateException if default display bounds are not available @@ -72,10 +109,10 @@ open class PipAppHelper(instrumentation: Instrumentation) : val displayRect = wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect ?: throw IllegalStateException("Default display is null") - // the offset to the right of the display center to drag the window to + // the offset to the right (or left) of the display center to drag the window to val offset = 20 - // the actual final x coordinate with the offset included + // the actual final x coordinate with the offset included; // if the pip window is closer to the right edge of the display the offset is positive // otherwise the offset is negative val endX = displayRect.centerX() + offset * (if (isCloserToRightEdge(wmHelper)) 1 else -1) |