Merge "speech: API council feedback from model download with a listener" into udc-dev
diff --git a/core/api/current.txt b/core/api/current.txt
index 73a26f6..c943136 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9678,6 +9678,7 @@
method @Nullable public String getAttributionTag();
method @Nullable public android.content.AttributionSource getNext();
method @Nullable public String getPackageName();
+ method public int getPid();
method public int getUid();
method public boolean isTrusted(@NonNull android.content.Context);
method @NonNull public static android.content.AttributionSource myAttributionSource();
@@ -9692,6 +9693,7 @@
method @NonNull public android.content.AttributionSource.Builder setAttributionTag(@Nullable String);
method @NonNull public android.content.AttributionSource.Builder setNext(@Nullable android.content.AttributionSource);
method @NonNull public android.content.AttributionSource.Builder setPackageName(@Nullable String);
+ method @NonNull public android.content.AttributionSource.Builder setPid(int);
}
public abstract class BroadcastReceiver {
@@ -33610,6 +33612,7 @@
method @Deprecated public static final boolean supportsProcesses();
field public static final int BLUETOOTH_UID = 1002; // 0x3ea
field public static final int FIRST_APPLICATION_UID = 10000; // 0x2710
+ field public static final int INVALID_PID = -1; // 0xffffffff
field public static final int INVALID_UID = -1; // 0xffffffff
field public static final int LAST_APPLICATION_UID = 19999; // 0x4e1f
field public static final int PHONE_UID = 1001; // 0x3e9
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 574ed6f..0123cc9 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3986,6 +3986,7 @@
public static class WindowInfosListenerForTest.WindowInfo {
field @NonNull public final android.graphics.Rect bounds;
+ field public final boolean isTrustedOverlay;
field @NonNull public final String name;
field @NonNull public final android.os.IBinder windowToken;
}
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 181bd35..265b564 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -8408,9 +8408,9 @@
public int noteProxyOp(int op, @Nullable String proxiedPackageName, int proxiedUid,
@Nullable String proxiedAttributionTag, @Nullable String message) {
return noteProxyOp(op, new AttributionSource(mContext.getAttributionSource(),
- new AttributionSource(proxiedUid, proxiedPackageName, proxiedAttributionTag,
- mContext.getAttributionSource().getToken())), message,
- /*skipProxyOperation*/ false);
+ new AttributionSource(proxiedUid, Process.INVALID_PID, proxiedPackageName,
+ proxiedAttributionTag, mContext.getAttributionSource().getToken())),
+ message, /*skipProxyOperation*/ false);
}
/**
@@ -8495,8 +8495,9 @@
int proxiedUid, @Nullable String proxiedAttributionTag, @Nullable String message) {
return noteProxyOpNoThrow(strOpToOp(op), new AttributionSource(
mContext.getAttributionSource(), new AttributionSource(proxiedUid,
- proxiedPackageName, proxiedAttributionTag, mContext.getAttributionSource()
- .getToken())), message,/*skipProxyOperation*/ false);
+ Process.INVALID_PID, proxiedPackageName, proxiedAttributionTag,
+ mContext.getAttributionSource().getToken())), message,
+ /*skipProxyOperation*/ false);
}
/**
@@ -8906,9 +8907,9 @@
public int startProxyOp(@NonNull String op, int proxiedUid, @NonNull String proxiedPackageName,
@Nullable String proxiedAttributionTag, @Nullable String message) {
return startProxyOp(op, new AttributionSource(mContext.getAttributionSource(),
- new AttributionSource(proxiedUid, proxiedPackageName, proxiedAttributionTag,
- mContext.getAttributionSource().getToken())), message,
- /*skipProxyOperation*/ false);
+ new AttributionSource(proxiedUid, Process.INVALID_PID, proxiedPackageName,
+ proxiedAttributionTag, mContext.getAttributionSource().getToken())),
+ message, /*skipProxyOperation*/ false);
}
/**
@@ -8954,7 +8955,7 @@
@Nullable String message) {
return startProxyOpNoThrow(AppOpsManager.strOpToOp(op), new AttributionSource(
mContext.getAttributionSource(), new AttributionSource(proxiedUid,
- proxiedPackageName, proxiedAttributionTag,
+ Process.INVALID_PID, proxiedPackageName, proxiedAttributionTag,
mContext.getAttributionSource().getToken())), message,
/*skipProxyOperation*/ false);
}
@@ -9103,8 +9104,8 @@
@NonNull String proxiedPackageName, @Nullable String proxiedAttributionTag) {
IBinder token = mContext.getAttributionSource().getToken();
finishProxyOp(token, op, new AttributionSource(mContext.getAttributionSource(),
- new AttributionSource(proxiedUid, proxiedPackageName, proxiedAttributionTag,
- token)), /*skipProxyOperation*/ false);
+ new AttributionSource(proxiedUid, Process.INVALID_PID, proxiedPackageName,
+ proxiedAttributionTag, token)), /*skipProxyOperation*/ false);
}
/**
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 762fb04..0cd42a3 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -3461,7 +3461,9 @@
@Nullable AttributionSource nextAttributionSource,
@Nullable Set<String> renouncedPermissions) {
AttributionSource attributionSource = new AttributionSource(Process.myUid(),
- mOpPackageName, attributionTag, renouncedPermissions, nextAttributionSource);
+ Process.myPid(), mOpPackageName, attributionTag,
+ (renouncedPermissions != null) ? renouncedPermissions.toArray(new String[0]) : null,
+ nextAttributionSource);
// If we want to access protected data on behalf of another app we need to
// tell the OS that we opt in to participate in the attribution chain.
if (nextAttributionSource != null) {
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index 2b400c1f..cd45f4d 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -101,22 +101,28 @@
@TestApi
public AttributionSource(int uid, @Nullable String packageName,
@Nullable String attributionTag) {
- this(uid, packageName, attributionTag, sDefaultToken);
+ this(uid, Process.INVALID_PID, packageName, attributionTag, sDefaultToken);
+ }
+
+ /** @hide */
+ public AttributionSource(int uid, int pid, @Nullable String packageName,
+ @Nullable String attributionTag) {
+ this(uid, pid, packageName, attributionTag, sDefaultToken);
}
/** @hide */
@TestApi
public AttributionSource(int uid, @Nullable String packageName,
@Nullable String attributionTag, @NonNull IBinder token) {
- this(uid, packageName, attributionTag, token, /*renouncedPermissions*/ null,
- /*next*/ null);
+ this(uid, Process.INVALID_PID, packageName, attributionTag, token,
+ /*renouncedPermissions*/ null, /*next*/ null);
}
/** @hide */
- public AttributionSource(int uid, @Nullable String packageName,
- @Nullable String attributionTag, @NonNull IBinder token,
- @Nullable AttributionSource next) {
- this(uid, packageName, attributionTag, token, /*renouncedPermissions*/ null, next);
+ public AttributionSource(int uid, int pid, @Nullable String packageName,
+ @Nullable String attributionTag, @NonNull IBinder token) {
+ this(uid, pid, packageName, attributionTag, token, /*renouncedPermissions*/ null,
+ /*next*/ null);
}
/** @hide */
@@ -124,26 +130,33 @@
public AttributionSource(int uid, @Nullable String packageName,
@Nullable String attributionTag, @Nullable Set<String> renouncedPermissions,
@Nullable AttributionSource next) {
- this(uid, packageName, attributionTag, (renouncedPermissions != null)
- ? renouncedPermissions.toArray(new String[0]) : null, next);
+ this(uid, Process.INVALID_PID, packageName, attributionTag, sDefaultToken,
+ (renouncedPermissions != null)
+ ? renouncedPermissions.toArray(new String[0]) : null, /*next*/ next);
}
/** @hide */
public AttributionSource(@NonNull AttributionSource current, @Nullable AttributionSource next) {
- this(current.getUid(), current.getPackageName(), current.getAttributionTag(),
- current.getToken(), current.mAttributionSourceState.renouncedPermissions, next);
+ this(current.getUid(), current.getPid(), current.getPackageName(),
+ current.getAttributionTag(), current.getToken(),
+ current.mAttributionSourceState.renouncedPermissions, next);
}
- AttributionSource(int uid, @Nullable String packageName, @Nullable String attributionTag,
- @Nullable String[] renouncedPermissions, @Nullable AttributionSource next) {
- this(uid, packageName, attributionTag, sDefaultToken, renouncedPermissions, next);
+ /** @hide */
+ public AttributionSource(int uid, int pid, @Nullable String packageName,
+ @Nullable String attributionTag, @Nullable String[] renouncedPermissions,
+ @Nullable AttributionSource next) {
+ this(uid, pid, packageName, attributionTag, sDefaultToken, renouncedPermissions, next);
}
- AttributionSource(int uid, @Nullable String packageName, @Nullable String attributionTag,
- @NonNull IBinder token, @Nullable String[] renouncedPermissions,
+ /** @hide */
+ public AttributionSource(int uid, int pid, @Nullable String packageName,
+ @Nullable String attributionTag, @NonNull IBinder token,
+ @Nullable String[] renouncedPermissions,
@Nullable AttributionSource next) {
mAttributionSourceState = new AttributionSourceState();
mAttributionSourceState.uid = uid;
+ mAttributionSourceState.pid = pid;
mAttributionSourceState.token = token;
mAttributionSourceState.packageName = packageName;
mAttributionSourceState.attributionTag = attributionTag;
@@ -162,7 +175,17 @@
// Since we just unpacked this object as part of it transiting a Binder
// call, this is the perfect time to enforce that its UID and PID can be trusted
- enforceCallingUidAndPid();
+ enforceCallingUid();
+
+ // If this object is being constructed as part of a oneway Binder call, getCallingPid will
+ // return 0 instead of the true PID. In that case, invalidate the PID by setting it to
+ // INVALID_PID (-1).
+ final int callingPid = Binder.getCallingPid();
+ if (callingPid == 0) {
+ mAttributionSourceState.pid = Process.INVALID_PID;
+ }
+
+ enforceCallingPid();
}
/** @hide */
@@ -172,23 +195,29 @@
/** @hide */
public AttributionSource withNextAttributionSource(@Nullable AttributionSource next) {
- return new AttributionSource(getUid(), getPackageName(), getAttributionTag(),
- mAttributionSourceState.renouncedPermissions, next);
+ return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
+ getToken(), mAttributionSourceState.renouncedPermissions, next);
}
/** @hide */
public AttributionSource withPackageName(@Nullable String packageName) {
- return new AttributionSource(getUid(), packageName, getAttributionTag(),
- mAttributionSourceState.renouncedPermissions, getNext());
+ return new AttributionSource(getUid(), getPid(), packageName, getAttributionTag(),
+ getToken(), mAttributionSourceState.renouncedPermissions, getNext());
}
/** @hide */
public AttributionSource withToken(@NonNull Binder token) {
- return new AttributionSource(getUid(), getPackageName(), getAttributionTag(),
+ return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
token, mAttributionSourceState.renouncedPermissions, getNext());
}
/** @hide */
+ public AttributionSource withPid(int pid) {
+ return new AttributionSource(getUid(), pid, getPackageName(), getAttributionTag(),
+ getToken(), mAttributionSourceState.renouncedPermissions, getNext());
+ }
+
+ /** @hide */
public @NonNull AttributionSourceState asState() {
return mAttributionSourceState;
}
@@ -228,6 +257,7 @@
}
try {
return new AttributionSource.Builder(uid)
+ .setPid(Process.myPid())
.setPackageName(AppGlobals.getPackageManager().getPackagesForUid(uid)[0])
.build();
} catch (Exception ignored) {
@@ -265,18 +295,6 @@
}
/**
- * If you are handling an IPC and you don't trust the caller you need to validate whether the
- * attribution source is one for the calling app to prevent the caller to pass you a source from
- * another app without including themselves in the attribution chain.
- *
- * @throws SecurityException if the attribution source cannot be trusted to be from the caller.
- */
- private void enforceCallingUidAndPid() {
- enforceCallingUid();
- enforceCallingPid();
- }
-
- /**
* If you are handling an IPC and you don't trust the caller you need to validate
* whether the attribution source is one for the calling app to prevent the caller
* to pass you a source from another app without including themselves in the
@@ -312,7 +330,10 @@
}
/**
- * Validate that the pid being claimed for the calling app is not spoofed
+ * Validate that the pid being claimed for the calling app is not spoofed.
+ *
+ * Note that the PID may be unavailable, for example if we're in a oneway Binder call. In this
+ * case, calling enforceCallingPid is guaranteed to fail. The caller should anticipate this.
*
* @throws SecurityException if the attribution source cannot be trusted to be from the caller.
* @hide
@@ -320,8 +341,12 @@
@TestApi
public void enforceCallingPid() {
if (!checkCallingPid()) {
- throw new SecurityException("Calling pid: " + Binder.getCallingPid()
- + " doesn't match source pid: " + mAttributionSourceState.pid);
+ if (Binder.getCallingPid() == 0) {
+ throw new SecurityException("Calling pid unavailable due to oneway Binder call.");
+ } else {
+ throw new SecurityException("Calling pid: " + Binder.getCallingPid()
+ + " doesn't match source pid: " + mAttributionSourceState.pid);
+ }
}
}
@@ -332,7 +357,8 @@
*/
private boolean checkCallingPid() {
final int callingPid = Binder.getCallingPid();
- if (mAttributionSourceState.pid != -1 && callingPid != mAttributionSourceState.pid) {
+ if (mAttributionSourceState.pid != Process.INVALID_PID
+ && callingPid != mAttributionSourceState.pid) {
return false;
}
return true;
@@ -449,6 +475,13 @@
}
/**
+ * The PID that is accessing the permission protected data.
+ */
+ public int getPid() {
+ return mAttributionSourceState.pid;
+ }
+
+ /**
* The package that is accessing the permission protected data.
*/
public @Nullable String getPackageName() {
@@ -551,6 +584,7 @@
throw new IllegalArgumentException("current AttributionSource can not be null");
}
mAttributionSourceState.uid = current.getUid();
+ mAttributionSourceState.pid = current.getPid();
mAttributionSourceState.packageName = current.getPackageName();
mAttributionSourceState.attributionTag = current.getAttributionTag();
mAttributionSourceState.token = current.getToken();
@@ -559,11 +593,25 @@
}
/**
+ * The PID of the process that is accessing the permission protected data.
+ *
+ * If not called, pid will default to {@link Process@INVALID_PID} (-1). This indicates that
+ * the PID data is missing. Supplying a PID is not required, but recommended when
+ * accessible.
+ */
+ public @NonNull Builder setPid(int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mAttributionSourceState.pid = value;
+ return this;
+ }
+
+ /**
* The package that is accessing the permission protected data.
*/
public @NonNull Builder setPackageName(@Nullable String value) {
checkNotUsed();
- mBuilderFieldsSet |= 0x2;
+ mBuilderFieldsSet |= 0x4;
mAttributionSourceState.packageName = value;
return this;
}
@@ -573,7 +621,7 @@
*/
public @NonNull Builder setAttributionTag(@Nullable String value) {
checkNotUsed();
- mBuilderFieldsSet |= 0x4;
+ mBuilderFieldsSet |= 0x8;
mAttributionSourceState.attributionTag = value;
return this;
}
@@ -606,7 +654,7 @@
@RequiresPermission(android.Manifest.permission.RENOUNCE_PERMISSIONS)
public @NonNull Builder setRenouncedPermissions(@Nullable Set<String> value) {
checkNotUsed();
- mBuilderFieldsSet |= 0x8;
+ mBuilderFieldsSet |= 0x10;
mAttributionSourceState.renouncedPermissions = (value != null)
? value.toArray(new String[0]) : null;
return this;
@@ -617,7 +665,7 @@
*/
public @NonNull Builder setNext(@Nullable AttributionSource value) {
checkNotUsed();
- mBuilderFieldsSet |= 0x10;
+ mBuilderFieldsSet |= 0x20;
mAttributionSourceState.next = (value != null) ? new AttributionSourceState[]
{value.mAttributionSourceState} : mAttributionSourceState.next;
return this;
@@ -629,15 +677,18 @@
mBuilderFieldsSet |= 0x40; // Mark builder used
if ((mBuilderFieldsSet & 0x2) == 0) {
- mAttributionSourceState.packageName = null;
+ mAttributionSourceState.pid = Process.INVALID_PID;
}
if ((mBuilderFieldsSet & 0x4) == 0) {
- mAttributionSourceState.attributionTag = null;
+ mAttributionSourceState.packageName = null;
}
if ((mBuilderFieldsSet & 0x8) == 0) {
- mAttributionSourceState.renouncedPermissions = null;
+ mAttributionSourceState.attributionTag = null;
}
if ((mBuilderFieldsSet & 0x10) == 0) {
+ mAttributionSourceState.renouncedPermissions = null;
+ }
+ if ((mBuilderFieldsSet & 0x20) == 0) {
mAttributionSourceState.next = null;
}
diff --git a/core/java/android/content/PermissionChecker.java b/core/java/android/content/PermissionChecker.java
index 8d3452e..0e3217d 100644
--- a/core/java/android/content/PermissionChecker.java
+++ b/core/java/android/content/PermissionChecker.java
@@ -152,7 +152,7 @@
@NonNull String permission, int pid, int uid, @Nullable String packageName,
@Nullable String attributionTag, @Nullable String message, boolean startDataDelivery) {
return checkPermissionForDataDelivery(context, permission, pid, new AttributionSource(uid,
- packageName, attributionTag), message, startDataDelivery);
+ pid, packageName, attributionTag), message, startDataDelivery);
}
/**
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index 0806f1d..493a4ff 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -167,6 +167,48 @@
}
/**
+ * Gets a {@link GetPendingCredentialResponse} that can launch the credential retrieval UI flow
+ * to request a user credential for your app.
+ *
+ * @param request the request specifying type(s) of credentials to get from the user
+ * @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
+ *
+ * @hide
+ */
+ public void getPendingCredential(
+ @NonNull GetCredentialRequest request,
+ @Nullable CancellationSignal cancellationSignal,
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<
+ GetPendingCredentialResponse, GetCredentialException> callback) {
+ requireNonNull(request, "request must not be null");
+ requireNonNull(executor, "executor must not be null");
+ requireNonNull(callback, "callback must not be null");
+
+ if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+ Log.w(TAG, "getPendingCredential already canceled");
+ return;
+ }
+
+ ICancellationSignal cancelRemote = null;
+ try {
+ cancelRemote =
+ mService.executeGetPendingCredential(
+ request,
+ new GetPendingCredentialTransport(executor, callback),
+ mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+
+ if (cancellationSignal != null && cancelRemote != null) {
+ cancellationSignal.setRemote(cancelRemote);
+ }
+ }
+
+ /**
* Launches the necessary flows to register an app credential for the user.
*
* <p>The execution can potentially launch UI flows to collect user consent to creating or
@@ -442,6 +484,32 @@
}
}
+ private static class GetPendingCredentialTransport extends IGetPendingCredentialCallback.Stub {
+ // TODO: listen for cancellation to release callback.
+
+ private final Executor mExecutor;
+ private final OutcomeReceiver<
+ GetPendingCredentialResponse, GetCredentialException> mCallback;
+
+ private GetPendingCredentialTransport(
+ Executor executor,
+ OutcomeReceiver<GetPendingCredentialResponse, GetCredentialException> callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onResponse(GetPendingCredentialResponse response) {
+ mExecutor.execute(() -> mCallback.onResult(response));
+ }
+
+ @Override
+ public void onError(String errorType, String message) {
+ mExecutor.execute(
+ () -> mCallback.onError(new GetCredentialException(errorType, message)));
+ }
+ }
+
private static class GetCredentialTransport extends IGetCredentialCallback.Stub {
// TODO: listen for cancellation to release callback.
diff --git a/core/java/android/credentials/GetPendingCredentialResponse.aidl b/core/java/android/credentials/GetPendingCredentialResponse.aidl
new file mode 100644
index 0000000..1cdd637
--- /dev/null
+++ b/core/java/android/credentials/GetPendingCredentialResponse.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials;
+
+parcelable GetPendingCredentialResponse;
\ No newline at end of file
diff --git a/core/java/android/credentials/GetPendingCredentialResponse.java b/core/java/android/credentials/GetPendingCredentialResponse.java
new file mode 100644
index 0000000..9005d9d
--- /dev/null
+++ b/core/java/android/credentials/GetPendingCredentialResponse.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.os.CancellationSignal;
+import android.os.OutcomeReceiver;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.concurrent.Executor;
+
+
+/**
+ * A response object that prefetches user app credentials and provides metadata about them. It can
+ * then be used to issue the full credential retrieval flow via the
+ * {@link #show(Activity, CancellationSignal, Executor, OutcomeReceiver)} method to perform the
+ * necessary flows such as consent collection and officially retrieve a credential.
+ *
+ * @hide
+ */
+public final class GetPendingCredentialResponse implements Parcelable {
+ private final boolean mHasCredentialResults;
+ private final boolean mHasAuthenticationResults;
+ private final boolean mHasRemoteResults;
+
+ /** Returns true if the user has any candidate credentials, and false otherwise. */
+ public boolean hasCredentialResults() {
+ return mHasCredentialResults;
+ }
+
+ /**
+ * Returns true if the user has any candidate authentication actions (locked credential
+ * supplier), and false otherwise.
+ */
+ public boolean hasAuthenticationResults() {
+ return mHasAuthenticationResults;
+ }
+
+ /**
+ * Returns true if the user has any candidate remote credential results, and false otherwise.
+ */
+ public boolean hasRemoteResults() {
+ return mHasRemoteResults;
+ }
+
+ /**
+ * Launches the necessary flows such as consent collection and credential selection to
+ * officially retrieve a credential among the pending credential candidates.
+ *
+ * @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 show(@NonNull Activity activity, @Nullable CancellationSignal cancellationSignal,
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
+ // TODO(b/273308895): implement
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mHasCredentialResults);
+ dest.writeBoolean(mHasAuthenticationResults);
+ dest.writeBoolean(mHasRemoteResults);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "GetCredentialResponse {" + "credential=" + mHasCredentialResults + "}";
+ }
+
+ /**
+ * Constructs a {@link GetPendingCredentialResponse}.
+ *
+ * @param hasCredentialResults whether the user has any candidate credentials
+ * @param hasAuthenticationResults whether the user has any candidate authentication actions
+ * @param hasRemoteResults whether the user has any candidate remote options
+ */
+ public GetPendingCredentialResponse(boolean hasCredentialResults,
+ boolean hasAuthenticationResults, boolean hasRemoteResults) {
+ mHasCredentialResults = hasCredentialResults;
+ mHasAuthenticationResults = hasAuthenticationResults;
+ mHasRemoteResults = hasRemoteResults;
+ }
+
+ private GetPendingCredentialResponse(@NonNull Parcel in) {
+ mHasCredentialResults = in.readBoolean();
+ mHasAuthenticationResults = in.readBoolean();
+ mHasRemoteResults = in.readBoolean();
+ }
+
+ public static final @NonNull Creator<GetPendingCredentialResponse> CREATOR = new Creator<>() {
+ @Override
+ public GetPendingCredentialResponse[] newArray(int size) {
+ return new GetPendingCredentialResponse[size];
+ }
+
+ @Override
+ public GetPendingCredentialResponse createFromParcel(@NonNull Parcel in) {
+ return new GetPendingCredentialResponse(in);
+ }
+ };
+}
diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl
index 8c2cb5a..af8e7b4 100644
--- a/core/java/android/credentials/ICredentialManager.aidl
+++ b/core/java/android/credentials/ICredentialManager.aidl
@@ -27,6 +27,7 @@
import android.credentials.IClearCredentialStateCallback;
import android.credentials.ICreateCredentialCallback;
import android.credentials.IGetCredentialCallback;
+import android.credentials.IGetPendingCredentialCallback;
import android.credentials.ISetEnabledProvidersCallback;
import android.content.ComponentName;
import android.os.ICancellationSignal;
@@ -40,6 +41,8 @@
@nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback, String callingPackage);
+ @nullable ICancellationSignal executeGetPendingCredential(in GetCredentialRequest request, in IGetPendingCredentialCallback callback, String callingPackage);
+
@nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback, String callingPackage);
@nullable ICancellationSignal clearCredentialState(in ClearCredentialStateRequest request, in IClearCredentialStateCallback callback, String callingPackage);
diff --git a/core/java/android/credentials/IGetPendingCredentialCallback.aidl b/core/java/android/credentials/IGetPendingCredentialCallback.aidl
new file mode 100644
index 0000000..4ab0f99
--- /dev/null
+++ b/core/java/android/credentials/IGetPendingCredentialCallback.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials;
+
+import android.app.PendingIntent;
+import android.credentials.GetPendingCredentialResponse;
+
+/**
+ * Listener for a executeGetPendingCredential request.
+ *
+ * @hide
+ */
+interface IGetPendingCredentialCallback {
+ oneway void onResponse(in GetPendingCredentialResponse response);
+ oneway void onError(String errorType, String message);
+}
\ No newline at end of file
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index ac1583a..b2208d1 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -365,6 +365,11 @@
public static final int LAST_APPLICATION_CACHE_GID = 29999;
/**
+ * An invalid PID value.
+ */
+ public static final int INVALID_PID = -1;
+
+ /**
* Standard priority of application threads.
* Use with {@link #setThreadPriority(int)} and
* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java
index 071c20f..1e6c1ff 100644
--- a/core/java/android/window/SnapshotDrawerUtils.java
+++ b/core/java/android/window/SnapshotDrawerUtils.java
@@ -223,6 +223,11 @@
PixelFormat.RGBA_8888,
GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_HW_COMPOSER
| GraphicBuffer.USAGE_SW_WRITE_RARELY);
+ if (background == null) {
+ Log.e(TAG, "Unable to draw snapshot: failed to allocate graphic buffer for "
+ + mTitle);
+ return;
+ }
// TODO: Support this on HardwareBuffer
final Canvas c = background.lockCanvas();
drawBackgroundAndBars(c, frame);
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index b8bd703..4c482460 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -24,6 +24,7 @@
import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN;
import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
@@ -58,6 +59,7 @@
* @hide
*/
public final class TransitionInfo implements Parcelable {
+ private static final String TAG = "TransitionInfo";
/**
* Modes are only a sub-set of all the transit-types since they are per-container
@@ -184,9 +186,7 @@
private final @TransitionType int mType;
private final @TransitionFlags int mFlags;
private final ArrayList<Change> mChanges = new ArrayList<>();
-
- private SurfaceControl mRootLeash;
- private final Point mRootOffset = new Point();
+ private final ArrayList<Root> mRoots = new ArrayList<>();
private AnimationOptions mOptions;
@@ -200,10 +200,7 @@
mType = in.readInt();
mFlags = in.readInt();
in.readTypedList(mChanges, Change.CREATOR);
- mRootLeash = new SurfaceControl();
- mRootLeash.readFromParcel(in);
- mRootLeash.setUnreleasedWarningCallSite("TransitionInfo");
- mRootOffset.readFromParcel(in);
+ in.readTypedList(mRoots, Root.CREATOR);
mOptions = in.readTypedObject(AnimationOptions.CREATOR);
}
@@ -213,8 +210,7 @@
dest.writeInt(mType);
dest.writeInt(mFlags);
dest.writeTypedList(mChanges);
- mRootLeash.writeToParcel(dest, flags);
- mRootOffset.writeToParcel(dest, flags);
+ dest.writeTypedList(mRoots, flags);
dest.writeTypedObject(mOptions, flags);
}
@@ -238,10 +234,15 @@
return 0;
}
- /** @see #getRootLeash() */
- public void setRootLeash(@NonNull SurfaceControl leash, int offsetLeft, int offsetTop) {
- mRootLeash = leash;
- mRootOffset.set(offsetLeft, offsetTop);
+ /** @see #getRoot */
+ public void addRootLeash(int displayId, @NonNull SurfaceControl leash,
+ int offsetLeft, int offsetTop) {
+ mRoots.add(new Root(displayId, leash, offsetLeft, offsetTop));
+ }
+
+ /** @see #getRoot */
+ public void addRoot(Root other) {
+ mRoots.add(other);
}
public void setAnimationOptions(AnimationOptions options) {
@@ -257,23 +258,52 @@
}
/**
+ * @return The number of animation roots. Most transitions should have 1, but there may be more
+ * in some cases (such as a transition spanning multiple displays).
+ */
+ public int getRootCount() {
+ return mRoots.size();
+ }
+
+ /**
+ * @return the transition-root at a specific index.
+ */
+ @NonNull
+ public Root getRoot(int idx) {
+ return mRoots.get(idx);
+ }
+
+ /**
+ * @return the index of the transition-root associated with `displayId` or -1 if not found.
+ */
+ public int findRootIndex(int displayId) {
+ for (int i = 0; i < mRoots.size(); ++i) {
+ if (mRoots.get(i).mDisplayId == displayId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
* @return a surfacecontrol that can serve as a parent surfacecontrol for all the changing
* participants to animate within. This will generally be placed at the highest-z-order
* shared ancestor of all participants. While this is non-null, it's possible for the rootleash
* to be invalid if the transition is a no-op.
+ *
+ * @deprecated Use {@link #getRoot} instead. This call assumes there is only one root.
*/
+ @Deprecated
@NonNull
public SurfaceControl getRootLeash() {
- if (mRootLeash == null) {
- throw new IllegalStateException("Trying to get a leash which wasn't set");
+ if (mRoots.isEmpty()) {
+ throw new IllegalStateException("Trying to get a root leash from a no-op transition.");
}
- return mRootLeash;
- }
-
- /** @return the offset (relative to the screen) of the root leash. */
- @NonNull
- public Point getRootOffset() {
- return mRootOffset;
+ if (mRoots.size() > 1) {
+ android.util.Log.e(TAG, "Assuming one animation root when there are more.",
+ new Throwable());
+ }
+ return mRoots.get(0).mLeash;
}
public AnimationOptions getAnimationOptions() {
@@ -320,8 +350,15 @@
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
- sb.append("{t=" + transitTypeToString(mType) + " f=0x" + Integer.toHexString(mFlags)
- + " ro=" + mRootOffset + " c=[");
+ sb.append("{t=").append(transitTypeToString(mType)).append(" f=0x")
+ .append(Integer.toHexString(mFlags)).append(" r=[");
+ for (int i = 0; i < mRoots.size(); ++i) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ sb.append(mRoots.get(i).mDisplayId).append("@").append(mRoots.get(i).mOffset);
+ }
+ sb.append("] c=[");
for (int i = 0; i < mChanges.size(); ++i) {
if (i > 0) {
sb.append(',');
@@ -448,8 +485,8 @@
c.mSnapshot = null;
}
}
- if (mRootLeash != null) {
- mRootLeash.release();
+ for (int i = 0; i < mRoots.size(); ++i) {
+ mRoots.get(i).mLeash.release();
}
}
@@ -476,10 +513,11 @@
for (int i = 0; i < mChanges.size(); ++i) {
out.mChanges.add(mChanges.get(i).localRemoteCopy());
}
- out.mRootLeash = mRootLeash != null ? new SurfaceControl(mRootLeash, "localRemote") : null;
+ for (int i = 0; i < mRoots.size(); ++i) {
+ out.mRoots.add(mRoots.get(i).localRemoteCopy());
+ }
// Doesn't have any native stuff, so no need for actual copy
out.mOptions = mOptions;
- out.mRootOffset.set(mRootOffset);
return out;
}
@@ -496,6 +534,8 @@
private final Point mEndRelOffset = new Point();
private ActivityManager.RunningTaskInfo mTaskInfo = null;
private boolean mAllowEnterPip;
+ private int mStartDisplayId = INVALID_DISPLAY;
+ private int mEndDisplayId = INVALID_DISPLAY;
private @Surface.Rotation int mStartRotation = ROTATION_UNDEFINED;
private @Surface.Rotation int mEndRotation = ROTATION_UNDEFINED;
/**
@@ -526,6 +566,8 @@
mEndRelOffset.readFromParcel(in);
mTaskInfo = in.readTypedObject(ActivityManager.RunningTaskInfo.CREATOR);
mAllowEnterPip = in.readBoolean();
+ mStartDisplayId = in.readInt();
+ mEndDisplayId = in.readInt();
mStartRotation = in.readInt();
mEndRotation = in.readInt();
mEndFixedRotation = in.readInt();
@@ -546,6 +588,8 @@
out.mEndRelOffset.set(mEndRelOffset);
out.mTaskInfo = mTaskInfo;
out.mAllowEnterPip = mAllowEnterPip;
+ out.mStartDisplayId = mStartDisplayId;
+ out.mEndDisplayId = mEndDisplayId;
out.mStartRotation = mStartRotation;
out.mEndRotation = mEndRotation;
out.mEndFixedRotation = mEndFixedRotation;
@@ -608,6 +652,12 @@
}
/** Sets the start and end rotation of this container. */
+ public void setDisplayId(int start, int end) {
+ mStartDisplayId = start;
+ mEndDisplayId = end;
+ }
+
+ /** Sets the start and end rotation of this container. */
public void setRotation(@Surface.Rotation int start, @Surface.Rotation int end) {
mStartRotation = start;
mEndRotation = end;
@@ -725,6 +775,14 @@
return mAllowEnterPip;
}
+ public int getStartDisplayId() {
+ return mStartDisplayId;
+ }
+
+ public int getEndDisplayId() {
+ return mEndDisplayId;
+ }
+
@Surface.Rotation
public int getStartRotation() {
return mStartRotation;
@@ -776,6 +834,8 @@
mEndRelOffset.writeToParcel(dest, flags);
dest.writeTypedObject(mTaskInfo, flags);
dest.writeBoolean(mAllowEnterPip);
+ dest.writeInt(mStartDisplayId);
+ dest.writeInt(mEndDisplayId);
dest.writeInt(mStartRotation);
dest.writeInt(mEndRotation);
dest.writeInt(mEndFixedRotation);
@@ -822,6 +882,11 @@
if (mEndRelOffset.x != 0 || mEndRelOffset.y != 0) {
sb.append(" eo="); sb.append(mEndRelOffset);
}
+ sb.append(" d=");
+ if (mStartDisplayId != mEndDisplayId) {
+ sb.append(mStartDisplayId).append("->");
+ }
+ sb.append(mEndDisplayId);
if (mStartRotation != mEndRotation) {
sb.append(" r="); sb.append(mStartRotation);
sb.append("->"); sb.append(mEndRotation);
@@ -1108,4 +1173,86 @@
};
}
}
+
+ /**
+ * An animation root in a transition. There is one of these for each display that contains
+ * participants. It will be placed, in z-order, right above the top-most participant and at the
+ * same position in the hierarchy. As a result, if all participants are animating within a
+ * part of the screen, the root-leash will only be in that part of the screen. In these cases,
+ * it's relative position (from the screen) is stored in {@link Root#getOffset}.
+ */
+ public static final class Root implements Parcelable {
+ private final int mDisplayId;
+ private final SurfaceControl mLeash;
+ private final Point mOffset = new Point();
+
+ public Root(int displayId, @NonNull SurfaceControl leash, int offsetLeft, int offsetTop) {
+ mDisplayId = displayId;
+ mLeash = leash;
+ mOffset.set(offsetLeft, offsetTop);
+ }
+
+ private Root(Parcel in) {
+ mDisplayId = in.readInt();
+ mLeash = new SurfaceControl();
+ mLeash.readFromParcel(in);
+ mLeash.setUnreleasedWarningCallSite("TransitionInfo.Root");
+ mOffset.readFromParcel(in);
+ }
+
+ private Root localRemoteCopy() {
+ return new Root(mDisplayId, new SurfaceControl(mLeash, "localRemote"),
+ mOffset.x, mOffset.y);
+ }
+
+ /** @return the id of the display this root is on. */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /** @return the root's leash. Surfaces should be parented to this while animating. */
+ @NonNull
+ public SurfaceControl getLeash() {
+ return mLeash;
+ }
+
+ /** @return the offset (relative to its screen) of the root leash. */
+ @NonNull
+ public Point getOffset() {
+ return mOffset;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mDisplayId);
+ mLeash.writeToParcel(dest, flags);
+ mOffset.writeToParcel(dest, flags);
+ }
+
+ @NonNull
+ public static final Creator<Root> CREATOR =
+ new Creator<Root>() {
+ @Override
+ public Root createFromParcel(Parcel in) {
+ return new Root(in);
+ }
+
+ @Override
+ public Root[] newArray(int size) {
+ return new Root[size];
+ }
+ };
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return mDisplayId + "@" + mOffset + ":" + mLeash;
+ }
+ }
}
diff --git a/core/java/android/window/WindowInfosListenerForTest.java b/core/java/android/window/WindowInfosListenerForTest.java
index 429156f..01e577f 100644
--- a/core/java/android/window/WindowInfosListenerForTest.java
+++ b/core/java/android/window/WindowInfosListenerForTest.java
@@ -63,10 +63,17 @@
@NonNull
public final Rect bounds;
- WindowInfo(@NonNull IBinder windowToken, @NonNull String name, @NonNull Rect bounds) {
+ /**
+ * True if the window is a trusted overlay.
+ */
+ public final boolean isTrustedOverlay;
+
+ WindowInfo(@NonNull IBinder windowToken, @NonNull String name, @NonNull Rect bounds,
+ int inputConfig) {
this.windowToken = windowToken;
this.name = name;
this.bounds = bounds;
+ this.isTrustedOverlay = (inputConfig & InputConfig.TRUSTED_OVERLAY) != 0;
}
}
@@ -129,7 +136,8 @@
}
var bounds = new Rect(handle.frameLeft, handle.frameTop, handle.frameRight,
handle.frameBottom);
- windowInfos.add(new WindowInfo(handle.getWindowToken(), handle.name, bounds));
+ windowInfos.add(new WindowInfo(handle.getWindowToken(), handle.name, bounds,
+ handle.inputConfig));
}
return windowInfos;
}
diff --git a/core/java/com/android/internal/app/LocaleHelper.java b/core/java/com/android/internal/app/LocaleHelper.java
index 57bd3f9..d521866 100644
--- a/core/java/com/android/internal/app/LocaleHelper.java
+++ b/core/java/com/android/internal/app/LocaleHelper.java
@@ -20,6 +20,7 @@
import android.compat.annotation.UnsupportedAppUsage;
import android.icu.text.CaseMap;
import android.icu.text.ListFormatter;
+import android.icu.text.NumberingSystem;
import android.icu.util.ULocale;
import android.os.LocaleList;
import android.text.TextUtils;
@@ -173,6 +174,21 @@
}
/**
+ * Returns numbering system value of a locale for display in the provided locale.
+ *
+ * @param locale The locale whose key value is displayed.
+ * @param displayLocale The locale in which to display the key value.
+ * @return The string of numbering system.
+ */
+ public static String getDisplayNumberingSystemKeyValue(
+ Locale locale, Locale displayLocale) {
+ ULocale uLocale = new ULocale.Builder()
+ .setUnicodeLocaleKeyword("nu", NumberingSystem.getInstance(locale).getName())
+ .build();
+ return uLocale.getDisplayKeywordValue("numbers", ULocale.forLocale(displayLocale));
+ }
+
+ /**
* Adds the likely subtags for a provided locale ID.
*
* @param locale the locale to maximize.
diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java
index 685bd9a..5dfc0ea 100644
--- a/core/java/com/android/internal/app/LocalePickerWithRegion.java
+++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java
@@ -61,6 +61,7 @@
private int mTopDistance = 0;
private CharSequence mTitle = null;
private OnActionExpandListener mOnActionExpandListener;
+ private boolean mIsNumberingSystem = false;
/**
* Other classes can register to be notified when a locale was selected.
@@ -90,6 +91,18 @@
boolean hasSpecificPackageName();
}
+ private static LocalePickerWithRegion createNumberingSystemPicker(
+ LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
+ boolean translatedOnly, OnActionExpandListener onActionExpandListener,
+ LocaleCollectorBase localePickerCollector) {
+ LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
+ localePicker.setOnActionExpandListener(onActionExpandListener);
+ localePicker.setIsNumberingSystem(true);
+ boolean shouldShowTheList = localePicker.setListener(listener, parent,
+ translatedOnly, localePickerCollector);
+ return shouldShowTheList ? localePicker : null;
+ }
+
private static LocalePickerWithRegion createCountryPicker(
LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
boolean translatedOnly, OnActionExpandListener onActionExpandListener,
@@ -128,6 +141,10 @@
return localePicker;
}
+ private void setIsNumberingSystem(boolean isNumberingSystem) {
+ mIsNumberingSystem = isNumberingSystem;
+ }
+
/**
* Sets the listener and initializes the locale list.
*
@@ -184,6 +201,7 @@
final boolean hasSpecificPackageName =
mLocalePickerCollector != null && mLocalePickerCollector.hasSpecificPackageName();
mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, hasSpecificPackageName);
+ mAdapter.setNumberingSystemMode(mIsNumberingSystem);
final LocaleHelper.LocaleInfoComparator comp =
new LocaleHelper.LocaleInfoComparator(sortingLocale, countryMode);
mAdapter.sort(comp);
@@ -213,7 +231,6 @@
@Override
public void onResume() {
super.onResume();
-
if (mParentLocale != null) {
getActivity().setTitle(mParentLocale.getFullNameNative());
} else {
@@ -250,16 +267,28 @@
// Special case for resetting the app locale to equal the system locale.
boolean isSystemLocale = locale.isSystemLocale();
boolean isRegionLocale = locale.getParent() != null;
+ boolean mayHaveDifferentNumberingSystem = locale.hasNumberingSystems();
- if (isSystemLocale || isRegionLocale) {
+ if (isSystemLocale
+ || (isRegionLocale && !mayHaveDifferentNumberingSystem)
+ || mIsNumberingSystem) {
if (mListener != null) {
mListener.onLocaleSelected(locale);
}
returnToParentFrame();
} else {
- LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
- mListener, locale, mTranslatedOnly /* translate only */,
- mOnActionExpandListener, this.mLocalePickerCollector);
+ LocalePickerWithRegion selector;
+ if (mayHaveDifferentNumberingSystem) {
+ selector =
+ LocalePickerWithRegion.createNumberingSystemPicker(
+ mListener, locale, mTranslatedOnly /* translate only */,
+ mOnActionExpandListener, this.mLocalePickerCollector);
+ } else {
+ selector = LocalePickerWithRegion.createCountryPicker(
+ mListener, locale, mTranslatedOnly /* translate only */,
+ mOnActionExpandListener, this.mLocalePickerCollector);
+ }
+
if (selector != null) {
getFragmentManager().beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java
index 8b41829..bcbfdc9 100644
--- a/core/java/com/android/internal/app/LocaleStore.java
+++ b/core/java/com/android/internal/app/LocaleStore.java
@@ -39,6 +39,9 @@
import java.util.Set;
public class LocaleStore {
+ private static final int TIER_LANGUAGE = 1;
+ private static final int TIER_REGION = 2;
+ private static final int TIER_NUMBERING = 3;
private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
private static final String TAG = LocaleStore.class.getSimpleName();
private static boolean sFullyInitialized = false;
@@ -68,10 +71,13 @@
private String mFullCountryNameNative;
private String mLangScriptKey;
+ private boolean mHasNumberingSystems;
+
private LocaleInfo(Locale locale) {
this.mLocale = locale;
this.mId = locale.toLanguageTag();
this.mParent = getParent(locale);
+ this.mHasNumberingSystems = false;
this.mIsChecked = false;
this.mSuggestionFlags = SUGGESTION_TYPE_NONE;
this.mIsTranslated = false;
@@ -93,6 +99,11 @@
.build();
}
+ /** Return true if there are any same locales with different numbering system. */
+ public boolean hasNumberingSystems() {
+ return mHasNumberingSystems;
+ }
+
@Override
public String toString() {
return mId;
@@ -195,6 +206,10 @@
}
}
+ String getNumberingSystem() {
+ return LocaleHelper.getDisplayNumberingSystemKeyValue(mLocale, mLocale);
+ }
+
String getContentDescription(boolean countryMode) {
if (countryMode) {
return getFullCountryNameInUiLanguage();
@@ -383,6 +398,12 @@
final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
+ Set<Locale> numberSystemLocaleList = new HashSet<>();
+ for (String localeId : LocalePicker.getSupportedLocales(context)) {
+ if (Locale.forLanguageTag(localeId).getUnicodeLocaleType("nu") != null) {
+ numberSystemLocaleList.add(Locale.forLanguageTag(localeId));
+ }
+ }
for (String localeId : LocalePicker.getSupportedLocales(context)) {
if (localeId.isEmpty()) {
throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
@@ -403,6 +424,12 @@
if (simCountries.contains(li.getLocale().getCountry())) {
li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
}
+ numberSystemLocaleList.forEach(l -> {
+ if (li.getLocale().stripExtensions().equals(l.stripExtensions())) {
+ li.mHasNumberingSystems = true;
+ }
+ });
+
sLocaleCache.put(li.getId(), li);
final Locale parent = li.getParent();
if (parent != null) {
@@ -445,20 +472,43 @@
sFullyInitialized = true;
}
- private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
- if (ignorables.contains(li.getId())) return 0;
- if (li.mIsPseudo) return 2;
- if (translatedOnly && !li.isTranslated()) return 0;
- if (li.getParent() != null) return 2;
- return 0;
+ private static boolean isShallIgnore(
+ Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
+ if (ignorables.stream().anyMatch(tag ->
+ Locale.forLanguageTag(tag).stripExtensions()
+ .equals(li.getLocale().stripExtensions()))) {
+ return true;
+ }
+ if (li.mIsPseudo) return false;
+ if (translatedOnly && !li.isTranslated()) return true;
+ if (li.getParent() != null) return false;
+ return true;
+ }
+
+ private static int getLocaleTier(LocaleInfo parent) {
+ if (parent == null) {
+ return TIER_LANGUAGE;
+ } else if (parent.getLocale().getCountry().isEmpty()) {
+ return TIER_REGION;
+ } else {
+ return TIER_NUMBERING;
+ }
}
/**
* Returns a list of locales for language or region selection.
+ *
* If the parent is null, then it is the language list.
+ *
* If it is not null, then the list will contain all the locales that belong to that parent.
* Example: if the parent is "ar", then the region list will contain all Arabic locales.
- * (this is not language based, but language-script, so that it works for zh-Hant and so on.
+ * (this is not language based, but language-script, so that it works for zh-Hant and so on.)
+ *
+ * If it is not null and has country, then the list will contain all locales with that parent's
+ * language and country, i.e. containing alternate numbering systems.
+ *
+ * Example: if the parent is "ff-Adlm-BF", then the numbering list will contain all
+ * Fula (Adlam, Burkina Faso) i.e. "ff-Adlm-BF" and "ff-Adlm-BF-u-nu-latn"
*/
@UnsupportedAppUsage
public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
@@ -478,28 +528,49 @@
*/
public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
LocaleInfo parent, boolean translatedOnly, LocaleList explicitLocales) {
- fillCache(context);
- String parentId = parent == null ? null : parent.getId();
- HashSet<LocaleInfo> result = new HashSet<>();
+ if (context != null) {
+ fillCache(context);
+ }
HashMap<String, LocaleInfo> supportedLcoaleInfos =
explicitLocales == null
? sLocaleCache
: convertExplicitLocales(explicitLocales, sLocaleCache.values());
+ return getTierLocales(ignorables, parent, translatedOnly, supportedLcoaleInfos);
+ }
+ private static Set<LocaleInfo> getTierLocales(
+ Set<String> ignorables,
+ LocaleInfo parent,
+ boolean translatedOnly,
+ HashMap<String, LocaleInfo> supportedLcoaleInfos) {
+
+ boolean hasTargetParent = parent != null;
+ String parentId = hasTargetParent ? parent.getId() : null;
+ HashSet<LocaleInfo> result = new HashSet<>();
for (LocaleStore.LocaleInfo li : supportedLcoaleInfos.values()) {
- int level = getLevel(ignorables, li, translatedOnly);
- if (level == 2) {
- if (parent != null) { // region selection
- if (parentId.equals(li.getParent().toLanguageTag())) {
- result.add(li);
- }
- } else { // language selection
+ if (isShallIgnore(ignorables, li, translatedOnly)) {
+ continue;
+ }
+ switch(getLocaleTier(parent)) {
+ case TIER_LANGUAGE:
if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
result.add(li);
} else {
- result.add(getLocaleInfo(li.getParent()));
+ result.add(getLocaleInfo(li.getParent(), supportedLcoaleInfos));
}
- }
+ break;
+ case TIER_REGION:
+ if (parentId.equals(li.getParent().toLanguageTag())) {
+ result.add(getLocaleInfo(
+ li.getLocale().stripExtensions(), supportedLcoaleInfos));
+ }
+ break;
+ case TIER_NUMBERING:
+ if (parent.getLocale().stripExtensions()
+ .equals(li.getLocale().stripExtensions())) {
+ result.add(li);
+ }
+ break;
}
}
return result;
@@ -538,18 +609,21 @@
}
private static LocaleList matchLocaleFromSupportedLocaleList(
- LocaleList explicitLocales, Collection<LocaleInfo> localeinfo) {
+ LocaleList explicitLocales, Collection<LocaleInfo> localeInfos) {
+ if (localeInfos == null) {
+ return explicitLocales;
+ }
//TODO: Adds a function for unicode extension if needed.
Locale[] resultLocales = new Locale[explicitLocales.size()];
for (int i = 0; i < explicitLocales.size(); i++) {
- Locale locale = explicitLocales.get(i).stripExtensions();
+ Locale locale = explicitLocales.get(i);
if (!TextUtils.isEmpty(locale.getCountry())) {
- for (LocaleInfo localeInfo :localeinfo) {
+ for (LocaleInfo localeInfo :localeInfos) {
if (LocaleList.matchesLanguageAndScript(locale, localeInfo.getLocale())
&& TextUtils.equals(locale.getCountry(),
localeInfo.getLocale().getCountry())) {
resultLocales[i] = localeInfo.getLocale();
- continue;
+ break;
}
}
}
@@ -562,18 +636,23 @@
@UnsupportedAppUsage
public static LocaleInfo getLocaleInfo(Locale locale) {
+ return getLocaleInfo(locale, sLocaleCache);
+ }
+
+ private static LocaleInfo getLocaleInfo(
+ Locale locale, HashMap<String, LocaleInfo> localeInfos) {
String id = locale.toLanguageTag();
LocaleInfo result;
- if (!sLocaleCache.containsKey(id)) {
+ if (!localeInfos.containsKey(id)) {
// Locale preferences can modify the language tag to current system languages, so we
// need to check the input locale without extra u extension except numbering system.
Locale filteredLocale = new Locale.Builder()
.setLocale(locale.stripExtensions())
.setUnicodeLocaleKeyword("nu", locale.getUnicodeLocaleType("nu"))
.build();
- if (sLocaleCache.containsKey(filteredLocale.toLanguageTag())) {
+ if (localeInfos.containsKey(filteredLocale.toLanguageTag())) {
result = new LocaleInfo(locale);
- LocaleInfo localeInfo = sLocaleCache.get(filteredLocale.toLanguageTag());
+ LocaleInfo localeInfo = localeInfos.get(filteredLocale.toLanguageTag());
// This locale is included in supported locales, so follow the settings
// of supported locales.
result.mIsPseudo = localeInfo.mIsPseudo;
@@ -582,9 +661,9 @@
return result;
}
result = new LocaleInfo(locale);
- sLocaleCache.put(id, result);
+ localeInfos.put(id, result);
} else {
- result = sLocaleCache.get(id);
+ result = localeInfos.get(id);
}
return result;
}
diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
index a61a6d7..08de4dfb 100644
--- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
+++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -64,6 +64,7 @@
protected ArrayList<LocaleStore.LocaleInfo> mOriginalLocaleOptions;
protected int mSuggestionCount;
protected final boolean mCountryMode;
+ protected boolean mIsNumberingMode;
protected LayoutInflater mInflater;
protected Locale mDisplayLocale = null;
@@ -89,6 +90,14 @@
}
}
+ public void setNumberingSystemMode(boolean isNumberSystemMode) {
+ mIsNumberingMode = isNumberSystemMode;
+ }
+
+ public boolean getIsForNumberingSystem() {
+ return mIsNumberingMode;
+ }
+
@Override
public boolean areAllItemsEnabled() {
return false;
@@ -209,7 +218,6 @@
if (convertView == null && mInflater == null) {
mInflater = LayoutInflater.from(parent.getContext());
}
-
int itemType = getItemViewType(position);
View itemView = getNewViewIfNeeded(convertView, parent, itemType, position);
switch (itemType) {
@@ -217,13 +225,13 @@
case TYPE_HEADER_ALL_OTHERS:
TextView textView = (TextView) itemView;
if (itemType == TYPE_HEADER_SUGGESTED) {
- if (mCountryMode) {
+ if (mCountryMode && !mIsNumberingMode) {
setTextTo(textView, R.string.language_picker_regions_section_suggested);
} else {
setTextTo(textView, R.string.language_picker_section_suggested);
}
} else {
- if (mCountryMode) {
+ if (mCountryMode && !mIsNumberingMode) {
setTextTo(textView, R.string.region_picker_section_all);
} else {
setTextTo(textView, R.string.language_picker_section_all);
@@ -419,9 +427,11 @@
private void updateTextView(View convertView, TextView text, int position) {
LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position);
- text.setText(item.getLabel(mCountryMode));
+ text.setText(mIsNumberingMode
+ ? item.getNumberingSystem() : item.getLabel(mCountryMode));
text.setTextLocale(item.getLocale());
- text.setContentDescription(item.getContentDescription(mCountryMode));
+ text.setContentDescription(mIsNumberingMode
+ ? item.getNumberingSystem() : item.getContentDescription(mCountryMode));
if (mCountryMode) {
int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent());
//noinspection ResourceType
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 87cf655..45bb73b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -489,10 +489,11 @@
// Reparent the pip leash to the root with max layer so that we can animate it outside of
// parent crop, and make sure it is not covered by other windows.
final SurfaceControl pipLeash = pipChange.getLeash();
- startTransaction.reparent(pipLeash, info.getRootLeash());
+ final int rootIdx = TransitionUtil.rootIndexFor(pipChange, info);
+ startTransaction.reparent(pipLeash, info.getRoot(rootIdx).getLeash());
startTransaction.setLayer(pipLeash, Integer.MAX_VALUE);
// Note: because of this, the bounds to animate should be translated to the root coordinate.
- final Point offset = info.getRootOffset();
+ final Point offset = info.getRoot(rootIdx).getOffset();
final Rect currentBounds = mPipBoundsState.getBounds();
currentBounds.offset(-offset.x, -offset.y);
startTransaction.setPosition(pipLeash, currentBounds.left, currentBounds.top);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index e1c0895..e09c3c9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -37,6 +37,7 @@
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
import android.view.SurfaceControl;
@@ -128,6 +129,7 @@
final int mode = info.getChanges().get(i).getMode();
if (mode == TRANSIT_CHANGE) {
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
if (change.getParent() != null) {
// This is probably reparented, so we want the parent to be immediately visible
final TransitionInfo.Change parentChange = info.getChange(change.getParent());
@@ -135,7 +137,7 @@
t.setAlpha(parentChange.getLeash(), 1.f);
// and then animate this layer outside the parent (since, for example, this is
// the home task animating from fullscreen to part-screen).
- t.reparent(leash, info.getRootLeash());
+ t.reparent(leash, info.getRoot(rootIdx).getLeash());
t.setLayer(leash, info.getChanges().size() - i);
// build the finish reparent/reposition
mFinishTransaction.reparent(leash, parentChange.getLeash());
@@ -145,8 +147,9 @@
// TODO(shell-transitions): screenshot here
final Rect startBounds = new Rect(change.getStartAbsBounds());
final Rect endBounds = new Rect(change.getEndAbsBounds());
- startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
- endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
+ final Point rootOffset = info.getRoot(rootIdx).getOffset();
+ startBounds.offset(-rootOffset.x, -rootOffset.y);
+ endBounds.offset(-rootOffset.x, -rootOffset.y);
startExampleResizeAnimation(leash, startBounds, endBounds);
}
boolean isRootOrSplitSideRoot = change.getParent() == null
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 75112b6..2e86448 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -179,7 +179,9 @@
out.getChanges().add(info.getChanges().get(i));
}
}
- out.setRootLeash(info.getRootLeash(), info.getRootOffset().x, info.getRootOffset().y);
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ out.addRoot(info.getRoot(i));
+ }
out.setAnimationOptions(info.getAnimationOptions());
return out;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index f66c26b..63c7969 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -383,9 +383,10 @@
continue;
}
// No default animation for this, so just update bounds/position.
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
startTransaction.setPosition(change.getLeash(),
- change.getEndAbsBounds().left - info.getRootOffset().x,
- change.getEndAbsBounds().top - info.getRootOffset().y);
+ change.getEndAbsBounds().left - info.getRoot(rootIdx).getOffset().x,
+ change.getEndAbsBounds().top - info.getRoot(rootIdx).getOffset().y);
// Seamless display transition doesn't need to animate.
if (isSeamlessDisplayChange) continue;
if (isTask || (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)
@@ -474,8 +475,10 @@
}
if (backgroundColorForTransition != 0) {
- addBackgroundToTransition(info.getRootLeash(), backgroundColorForTransition,
- startTransaction, finishTransaction);
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ addBackgroundToTransition(info.getRoot(i).getLeash(), backgroundColorForTransition,
+ startTransaction, finishTransaction);
+ }
}
if (postStartTransactionCallbacks.size() > 0) {
@@ -520,8 +523,10 @@
private void startRotationAnimation(SurfaceControl.Transaction startTransaction,
TransitionInfo.Change change, TransitionInfo info, int animHint,
ArrayList<Animator> animations, Runnable onAnimFinish) {
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession,
- mTransactionPool, startTransaction, change, info.getRootLeash(), animHint);
+ mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(),
+ animHint);
// The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real
// content, and background color. The item of "animGroup" will be removed if the sub
// animation is finished. Then if the list becomes empty, the rotation animation is done.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 27b82c0..039bde9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -75,6 +75,7 @@
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.Arrays;
@@ -415,8 +416,8 @@
private static void setupAnimHierarchy(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
boolean isOpening = isOpeningType(info.getType());
- if (info.getRootLeash().isValid()) {
- t.show(info.getRootLeash());
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ t.show(info.getRoot(i).getLeash());
}
final int numChanges = info.getChanges().size();
// Put animating stuff above this line and put static stuff below it.
@@ -434,10 +435,12 @@
boolean hasParent = change.getParent() != null;
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
if (!hasParent) {
- t.reparent(leash, info.getRootLeash());
- t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x,
- change.getStartAbsBounds().top - info.getRootOffset().y);
+ t.reparent(leash, info.getRoot(rootIdx).getLeash());
+ t.setPosition(leash,
+ change.getStartAbsBounds().left - info.getRoot(rootIdx).getOffset().x,
+ change.getStartAbsBounds().top - info.getRoot(rootIdx).getOffset().y);
}
final int layer;
// Put all the OPEN/SHOW on top
@@ -532,12 +535,6 @@
if (info.getType() == TRANSIT_SLEEP) {
if (activeIdx > 0) {
- if (!info.getRootLeash().isValid()) {
- // Shell has some debug settings which makes calling binders with invalid
- // surfaces crash, so replace it with a "real" one.
- info.setRootLeash(new SurfaceControl.Builder().setName("Invalid")
- .setContainerLayer().build(), 0, 0);
- }
// Sleep starts a process of forcing all prior transitions to finish immediately
finishForSleep(null /* forceFinish */);
return;
@@ -546,10 +543,10 @@
// Allow to notify keyguard un-occluding state to KeyguardService, which can happen while
// screen-off, so there might no visibility change involved.
- if (!info.getRootLeash().isValid() && info.getType() != TRANSIT_KEYGUARD_UNOCCLUDE) {
- // Invalid root-leash implies that the transition is empty/no-op, so just do
+ if (info.getRootCount() == 0 && info.getType() != TRANSIT_KEYGUARD_UNOCCLUDE) {
+ // No root-leashes implies that the transition is empty/no-op, so just do
// housekeeping and return.
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Invalid root leash (%s): %s",
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "No transition roots (%s): %s",
transitionToken, info);
onAbort(active);
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
index 8c6e1e7..7595c96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
@@ -139,11 +139,12 @@
// changes should be ordered top-to-bottom in z
final int mode = change.getMode();
- t.reparent(leash, info.getRootLeash());
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
+ t.reparent(leash, info.getRoot(rootIdx).getLeash());
final Rect absBounds =
(mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds();
- t.setPosition(leash, absBounds.left - info.getRootOffset().x,
- absBounds.top - info.getRootOffset().y);
+ t.setPosition(leash, absBounds.left - info.getRoot(rootIdx).getOffset().x,
+ absBounds.top - info.getRoot(rootIdx).getOffset().y);
// Put all the OPEN/SHOW on top
if (TransitionUtil.isOpeningType(mode)) {
@@ -179,12 +180,13 @@
// making leashes means we have to handle them specially.
return change.getLeash();
}
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
SurfaceControl leashSurface = new SurfaceControl.Builder()
.setName(change.getLeash().toString() + "_transition-leash")
.setContainerLayer()
// Initial the surface visible to respect the visibility of the original surface.
.setHidden(false)
- .setParent(info.getRootLeash())
+ .setParent(info.getRoot(rootIdx).getLeash())
.build();
// Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
setupLeash(leashSurface, change, info.getChanges().size() - order, info, t);
@@ -261,4 +263,18 @@
target.setRotationChange(change.getEndRotation() - change.getStartRotation());
return target;
}
+
+ /**
+ * Finds the "correct" root idx for a change. The change's end display is prioritized, then
+ * the start display. If there is no display, it will fallback on the 0th root in the
+ * transition. There MUST be at-least 1 root in the transition (ie. it's not a no-op).
+ */
+ public static int rootIndexFor(@NonNull TransitionInfo.Change change,
+ @NonNull TransitionInfo info) {
+ int rootIdx = info.findRootIndex(change.getEndDisplayId());
+ if (rootIdx >= 0) return rootIdx;
+ rootIdx = info.findRootIndex(change.getStartDisplayId());
+ if (rootIdx >= 0) return rootIdx;
+ return 0;
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java
index 35c374d..26b787f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java
@@ -30,6 +30,7 @@
*/
public class TransitionInfoBuilder {
final TransitionInfo mInfo;
+ static final int DISPLAY_ID = 0;
public TransitionInfoBuilder(@WindowManager.TransitionType int type) {
this(type, 0 /* flags */);
@@ -38,7 +39,7 @@
public TransitionInfoBuilder(@WindowManager.TransitionType int type,
@WindowManager.TransitionFlags int flags) {
mInfo = new TransitionInfo(type, flags);
- mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0);
+ mInfo.addRootLeash(DISPLAY_ID, createMockSurface(true /* valid */), 0, 0);
}
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
@@ -61,6 +62,7 @@
}
public TransitionInfoBuilder addChange(TransitionInfo.Change change) {
+ change.setDisplayId(DISPLAY_ID, DISPLAY_ID);
mInfo.addChange(change);
return this;
}
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index 5bc8c04..0a0a626 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -436,7 +436,7 @@
if (mEventHandler != null) {
mEventHandler.sendMessage(
mEventHandler.obtainMessage(
- EventHandler.MSG_CAS_EVENT, event, arg, data));
+ EventHandler.MSG_CAS_EVENT, event, arg, toBytes(data)));
}
}
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index 9c629bb..b1b7d40 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -1210,7 +1210,7 @@
/**
* A key describing the desired bitrate mode to be used by an encoder.
- * Constants are declared in {@link MediaCodecInfo.CodecCapabilities}.
+ * Constants are declared in {@link MediaCodecInfo.EncoderCapabilities}.
*
* @see MediaCodecInfo.EncoderCapabilities#isBitrateModeSupported(int)
*/
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt
index 6946e6b..03e1e66 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt
@@ -59,12 +59,14 @@
// changes should be ordered top-to-bottom in z
val mode = change.mode
+ var rootIdx = info.findRootIndex(change.endDisplayId)
+ if (rootIdx < 0) rootIdx = 0
// Launcher animates leaf tasks directly, so always reparent all task leashes to root.
- t.reparent(leash, info.rootLeash)
+ t.reparent(leash, info.getRoot(rootIdx).leash)
t.setPosition(
leash,
- (change.startAbsBounds.left - info.rootOffset.x).toFloat(),
- (change.startAbsBounds.top - info.rootOffset.y).toFloat()
+ (change.startAbsBounds.left - info.getRoot(rootIdx).offset.x).toFloat(),
+ (change.startAbsBounds.top - info.getRoot(rootIdx).offset.y).toFloat()
)
t.show(leash)
// Put all the OPEN/SHOW on top
@@ -114,8 +116,11 @@
.setName(change.leash.toString() + "_transition-leash")
.setContainerLayer()
.setParent(
- if (change.parent == null) info.rootLeash
- else info.getChange(change.parent!!)!!.leash
+ if (change.parent == null) {
+ var rootIdx = info.findRootIndex(change.endDisplayId)
+ if (rootIdx < 0) rootIdx = 0
+ info.getRoot(rootIdx).leash
+ } else info.getChange(change.parent!!)!!.leash
)
.build()
// Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index 1d28c63..c0b69c1 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -189,10 +189,12 @@
/** Get the text for secondaryLabel. */
public String getSecondaryLabel(String stateText) {
- if (TextUtils.isEmpty(secondaryLabel)) {
+ // Use a local reference as the value might change from other threads
+ CharSequence localSecondaryLabel = secondaryLabel;
+ if (TextUtils.isEmpty(localSecondaryLabel)) {
return stateText;
}
- return secondaryLabel.toString();
+ return localSecondaryLabel.toString();
}
public boolean copyTo(State other) {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
index 6f7d66d..58e7747 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
@@ -321,7 +321,9 @@
} else {
// We are receiving new opening tasks, so convert to onTasksAppeared.
targets[i] = TransitionUtil.newTarget(change, layer, info, t, mLeashMap);
- t.reparent(targets[i].leash, mInfo.getRootLeash());
+ // reparent into the original `mInfo` since that's where we are animating.
+ final int rootIdx = TransitionUtil.rootIndexFor(change, mInfo);
+ t.reparent(targets[i].leash, mInfo.getRoot(rootIdx).getLeash());
t.setLayer(targets[i].leash, layer);
mOpeningTasks.add(new TaskState(change, targets[i].leash));
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
index 50722d5..6d95882 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
@@ -26,18 +26,21 @@
import android.util.Log
import androidx.annotation.VisibleForTesting
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.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
@SysUISingleton
class KeyguardRemotePreviewManager
@Inject
constructor(
private val previewRendererFactory: KeyguardPreviewRendererFactory,
+ @Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
@Background private val backgroundHandler: Handler,
) {
@@ -55,7 +58,13 @@
// Destroy any previous renderer associated with this token.
activePreviews[renderer.hostToken]?.let { destroyObserver(it) }
- observer = PreviewLifecycleObserver(renderer, mainDispatcher, ::destroyObserver)
+ observer =
+ PreviewLifecycleObserver(
+ renderer,
+ applicationScope,
+ mainDispatcher,
+ ::destroyObserver,
+ )
activePreviews[renderer.hostToken] = observer
renderer.render()
renderer.hostToken?.linkToDeath(observer, 0)
@@ -92,13 +101,18 @@
private class PreviewLifecycleObserver(
private val renderer: KeyguardPreviewRenderer,
+ private val scope: CoroutineScope,
private val mainDispatcher: CoroutineDispatcher,
private val requestDestruction: (PreviewLifecycleObserver) -> Unit,
) : Handler.Callback, IBinder.DeathRecipient {
- private var isDestroyed = false
+ private var isDestroyedOrDestroying = false
override fun handleMessage(message: Message): Boolean {
+ if (isDestroyedOrDestroying) {
+ return true
+ }
+
when (message.what) {
KeyguardQuickAffordancePreviewConstants.MESSAGE_ID_SLOT_SELECTED -> {
message.data
@@ -118,14 +132,14 @@
}
fun onDestroy(): IBinder? {
- if (isDestroyed) {
+ if (isDestroyedOrDestroying) {
return null
}
- isDestroyed = true
+ isDestroyedOrDestroying = true
val hostToken = renderer.hostToken
hostToken?.unlinkToDeath(this, 0)
- runBlocking(mainDispatcher) { renderer.destroy() }
+ scope.launch(mainDispatcher) { renderer.destroy() }
return hostToken
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 1004ec1..4d0e746 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -389,11 +389,11 @@
// First check whether this notification should launch a full screen intent, and
// launch it if needed.
val fsiDecision = mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
- if (fsiDecision != null && fsiDecision.shouldLaunch) {
- mNotificationInterruptStateProvider.logFullScreenIntentDecision(entry, fsiDecision)
+ mNotificationInterruptStateProvider.logFullScreenIntentDecision(entry, fsiDecision)
+ if (fsiDecision.shouldLaunch) {
mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
} else if (mFlags.fsiOnDNDUpdate() &&
- fsiDecision.equals(FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)) {
+ fsiDecision == FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND) {
// If DND was the only reason this entry was suppressed, note it for potential
// reconsideration on later ranking updates.
addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
@@ -514,14 +514,24 @@
mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
if (decision.shouldLaunch) {
// Log both the launch of the full screen and also that this was via a
- // ranking update.
- mLogger.logEntryUpdatedToFullScreen(entry.key)
+ // ranking update, and finally revoke candidacy for FSI reconsideration
+ mLogger.logEntryUpdatedToFullScreen(entry.key, decision.name)
mNotificationInterruptStateProvider.logFullScreenIntentDecision(
entry, decision)
mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+ mFSIUpdateCandidates.remove(entry.key)
// if we launch the FSI then this is no longer a candidate for HUN
continue
+ } else if (decision == FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND) {
+ // decision has not changed; no need to log
+ } else {
+ // some other condition is now blocking FSI; log that and revoke candidacy
+ // for FSI reconsideration
+ mLogger.logEntryDisqualifiedFromFullScreen(entry.key, decision.name)
+ mNotificationInterruptStateProvider.logFullScreenIntentDecision(
+ entry, decision)
+ mFSIUpdateCandidates.remove(entry.key)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 2c6bf6b..e936559 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -70,11 +70,21 @@
})
}
- fun logEntryUpdatedToFullScreen(key: String) {
+ fun logEntryUpdatedToFullScreen(key: String, reason: String) {
buffer.log(TAG, LogLevel.DEBUG, {
str1 = key
+ str2 = reason
}, {
- "updating entry to launch full screen intent: $str1"
+ "updating entry to launch full screen intent: $str1 because $str2"
+ })
+ }
+
+ fun logEntryDisqualifiedFromFullScreen(key: String, reason: String) {
+ buffer.log(TAG, LogLevel.DEBUG, {
+ str1 = key
+ str2 = reason
+ }, {
+ "updated entry no longer qualifies for full screen intent: $str1 because $str2"
})
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
index 9001470..5ba8801 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.interruption;
+import androidx.annotation.NonNull;
+
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
/**
@@ -153,7 +155,8 @@
* @param entry the entry to evaluate
* @return FullScreenIntentDecision representing the decision for whether to show the intent
*/
- FullScreenIntentDecision getFullScreenIntentDecision(NotificationEntry entry);
+ @NonNull
+ FullScreenIntentDecision getFullScreenIntentDecision(@NonNull NotificationEntry entry);
/**
* Write the full screen launch decision for the given entry to logs.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index 9f45b9d..274377f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -35,6 +35,8 @@
import android.service.notification.StatusBarNotification;
import android.util.Log;
+import androidx.annotation.NonNull;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
@@ -232,6 +234,7 @@
// suppressor.
//
// If the entry was not suppressed by DND, just returns the given decision.
+ @NonNull
private FullScreenIntentDecision getDecisionGivenSuppression(FullScreenIntentDecision decision,
boolean suppressedByDND) {
if (suppressedByDND) {
@@ -243,7 +246,7 @@
}
@Override
- public FullScreenIntentDecision getFullScreenIntentDecision(NotificationEntry entry) {
+ public FullScreenIntentDecision getFullScreenIntentDecision(@NonNull NotificationEntry entry) {
if (entry.getSbn().getNotification().fullScreenIntent == null) {
if (entry.isStickyAndNotDemoted()) {
return FullScreenIntentDecision.NO_FSI_SHOW_STICKY_HUN;
@@ -336,52 +339,30 @@
final int uid = entry.getSbn().getUid();
final String packageName = entry.getSbn().getPackageName();
switch (decision) {
- case NO_FSI_SHOW_STICKY_HUN:
- mLogger.logNoFullscreen(entry, "Permission denied, show sticky HUN");
- return;
case NO_FULL_SCREEN_INTENT:
- return;
- case NO_FSI_SUPPRESSED_BY_DND:
- case NO_FSI_SUPPRESSED_ONLY_BY_DND:
- mLogger.logNoFullscreen(entry, "Suppressed by DND");
- return;
- case NO_FSI_NOT_IMPORTANT_ENOUGH:
- mLogger.logNoFullscreen(entry, "Not important enough");
+ // explicitly prevent logging for this (frequent) case
return;
case NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR:
android.util.EventLog.writeEvent(0x534e4554, "231322873", uid,
"groupAlertBehavior");
mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid,
packageName);
- mLogger.logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
- return;
- case FSI_DEVICE_NOT_INTERACTIVE:
- mLogger.logFullscreen(entry, "Device is not interactive");
- return;
- case FSI_DEVICE_IS_DREAMING:
- mLogger.logFullscreen(entry, "Device is dreaming");
- return;
- case FSI_KEYGUARD_SHOWING:
- mLogger.logFullscreen(entry, "Keyguard is showing");
- return;
- case NO_FSI_EXPECTED_TO_HUN:
- mLogger.logNoFullscreen(entry, "Expected to HUN");
- return;
- case FSI_KEYGUARD_OCCLUDED:
- mLogger.logFullscreen(entry,
- "Expected not to HUN while keyguard occluded");
- return;
- case FSI_LOCKED_SHADE:
- mLogger.logFullscreen(entry, "Keyguard is showing and not occluded");
+ mLogger.logNoFullscreenWarning(entry,
+ decision + ": GroupAlertBehavior will prevent HUN");
return;
case NO_FSI_NO_HUN_OR_KEYGUARD:
android.util.EventLog.writeEvent(0x534e4554, "231322873", uid,
"no hun or keyguard");
mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName);
- mLogger.logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard");
+ mLogger.logNoFullscreenWarning(entry,
+ decision + ": Expected not to HUN while not on keyguard");
return;
- case FSI_EXPECTED_NOT_TO_HUN:
- mLogger.logFullscreen(entry, "Expected not to HUN");
+ default:
+ if (decision.shouldLaunch) {
+ mLogger.logFullscreen(entry, decision.name());
+ } else {
+ mLogger.logNoFullscreen(entry, decision.name());
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index f1fc386..f866d65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -18,7 +18,6 @@
import android.content.Context
import android.content.IntentFilter
-import android.telephony.CellSignalStrength
import android.telephony.CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
import android.telephony.CellSignalStrengthCdma
import android.telephony.ServiceState
@@ -28,12 +27,12 @@
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
import android.telephony.TelephonyManager
-import android.telephony.TelephonyManager.ERI_OFF
+import android.telephony.TelephonyManager.ERI_FLASH
+import android.telephony.TelephonyManager.ERI_ON
import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import com.android.settingslib.Utils
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.table.TableLogBuffer
@@ -59,16 +58,14 @@
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.stateIn
/**
@@ -100,8 +97,6 @@
}
}
- private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
-
/**
* This flow defines the single shared connection to system_server via TelephonyCallback. Any
* new callback should be added to this listener and funneled through callbackEvents via a data
@@ -109,9 +104,15 @@
*
* The reason we need to do this is because TelephonyManager limits the number of registered
* listeners per-process, so we don't want to create a new listener for every callback.
+ *
+ * A note on the design for back pressure here: We use the [coalesce] operator here to change
+ * the backpressure strategy to store exactly the last callback event of _each type_ here, as
+ * opposed to the default strategy which is to drop the oldest event (regardless of type). This
+ * means that we should never miss any single event as long as the flow has been started.
*/
- private val callbackEvents: SharedFlow<CallbackEvent> =
- conflatedCallbackFlow {
+ private val callbackEvents: StateFlow<TelephonyCallbackState> = run {
+ val initial = TelephonyCallbackState()
+ callbackFlow {
val callback =
object :
TelephonyCallback(),
@@ -165,48 +166,50 @@
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
- .shareIn(scope, SharingStarted.WhileSubscribed())
+ .scan(initial = initial) { state, event -> state.applyEvent(event) }
+ .stateIn(scope = scope, started = SharingStarted.WhileSubscribed(), initial)
+ }
override val isEmergencyOnly =
callbackEvents
- .filterIsInstance<CallbackEvent.OnServiceStateChanged>()
+ .mapNotNull { it.onServiceStateChanged }
.map { it.serviceState.isEmergencyOnly }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val isRoaming =
callbackEvents
- .filterIsInstance<CallbackEvent.OnServiceStateChanged>()
+ .mapNotNull { it.onServiceStateChanged }
.map { it.serviceState.roaming }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val operatorAlphaShort =
callbackEvents
- .filterIsInstance<CallbackEvent.OnServiceStateChanged>()
+ .mapNotNull { it.onServiceStateChanged }
.map { it.serviceState.operatorAlphaShort }
.stateIn(scope, SharingStarted.WhileSubscribed(), null)
override val isInService =
callbackEvents
- .filterIsInstance<CallbackEvent.OnServiceStateChanged>()
+ .mapNotNull { it.onServiceStateChanged }
.map { Utils.isInService(it.serviceState) }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val isGsm =
callbackEvents
- .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>()
+ .mapNotNull { it.onSignalStrengthChanged }
.map { it.signalStrength.isGsm }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val cdmaLevel =
callbackEvents
- .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>()
+ .mapNotNull { it.onSignalStrengthChanged }
.map {
it.signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java).let {
strengths ->
if (strengths.isNotEmpty()) {
strengths[0].level
} else {
- CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+ SIGNAL_STRENGTH_NONE_OR_UNKNOWN
}
}
}
@@ -214,19 +217,19 @@
override val primaryLevel =
callbackEvents
- .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>()
+ .mapNotNull { it.onSignalStrengthChanged }
.map { it.signalStrength.level }
.stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
override val dataConnectionState =
callbackEvents
- .filterIsInstance<CallbackEvent.OnDataConnectionStateChanged>()
+ .mapNotNull { it.onDataConnectionStateChanged }
.map { it.dataState.toDataConnectionType() }
.stateIn(scope, SharingStarted.WhileSubscribed(), Disconnected)
override val dataActivityDirection =
callbackEvents
- .filterIsInstance<CallbackEvent.OnDataActivity>()
+ .mapNotNull { it.onDataActivity }
.map { it.direction.toMobileDataActivityModel() }
.stateIn(
scope,
@@ -236,28 +239,26 @@
override val carrierNetworkChangeActive =
callbackEvents
- .filterIsInstance<CallbackEvent.OnCarrierNetworkChange>()
+ .mapNotNull { it.onCarrierNetworkChange }
.map { it.active }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val resolvedNetworkType =
callbackEvents
- .filterIsInstance<CallbackEvent.OnDisplayInfoChanged>()
+ .mapNotNull { it.onDisplayInfoChanged }
.map {
- if (it.telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) {
- UnknownNetworkType
- } else if (
- it.telephonyDisplayInfo.overrideNetworkType == OVERRIDE_NETWORK_TYPE_NONE
- ) {
- DefaultNetworkType(
- mobileMappingsProxy.toIconKey(it.telephonyDisplayInfo.networkType)
- )
- } else {
+ if (it.telephonyDisplayInfo.overrideNetworkType != OVERRIDE_NETWORK_TYPE_NONE) {
OverrideNetworkType(
mobileMappingsProxy.toIconKeyOverride(
it.telephonyDisplayInfo.overrideNetworkType
)
)
+ } else if (it.telephonyDisplayInfo.networkType != NETWORK_TYPE_UNKNOWN) {
+ DefaultNetworkType(
+ mobileMappingsProxy.toIconKey(it.telephonyDisplayInfo.networkType)
+ )
+ } else {
+ UnknownNetworkType
}
}
.stateIn(scope, SharingStarted.WhileSubscribed(), UnknownNetworkType)
@@ -282,7 +283,10 @@
override val cdmaRoaming: StateFlow<Boolean> =
telephonyPollingEvent
- .mapLatest { telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber != ERI_OFF }
+ .mapLatest {
+ val cdmaEri = telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber
+ cdmaEri == ERI_ON || cdmaEri == ERI_FLASH
+ }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val networkName: StateFlow<NetworkNameModel> =
@@ -300,7 +304,8 @@
override val dataEnabled = run {
val initial = telephonyManager.isDataConnectionAllowed
callbackEvents
- .mapNotNull { (it as? CallbackEvent.OnDataEnabledChanged)?.enabled }
+ .mapNotNull { it.onDataEnabledChanged }
+ .map { it.enabled }
.stateIn(scope, SharingStarted.WhileSubscribed(), initial)
}
@@ -344,12 +349,41 @@
* Wrap every [TelephonyCallback] we care about in a data class so we can accept them in a single
* shared flow and then split them back out into other flows.
*/
-private sealed interface CallbackEvent {
+sealed interface CallbackEvent {
+ data class OnCarrierNetworkChange(val active: Boolean) : CallbackEvent
+ data class OnDataActivity(val direction: Int) : CallbackEvent
+ data class OnDataConnectionStateChanged(val dataState: Int) : CallbackEvent
+ data class OnDataEnabledChanged(val enabled: Boolean) : CallbackEvent
+ data class OnDisplayInfoChanged(val telephonyDisplayInfo: TelephonyDisplayInfo) : CallbackEvent
data class OnServiceStateChanged(val serviceState: ServiceState) : CallbackEvent
data class OnSignalStrengthChanged(val signalStrength: SignalStrength) : CallbackEvent
- data class OnDataConnectionStateChanged(val dataState: Int) : CallbackEvent
- data class OnDataActivity(val direction: Int) : CallbackEvent
- data class OnCarrierNetworkChange(val active: Boolean) : CallbackEvent
- data class OnDisplayInfoChanged(val telephonyDisplayInfo: TelephonyDisplayInfo) : CallbackEvent
- data class OnDataEnabledChanged(val enabled: Boolean) : CallbackEvent
+}
+
+/**
+ * A simple box type for 1-to-1 mapping of [CallbackEvent] to the batched event. Used in conjunction
+ * with [scan] to make sure we don't drop important callbacks due to late subscribers
+ */
+data class TelephonyCallbackState(
+ val onDataActivity: CallbackEvent.OnDataActivity? = null,
+ val onCarrierNetworkChange: CallbackEvent.OnCarrierNetworkChange? = null,
+ val onDataConnectionStateChanged: CallbackEvent.OnDataConnectionStateChanged? = null,
+ val onDataEnabledChanged: CallbackEvent.OnDataEnabledChanged? = null,
+ val onDisplayInfoChanged: CallbackEvent.OnDisplayInfoChanged? = null,
+ val onServiceStateChanged: CallbackEvent.OnServiceStateChanged? = null,
+ val onSignalStrengthChanged: CallbackEvent.OnSignalStrengthChanged? = null,
+) {
+ fun applyEvent(event: CallbackEvent): TelephonyCallbackState {
+ return when (event) {
+ is CallbackEvent.OnCarrierNetworkChange -> copy(onCarrierNetworkChange = event)
+ is CallbackEvent.OnDataActivity -> copy(onDataActivity = event)
+ is CallbackEvent.OnDataConnectionStateChanged ->
+ copy(onDataConnectionStateChanged = event)
+ is CallbackEvent.OnDataEnabledChanged -> copy(onDataEnabledChanged = event)
+ is CallbackEvent.OnDisplayInfoChanged -> copy(onDisplayInfoChanged = event)
+ is CallbackEvent.OnServiceStateChanged -> {
+ copy(onServiceStateChanged = event)
+ }
+ is CallbackEvent.OnSignalStrengthChanged -> copy(onSignalStrengthChanged = event)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 3c7d092..95cc12a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -1416,11 +1416,7 @@
@Override
public void onAnimationCancel(@NonNull Animator animation) {
mInteractionJankMonitor.cancel(CUJ_VOLUME_CONTROL);
- Log.i(TAG, "onAnimationCancel");
-
- // We can only have one animation listener for cancel, so the jank listener should
- // also call for cleanup.
- finishDismiss();
+ Log.d(TAG, "onAnimationCancel");
}
@Override
@@ -1529,7 +1525,12 @@
.setDuration(mDialogHideAnimationDurationMs)
.setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
.withEndAction(() -> mHandler.postDelayed(() -> {
- finishDismiss();
+ mController.notifyVisible(false);
+ mDialog.dismiss();
+ tryToRemoveCaptionsTooltip();
+ mIsAnimatingDismiss = false;
+
+ hideRingerDrawer();
}, 50));
if (!shouldSlideInVolumeTray()) {
animator.translationX(
@@ -1547,18 +1548,6 @@
Trace.endSection();
}
- /**
- * Clean up and hide volume dialog. Called when animation is finished/cancelled.
- */
- private void finishDismiss() {
- mController.notifyVisible(false);
- mDialog.dismiss();
- tryToRemoveCaptionsTooltip();
- mIsAnimatingDismiss = false;
-
- hideRingerDrawer();
- }
-
private boolean showActiveStreamOnly() {
return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|| mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
index 984f4be..1044131 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -192,6 +192,7 @@
)
underTest.previewManager =
KeyguardRemotePreviewManager(
+ applicationScope = testScope.backgroundScope,
previewRendererFactory = previewRendererFactory,
mainDispatcher = testDispatcher,
backgroundHandler = backgroundHandler,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index 64e58d0..0e2a3ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
@@ -134,7 +134,7 @@
TransitionInfoBuilder(@WindowManager.TransitionType int type) {
mInfo = new TransitionInfo(type, 0 /* flags */);
- mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0);
+ mInfo.addRootLeash(0, createMockSurface(true /* valid */), 0, 0);
}
TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
@@ -144,6 +144,7 @@
change.setMode(mode);
change.setFlags(flags);
change.setTaskInfo(taskInfo);
+ change.setDisplayId(0, 0);
mInfo.addChange(change);
return this;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index cb4f119..4bb14a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -22,6 +22,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.logcatLogBuffer
+import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
@@ -58,6 +59,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyString
+import org.mockito.BDDMockito.clearInvocations
import org.mockito.BDDMockito.given
import org.mockito.Mockito.never
import org.mockito.Mockito.times
@@ -166,6 +168,12 @@
mGroupChild1 = mHelper.createChildNotification(GROUP_ALERT_ALL, 1, "child", 350)
mGroupChild2 = mHelper.createChildNotification(GROUP_ALERT_ALL, 2, "child", 250)
mGroupChild3 = mHelper.createChildNotification(GROUP_ALERT_ALL, 3, "child", 150)
+
+ // Set the default FSI decision
+ setShouldFullScreen(any(), FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
+
+ // Run tests with default feature flag state
+ whenever(mFlags.fsiOnDNDUpdate()).thenReturn(Flags.FSI_ON_DND_UPDATE.default)
}
@Test
@@ -810,6 +818,39 @@
}
@Test
+ fun onEntryAdded_whenLaunchingFSI_doesLogDecision() {
+ // GIVEN A new notification can FSI
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(
+ mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ }
+
+ @Test
+ fun onEntryAdded_whenNotLaunchingFSI_doesLogDecision() {
+ // GIVEN A new notification can't FSI
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(
+ mEntry, FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
+ }
+
+ @Test
+ fun onEntryAdded_whenNotLaunchingFSIBecauseOfDnd_doesLogDecision() {
+ // GIVEN A new notification can't FSI because of DND
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(
+ mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ }
+
+ @Test
fun testOnRankingApplied_noFSIOnUpdateWhenFlagOff() {
// Ensure the feature flag is off
whenever(mFlags.fsiOnDNDUpdate()).thenReturn(false)
@@ -818,13 +859,22 @@
setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
mCollectionListener.onEntryAdded(mEntry)
+ // Verify that this causes a log
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(
+ mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ clearInvocations(mNotificationInterruptStateProvider)
+
// and it is then updated to allow full screen
setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
mCollectionListener.onRankingApplied()
// THEN it should not full screen because the feature is off
- verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+
+ // VERIFY that no additional logging happens either
+ verify(mNotificationInterruptStateProvider, never())
+ .logFullScreenIntentDecision(any(), any())
}
@Test
@@ -836,8 +886,11 @@
setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
mCollectionListener.onEntryAdded(mEntry)
- // at this point, it should not have full screened
- verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+ // at this point, it should not have full screened, but should have logged
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(mEntry,
+ FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ clearInvocations(mNotificationInterruptStateProvider)
// and it is then updated to allow full screen AND HUN
setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
@@ -847,10 +900,110 @@
mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
- // THEN it should full screen but it should NOT HUN
+ // THEN it should full screen and log but it should NOT HUN
verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
verify(mHeadsUpManager, never()).showNotification(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(mEntry,
+ FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ clearInvocations(mNotificationInterruptStateProvider)
+
+ // WHEN ranking updates again and the pipeline reruns
+ clearInvocations(mLaunchFullScreenIntentProvider)
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // VERIFY that the FSI does not launch again or log
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mNotificationInterruptStateProvider, never())
+ .logFullScreenIntentDecision(any(), any())
+ }
+
+ @Test
+ fun testOnRankingApplied_withOnlyDndSuppressionAllowsFsiLater() {
+ // Turn on the feature
+ whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
+
+ // GIVEN that mEntry was previously suppressed from full-screen only by DND
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // at this point, it should not have full screened, but should have logged
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(mEntry,
+ FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ clearInvocations(mNotificationInterruptStateProvider)
+
+ // ranking is applied with only DND blocking FSI
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN it should still not yet full screen or HUN
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+
+ // Same decision as before; is not logged
+ verify(mNotificationInterruptStateProvider, never())
+ .logFullScreenIntentDecision(any(), any())
+ clearInvocations(mNotificationInterruptStateProvider)
+
+ // and it is then updated to allow full screen AND HUN
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ setShouldHeadsUp(mEntry)
+ whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN it should full screen and log but it should NOT HUN
+ verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+ verify(mNotificationInterruptStateProvider).logFullScreenIntentDecision(mEntry,
+ FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ clearInvocations(mNotificationInterruptStateProvider)
+ }
+
+ @Test
+ fun testOnRankingApplied_newNonFullScreenAnswerInvalidatesCandidate() {
+ // Turn on the feature
+ whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
+
+ // GIVEN that mEntry was previously suppressed from full-screen only by DND
+ whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // at this point, it should not have full screened
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+
+ // now some other condition blocks FSI in addition to DND
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_BY_DND)
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN it should NOT full screen or HUN
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+
+ // NOW the DND logic changes and FSI and HUN are available
+ clearInvocations(mLaunchFullScreenIntentProvider)
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ setShouldHeadsUp(mEntry)
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // VERIFY that the FSI didn't happen, but that we do HUN
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+ finishBind(mEntry)
+ verify(mHeadsUpManager).showNotification(mEntry)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 07d0dbd..8acf507 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -36,9 +36,12 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
@@ -76,6 +79,10 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
/**
* Tests for the interruption state provider which understands whether the system & notification
* is in a state allowing a particular notification to hun, pulse, or bubble.
@@ -560,7 +567,7 @@
.isFalse();
verify(mLogger, never()).logFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logNoFullscreen(entry, "Suppressed by DND");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_SUPPRESSED_ONLY_BY_DND");
}
@Test
@@ -579,7 +586,7 @@
.isFalse();
verify(mLogger, never()).logFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logNoFullscreen(entry, "Suppressed by DND");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_SUPPRESSED_BY_DND");
}
@Test
@@ -599,7 +606,7 @@
.isEqualTo(FullScreenIntentDecision.NO_FSI_NOT_IMPORTANT_ENOUGH);
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
- verify(mLogger).logNoFullscreen(entry, "Not important enough");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_NOT_IMPORTANT_ENOUGH");
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
verify(mLogger, never()).logFullscreen(any(), any());
}
@@ -622,7 +629,8 @@
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
verify(mLogger, never()).logNoFullscreen(any(), any());
- verify(mLogger).logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
+ verify(mLogger).logNoFullscreenWarning(entry,
+ "NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR: GroupAlertBehavior will prevent HUN");
verify(mLogger, never()).logFullscreen(any(), any());
assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
@@ -652,7 +660,7 @@
.isTrue();
verify(mLogger, never()).logNoFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logFullscreen(entry, "Device is not interactive");
+ verify(mLogger).logFullscreen(entry, "FSI_DEVICE_NOT_INTERACTIVE");
}
@Test
@@ -674,7 +682,7 @@
.isTrue();
verify(mLogger, never()).logNoFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logFullscreen(entry, "Device is dreaming");
+ verify(mLogger).logFullscreen(entry, "FSI_DEVICE_IS_DREAMING");
}
@Test
@@ -696,7 +704,7 @@
.isTrue();
verify(mLogger, never()).logNoFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logFullscreen(entry, "Keyguard is showing");
+ verify(mLogger).logFullscreen(entry, "FSI_KEYGUARD_SHOWING");
}
@Test
@@ -717,7 +725,7 @@
.isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
- verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_EXPECTED_TO_HUN");
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
verify(mLogger, never()).logFullscreen(any(), any());
}
@@ -737,7 +745,7 @@
.isTrue();
verify(mLogger, never()).logNoFullscreen(any(), any());
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
- verify(mLogger).logFullscreen(entry, "Expected not to HUN");
+ verify(mLogger).logFullscreen(entry, "FSI_EXPECTED_NOT_TO_HUN");
}
@Test
@@ -756,7 +764,7 @@
.isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
- verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_EXPECTED_TO_HUN");
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
verify(mLogger, never()).logFullscreen(any(), any());
}
@@ -802,7 +810,7 @@
.isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
- verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_EXPECTED_TO_HUN");
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
verify(mLogger, never()).logFullscreen(any(), any());
}
@@ -848,12 +856,37 @@
.isEqualTo(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN);
assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.isFalse();
- verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
+ verify(mLogger).logNoFullscreen(entry, "NO_FSI_EXPECTED_TO_HUN");
verify(mLogger, never()).logNoFullscreenWarning(any(), any());
verify(mLogger, never()).logFullscreen(any(), any());
}
@Test
+ public void logFullScreenIntentDecision_shouldAlmostAlwaysLogOneTime() {
+ NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+ Set<FullScreenIntentDecision> warnings = new HashSet<>(Arrays.asList(
+ FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR,
+ FullScreenIntentDecision.NO_FSI_NO_HUN_OR_KEYGUARD
+ ));
+ for (FullScreenIntentDecision decision : FullScreenIntentDecision.values()) {
+ clearInvocations(mLogger);
+ boolean expectedToLog = decision != FullScreenIntentDecision.NO_FULL_SCREEN_INTENT;
+ boolean isWarning = warnings.contains(decision);
+ mNotifInterruptionStateProvider.logFullScreenIntentDecision(entry, decision);
+ if (decision.shouldLaunch) {
+ verify(mLogger).logFullscreen(eq(entry), contains(decision.name()));
+ } else if (expectedToLog) {
+ if (isWarning) {
+ verify(mLogger).logNoFullscreenWarning(eq(entry), contains(decision.name()));
+ } else {
+ verify(mLogger).logNoFullscreen(eq(entry), contains(decision.name()));
+ }
+ }
+ verifyNoMoreInteractions(mLogger);
+ }
+ }
+
+ @Test
public void testShouldHeadsUp_snoozed_unlocked_withStrictRules() throws Exception {
when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true);
NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index f6e5959..542b688 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -18,17 +18,16 @@
import android.content.Intent
import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL
-import android.telephony.CellSignalStrengthCdma
import android.telephony.NetworkRegistrationInfo
import android.telephony.ServiceState
import android.telephony.ServiceState.STATE_IN_SERVICE
import android.telephony.ServiceState.STATE_OUT_OF_SERVICE
-import android.telephony.SignalStrength
import android.telephony.TelephonyCallback
import android.telephony.TelephonyCallback.DataActivityListener
import android.telephony.TelephonyCallback.ServiceStateListener
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
import android.telephony.TelephonyManager
import android.telephony.TelephonyManager.DATA_ACTIVITY_DORMANT
import android.telephony.TelephonyManager.DATA_ACTIVITY_IN
@@ -68,6 +67,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.signalStrength
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
@@ -75,14 +75,12 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.After
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
@@ -99,7 +97,6 @@
@Mock private lateinit var logger: MobileInputLogger
@Mock private lateinit var tableLogger: TableLogBuffer
- private val scope = CoroutineScope(IMMEDIATE)
private val mobileMappings = FakeMobileMappingsProxy()
private val systemUiCarrierConfig =
SystemUiCarrierConfig(
@@ -107,6 +104,9 @@
createTestConfig(),
)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -124,21 +124,16 @@
systemUiCarrierConfig,
fakeBroadcastDispatcher,
mobileMappings,
- IMMEDIATE,
+ testDispatcher,
logger,
tableLogger,
- scope,
+ testScope.backgroundScope,
)
}
- @After
- fun tearDown() {
- scope.cancel()
- }
-
@Test
fun emergencyOnly() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this)
@@ -154,18 +149,15 @@
@Test
fun emergencyOnly_toggles() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this)
val callback = getTelephonyCallbackForType<ServiceStateListener>()
- val serviceState = ServiceState()
- serviceState.isEmergencyOnly = true
- callback.onServiceStateChanged(serviceState)
+ callback.onServiceStateChanged(ServiceState().also { it.isEmergencyOnly = true })
assertThat(latest).isTrue()
- serviceState.isEmergencyOnly = false
- callback.onServiceStateChanged(serviceState)
+ callback.onServiceStateChanged(ServiceState().also { it.isEmergencyOnly = false })
assertThat(latest).isFalse()
@@ -174,7 +166,7 @@
@Test
fun cdmaLevelUpdates() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Int? = null
val job = underTest.cdmaLevel.onEach { latest = it }.launchIn(this)
@@ -194,7 +186,7 @@
@Test
fun gsmLevelUpdates() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Int? = null
val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this)
@@ -214,7 +206,7 @@
@Test
fun isGsm() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isGsm.onEach { latest = it }.launchIn(this)
@@ -234,7 +226,7 @@
@Test
fun dataConnectionState_connected() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -249,7 +241,7 @@
@Test
fun dataConnectionState_connecting() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -264,7 +256,7 @@
@Test
fun dataConnectionState_disconnected() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -279,7 +271,7 @@
@Test
fun dataConnectionState_disconnecting() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -294,7 +286,7 @@
@Test
fun dataConnectionState_suspended() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -309,7 +301,7 @@
@Test
fun dataConnectionState_handoverInProgress() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -324,7 +316,7 @@
@Test
fun dataConnectionState_unknown() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -339,7 +331,7 @@
@Test
fun dataConnectionState_invalid() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataConnectionState? = null
val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
@@ -354,7 +346,7 @@
@Test
fun dataActivity() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: DataActivityModel? = null
val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this)
@@ -368,7 +360,7 @@
@Test
fun carrierNetworkChange() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.carrierNetworkChangeActive.onEach { latest = it }.launchIn(this)
@@ -382,7 +374,7 @@
@Test
fun networkType_default() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: ResolvedNetworkType? = null
val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
@@ -395,7 +387,7 @@
@Test
fun networkType_unknown_hasCorrectKey() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: ResolvedNetworkType? = null
val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
@@ -413,14 +405,19 @@
@Test
fun networkType_updatesUsingDefault() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: ResolvedNetworkType? = null
val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+ val overrideType = OVERRIDE_NETWORK_TYPE_NONE
val type = NETWORK_TYPE_LTE
val expected = DefaultNetworkType(mobileMappings.toIconKey(type))
- val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) }
+ val ti =
+ mock<TelephonyDisplayInfo>().also {
+ whenever(it.overrideNetworkType).thenReturn(overrideType)
+ whenever(it.networkType).thenReturn(type)
+ }
callback.onDisplayInfoChanged(ti)
assertThat(latest).isEqualTo(expected)
@@ -430,7 +427,7 @@
@Test
fun networkType_updatesUsingOverride() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: ResolvedNetworkType? = null
val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
@@ -450,16 +447,38 @@
}
@Test
+ fun networkType_unknownNetworkWithOverride_usesOverrideKey() =
+ testScope.runTest {
+ var latest: ResolvedNetworkType? = null
+ val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+ val unknown = NETWORK_TYPE_UNKNOWN
+ val type = OVERRIDE_NETWORK_TYPE_LTE_CA
+ val expected = OverrideNetworkType(mobileMappings.toIconKeyOverride(type))
+ val ti =
+ mock<TelephonyDisplayInfo>().also {
+ whenever(it.networkType).thenReturn(unknown)
+ whenever(it.overrideNetworkType).thenReturn(type)
+ }
+ callback.onDisplayInfoChanged(ti)
+
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
fun dataEnabled_initial_false() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
assertThat(underTest.dataEnabled.value).isFalse()
}
@Test
- fun `is data enabled - tracks telephony callback`() =
- runBlocking(IMMEDIATE) {
+ fun isDataEnabled_tracksTelephonyCallback() =
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
@@ -479,7 +498,7 @@
@Test
fun numberOfLevels_isDefault() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Int? = null
val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
@@ -489,51 +508,68 @@
}
@Test
- fun `roaming - cdma - queries telephony manager`() =
- runBlocking(IMMEDIATE) {
+ fun roaming_cdma_queriesTelephonyManager() =
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
val cb = getTelephonyCallbackForType<ServiceStateListener>()
- val serviceState = ServiceState()
- serviceState.roaming = false
-
- // CDMA roaming is off, GSM roaming is off
+ // CDMA roaming is off, GSM roaming is on
whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
- cb.onServiceStateChanged(serviceState)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = true })
assertThat(latest).isFalse()
- // CDMA roaming is off, GSM roaming is on
+ // CDMA roaming is on, GSM roaming is off
whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_ON)
- cb.onServiceStateChanged(serviceState)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = false })
assertThat(latest).isTrue()
job.cancel()
}
+ /**
+ * [TelephonyManager.getCdmaEnhancedRoamingIndicatorDisplayNumber] returns -1 if the service is
+ * not running or if there is an error while retrieving the cdma ERI
+ */
@Test
- fun `roaming - gsm - queries service state`() =
- runBlocking(IMMEDIATE) {
+ fun cdmaRoaming_ignoresNegativeOne() =
+ testScope.runTest {
var latest: Boolean? = null
- val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+ val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
val serviceState = ServiceState()
serviceState.roaming = false
val cb = getTelephonyCallbackForType<ServiceStateListener>()
- // CDMA roaming is off, GSM roaming is off
- whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+ // CDMA roaming is unavailable (-1), GSM roaming is off
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(-1)
cb.onServiceStateChanged(serviceState)
assertThat(latest).isFalse()
+ job.cancel()
+ }
+
+ @Test
+ fun roaming_gsm_queriesServiceState() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+
+ val cb = getTelephonyCallbackForType<ServiceStateListener>()
+
+ // CDMA roaming is off, GSM roaming is off
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = false })
+
+ assertThat(latest).isFalse()
+
// CDMA roaming is off, GSM roaming is on
- serviceState.roaming = true
- cb.onServiceStateChanged(serviceState)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = true })
assertThat(latest).isTrue()
@@ -541,8 +577,8 @@
}
@Test
- fun `activity - updates from callback`() =
- runBlocking(IMMEDIATE) {
+ fun activity_updatesFromCallback() =
+ testScope.runTest {
var latest: DataActivityModel? = null
val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this)
@@ -578,8 +614,8 @@
}
@Test
- fun `network name - default`() =
- runBlocking(IMMEDIATE) {
+ fun networkName_default() =
+ testScope.runTest {
var latest: NetworkNameModel? = null
val job = underTest.networkName.onEach { latest = it }.launchIn(this)
@@ -589,8 +625,8 @@
}
@Test
- fun `network name - uses broadcast info - returns derived`() =
- runBlocking(IMMEDIATE) {
+ fun networkName_usesBroadcastInfo_returnsDerived() =
+ testScope.runTest {
var latest: NetworkNameModel? = null
val job = underTest.networkName.onEach { latest = it }.launchIn(this)
@@ -606,8 +642,8 @@
}
@Test
- fun `network name - broadcast not for this sub id - keeps old value`() =
- runBlocking(IMMEDIATE) {
+ fun networkName_broadcastNotForThisSubId_keepsOldValue() =
+ testScope.runTest {
var latest: NetworkNameModel? = null
val job = underTest.networkName.onEach { latest = it }.launchIn(this)
@@ -631,8 +667,8 @@
}
@Test
- fun `network name - broadcast has no data - updates to default`() =
- runBlocking(IMMEDIATE) {
+ fun networkName_broadcastHasNoData_updatesToDefault() =
+ testScope.runTest {
var latest: NetworkNameModel? = null
val job = underTest.networkName.onEach { latest = it }.launchIn(this)
@@ -658,8 +694,8 @@
}
@Test
- fun `operatorAlphaShort - tracked`() =
- runBlocking(IMMEDIATE) {
+ fun operatorAlphaShort_tracked() =
+ testScope.runTest {
var latest: String? = null
val job = underTest.operatorAlphaShort.onEach { latest = it }.launchIn(this)
@@ -680,33 +716,45 @@
}
@Test
- fun `connection model - isInService - not iwlan`() =
- runBlocking(IMMEDIATE) {
+ fun isInService_notIwlan() =
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isInService.onEach { latest = it }.launchIn(this)
- val serviceState = ServiceState()
- serviceState.voiceRegState = STATE_IN_SERVICE
- serviceState.dataRegState = STATE_IN_SERVICE
-
- getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+ getTelephonyCallbackForType<ServiceStateListener>()
+ .onServiceStateChanged(
+ ServiceState().also {
+ it.voiceRegState = STATE_IN_SERVICE
+ it.dataRegState = STATE_IN_SERVICE
+ }
+ )
assertThat(latest).isTrue()
- serviceState.voiceRegState = STATE_OUT_OF_SERVICE
- getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+ getTelephonyCallbackForType<ServiceStateListener>()
+ .onServiceStateChanged(
+ ServiceState().also {
+ it.dataRegState = STATE_IN_SERVICE
+ it.voiceRegState = STATE_OUT_OF_SERVICE
+ }
+ )
assertThat(latest).isTrue()
- serviceState.dataRegState = STATE_OUT_OF_SERVICE
- getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+ getTelephonyCallbackForType<ServiceStateListener>()
+ .onServiceStateChanged(
+ ServiceState().also {
+ it.voiceRegState = STATE_OUT_OF_SERVICE
+ it.dataRegState = STATE_OUT_OF_SERVICE
+ }
+ )
assertThat(latest).isFalse()
job.cancel()
}
@Test
- fun `connection model - isInService - is iwlan - voice out of service - data in service`() =
- runBlocking(IMMEDIATE) {
+ fun isInService_isIwlan_voiceOutOfService_dataInService() =
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isInService.onEach { latest = it }.launchIn(this)
@@ -730,8 +778,8 @@
}
@Test
- fun `number of levels - uses carrier config`() =
- runBlocking(IMMEDIATE) {
+ fun numberOfLevels_usesCarrierConfig() =
+ testScope.runTest {
var latest: Int? = null
val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
@@ -756,19 +804,6 @@
return MobileTelephonyHelpers.getTelephonyCallbackForType(telephonyManager)
}
- /** Convenience constructor for SignalStrength */
- private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
- val signalStrength = mock<SignalStrength>()
- whenever(signalStrength.isGsm).thenReturn(isGsm)
- whenever(signalStrength.level).thenReturn(gsmLevel)
- val cdmaStrength =
- mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
- whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
- .thenReturn(listOf(cdmaStrength))
-
- return signalStrength
- }
-
private fun spnIntent(
subId: Int = SUB_1_ID,
showSpn: Boolean = true,
@@ -785,7 +820,6 @@
}
companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
private const val SUB_1_ID = 1
private val DEFAULT_NAME = NetworkNameModel.Default("default name")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt
new file mode 100644
index 0000000..bbf04ed2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt
@@ -0,0 +1,341 @@
+/*
+ * 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.statusbar.pipeline.mobile.data.repository.prod
+
+import android.telephony.ServiceState
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.CarrierNetworkListener
+import android.telephony.TelephonyCallback.DataActivityListener
+import android.telephony.TelephonyCallback.DataConnectionStateListener
+import android.telephony.TelephonyCallback.DataEnabledListener
+import android.telephony.TelephonyCallback.DisplayInfoListener
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.getTelephonyCallbackForType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.signalStrength
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+/**
+ * Test class to stress test the TelephonyCallbacks that we listen to. In particular, the callbacks
+ * all come back in on a single listener (for reasons defined in the system). This test is built to
+ * ensure that we don't miss any important callbacks.
+ *
+ * Kind of like an interaction test case build just for [TelephonyCallback]
+ *
+ * The list of telephony callbacks we use is: [TelephonyCallback.CarrierNetworkListener]
+ * [TelephonyCallback.DataActivityListener] [TelephonyCallback.DataConnectionStateListener]
+ * [TelephonyCallback.DataEnabledListener] [TelephonyCallback.DisplayInfoListener]
+ * [TelephonyCallback.ServiceStateListener] [TelephonyCallback.SignalStrengthsListener]
+ *
+ * Because each of these callbacks comes in on the same callbackFlow, collecting on a field backed
+ * by only a single callback can immediately create backpressure on the other fields related to a
+ * mobile connection.
+ *
+ * This test should be designed to test _at least_ each individual callback in a smoke-test fashion.
+ * The way we will achieve this is as follows:
+ * 1. Start up a listener (A) collecting on a field which is _not under test_
+ * 2. Send a single event to a telephony callback which supports the field under test (B)
+ * 3. Send many (may be as few as 2) events to the callback backing A to ensure we start seeing
+ * backpressure on other fields NOTE: poor handling of backpressure here would normally cause B
+ * to get dropped
+ * 4. Start up a new collector for B
+ * 5. Assert that B has the state sent in step #2
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileConnectionTelephonySmokeTests : SysuiTestCase() {
+ private lateinit var underTest: MobileConnectionRepositoryImpl
+ private lateinit var connectionsRepo: FakeMobileConnectionsRepository
+
+ @Mock private lateinit var telephonyManager: TelephonyManager
+ @Mock private lateinit var logger: MobileInputLogger
+ @Mock private lateinit var tableLogger: TableLogBuffer
+
+ private val mobileMappings = FakeMobileMappingsProxy()
+ private val systemUiCarrierConfig =
+ SystemUiCarrierConfig(
+ SUB_1_ID,
+ SystemUiCarrierConfigTest.createTestConfig(),
+ )
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
+
+ connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogger)
+
+ underTest =
+ MobileConnectionRepositoryImpl(
+ context,
+ SUB_1_ID,
+ DEFAULT_NAME,
+ SEP,
+ telephonyManager,
+ systemUiCarrierConfig,
+ fakeBroadcastDispatcher,
+ mobileMappings,
+ testDispatcher,
+ logger,
+ tableLogger,
+ testScope.backgroundScope,
+ )
+ }
+
+ @Test
+ fun carrierNetworkChangeListener_noisyActivity() =
+ testScope.runTest {
+ var latest: Boolean? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ val callback = getTelephonyCallbackForType<CarrierNetworkListener>()
+ callback.onCarrierNetworkChange(true)
+
+ flipActivity(100, activityCallback)
+
+ val job = underTest.carrierNetworkChangeActive.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ activityJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun dataActivityLate_noisyDisplayInfo() =
+ testScope.runTest {
+ var latest: DataActivityModel? = null
+
+ // start collecting displayInfo; don't care about the result
+ val displayInfoJob = underTest.resolvedNetworkType.launchIn(this)
+
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+ activityCallback.onDataActivity(DATA_ACTIVITY_INOUT)
+
+ val displayInfoCallback = getTelephonyCallbackForType<DisplayInfoListener>()
+ val type1 = NETWORK_TYPE_UNKNOWN
+ val type2 = NETWORK_TYPE_LTE
+ val t1 =
+ mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type1) }
+ val t2 =
+ mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type2) }
+
+ flipDisplayInfo(100, listOf(t1, t2), displayInfoCallback)
+
+ val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest)
+ .isEqualTo(
+ DataActivityModel(
+ hasActivityIn = true,
+ hasActivityOut = true,
+ )
+ )
+
+ displayInfoJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun dataConnectionStateListener_noisyActivity() =
+ testScope.runTest {
+ var latest: DataConnectionState? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+
+ val connectionCallback = getTelephonyCallbackForType<DataConnectionStateListener>()
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ connectionCallback.onDataConnectionStateChanged(
+ TelephonyManager.DATA_CONNECTED,
+ 200 /* unused */
+ )
+
+ // Send a bunch of events that we don't care about, to overrun the replay buffer
+ flipActivity(100, activityCallback)
+
+ val connectionJob = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(DataConnectionState.Connected)
+
+ activityJob.cancel()
+ connectionJob.cancel()
+ }
+
+ @Test
+ fun dataEnabledLate_noisyActivity() =
+ testScope.runTest {
+ var latest: Boolean? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+
+ val enabledCallback = getTelephonyCallbackForType<DataEnabledListener>()
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ enabledCallback.onDataEnabledChanged(true, 1 /* unused */)
+
+ // Send a bunch of events that we don't care about, to overrun the replay buffer
+ flipActivity(100, activityCallback)
+
+ val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ activityJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun displayInfoLate_noisyActivity() =
+ testScope.runTest {
+ var latest: ResolvedNetworkType? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+
+ val displayInfoCallback = getTelephonyCallbackForType<DisplayInfoListener>()
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ val type = NETWORK_TYPE_LTE
+ val expected = ResolvedNetworkType.DefaultNetworkType(mobileMappings.toIconKey(type))
+ val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) }
+ displayInfoCallback.onDisplayInfoChanged(ti)
+
+ // Send a bunch of events that we don't care about, to overrun the replay buffer
+ flipActivity(100, activityCallback)
+
+ val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(expected)
+
+ activityJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun serviceStateListener_noisyActivity() =
+ testScope.runTest {
+ var latest: Boolean? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+
+ val serviceStateCallback = getTelephonyCallbackForType<ServiceStateListener>()
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ // isEmergencyOnly comes in
+ val serviceState = ServiceState()
+ serviceState.isEmergencyOnly = true
+ serviceStateCallback.onServiceStateChanged(serviceState)
+
+ flipActivity(100, activityCallback)
+
+ val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ activityJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun signalStrengthsListenerLate_noisyActivity() =
+ testScope.runTest {
+ var latest: Int? = null
+
+ // Start collecting data activity; don't care about the result
+ val activityJob = underTest.dataActivityDirection.launchIn(this)
+ val activityCallback = getTelephonyCallbackForType<DataActivityListener>()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
+ val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
+ callback.onSignalStrengthsChanged(strength)
+
+ flipActivity(100, activityCallback)
+
+ val job = underTest.cdmaLevel.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(2)
+
+ activityJob.cancel()
+ job.cancel()
+ }
+
+ private fun flipActivity(
+ times: Int,
+ callback: DataActivityListener,
+ ) {
+ repeat(times) { index -> callback.onDataActivity(index % 4) }
+ }
+
+ private fun flipDisplayInfo(
+ times: Int,
+ infos: List<TelephonyDisplayInfo>,
+ callback: DisplayInfoListener,
+ ) {
+ val len = infos.size
+ repeat(times) { index -> callback.onDisplayInfoChanged(infos[index % len]) }
+ }
+
+ private inline fun <reified T> getTelephonyCallbackForType(): T {
+ return getTelephonyCallbackForType(telephonyManager)
+ }
+
+ companion object {
+ private const val SUB_1_ID = 1
+
+ private val DEFAULT_NAME = NetworkNameModel.Default("default name")
+ private const val SEP = "-"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt
index 621f793..d07b96f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt
@@ -16,10 +16,14 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.SignalStrength
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.mockito.Mockito.verify
@@ -31,6 +35,19 @@
return callbackCaptor.allValues
}
+ /** Convenience constructor for SignalStrength */
+ fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
+ val signalStrength = mock<SignalStrength>()
+ whenever(signalStrength.isGsm).thenReturn(isGsm)
+ whenever(signalStrength.level).thenReturn(gsmLevel)
+ val cdmaStrength =
+ mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
+ whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
+ .thenReturn(listOf(cdmaStrength))
+
+ return signalStrength
+ }
+
inline fun <reified T> getTelephonyCallbackForType(mockTelephonyManager: TelephonyManager): T {
val cbs = getTelephonyCallbacks(mockTelephonyManager).filterIsInstance<T>()
assertThat(cbs.size).isEqualTo(1)
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c74aa7f..2a1e860 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -19729,8 +19729,9 @@
final long identity = Binder.clearCallingIdentity();
try {
return superImpl.apply(code, new AttributionSource(shellUid,
- "com.android.shell", attributionSource.getAttributionTag(),
- attributionSource.getToken(), attributionSource.getNext()),
+ Process.INVALID_PID, "com.android.shell",
+ attributionSource.getAttributionTag(), attributionSource.getToken(),
+ /*renouncedPermissions*/ null, attributionSource.getNext()),
shouldCollectAsyncNotedOp, message, shouldCollectMessage,
skiProxyOperation);
} finally {
@@ -19781,8 +19782,9 @@
final long identity = Binder.clearCallingIdentity();
try {
return superImpl.apply(clientId, code, new AttributionSource(shellUid,
- "com.android.shell", attributionSource.getAttributionTag(),
- attributionSource.getToken(), attributionSource.getNext()),
+ Process.INVALID_PID, "com.android.shell",
+ attributionSource.getAttributionTag(), attributionSource.getToken(),
+ /*renouncedPermissions*/ null, attributionSource.getNext()),
startIfModeDefault, shouldCollectAsyncNotedOp, message,
shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
proxiedAttributionFlags, attributionChainId);
@@ -19806,8 +19808,9 @@
final long identity = Binder.clearCallingIdentity();
try {
superImpl.apply(clientId, code, new AttributionSource(shellUid,
- "com.android.shell", attributionSource.getAttributionTag(),
- attributionSource.getToken(), attributionSource.getNext()),
+ Process.INVALID_PID, "com.android.shell",
+ attributionSource.getAttributionTag(), attributionSource.getToken(),
+ /*renouncedPermissions*/ null, attributionSource.getNext()),
skipProxyOperation);
} finally {
Binder.restoreCallingIdentity(identity);
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index f236a96..d09ca5c 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -30,7 +30,6 @@
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.RequiresNoPermission;
-import android.app.AlarmManager;
import android.app.StatsManager;
import android.app.usage.NetworkStatsManager;
import android.bluetooth.BluetoothActivityEnergyInfo;
@@ -414,18 +413,6 @@
Slog.e(TAG, "Could not register INetworkManagement event observer " + e);
}
- final AlarmManager am = mContext.getSystemService(AlarmManager.class);
- mHandler.post(() -> {
- synchronized (mStats) {
- mStats.setLongPlugInAlarmInterface(new AlarmInterface(am, () -> {
- synchronized (mStats) {
- if (mStats.isOnBattery()) return;
- mStats.maybeResetWhilePluggedInLocked();
- }
- }));
- }
- });
-
synchronized (mPowerStatsLock) {
mPowerStatsInternal = LocalServices.getService(PowerStatsInternal.class);
if (mPowerStatsInternal != null) {
@@ -2529,32 +2516,6 @@
}
}
- final class AlarmInterface implements BatteryStatsImpl.AlarmInterface,
- AlarmManager.OnAlarmListener {
- private AlarmManager mAm;
- private Runnable mOnAlarm;
-
- AlarmInterface(AlarmManager am, Runnable onAlarm) {
- mAm = am;
- mOnAlarm = onAlarm;
- }
-
- @Override
- public void schedule(long rtcTimeMs, long windowLengthMs) {
- mAm.setWindow(AlarmManager.RTC, rtcTimeMs, windowLengthMs, TAG, this, mHandler);
- }
-
- @Override
- public void cancel() {
- mAm.cancel(this);
- }
-
- @Override
- public void onAlarm() {
- mOnAlarm.run();
- }
- }
-
private static native int nativeWaitWakeup(ByteBuffer outBuffer);
private void dumpHelp(PrintWriter pw) {
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index afae08d..2b5f874 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -421,6 +421,8 @@
static class Injector {
protected Context mContext;
+ private ServiceThread mHandlerThread;
+ private Handler mHandler;
public Injector(Context context) {
mContext = context;
@@ -431,14 +433,20 @@
}
public ServiceThread getServiceThread() {
- ServiceThread handlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
- true /*allowIo*/);
- handlerThread.start();
- return handlerThread;
+ if (mHandlerThread == null) {
+ mHandlerThread = new ServiceThread(TAG,
+ Process.THREAD_PRIORITY_BACKGROUND,
+ true /*allowIo*/);
+ mHandlerThread.start();
+ }
+ return mHandlerThread;
}
public Handler getHandler(ServiceThread handlerThread) {
- return new Handler(handlerThread.getLooper());
+ if (mHandler == null) {
+ mHandler = new Handler(handlerThread.getLooper());
+ }
+ return mHandler;
}
public LockSettingsStorage getStorage() {
@@ -519,7 +527,8 @@
public RebootEscrowManager getRebootEscrowManager(RebootEscrowManager.Callbacks callbacks,
LockSettingsStorage storage) {
- return new RebootEscrowManager(mContext, callbacks, storage);
+ return new RebootEscrowManager(mContext, callbacks, storage,
+ getHandler(getServiceThread()));
}
public int binderGetCallingUid() {
diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
index 9b42cfc..e1cd2c5 100644
--- a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
+++ b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
@@ -205,6 +205,8 @@
private final RebootEscrowKeyStoreManager mKeyStoreManager;
+ private final Handler mHandler;
+
PowerManager.WakeLock mWakeLock;
private ConnectivityManager.NetworkCallback mNetworkCallback;
@@ -399,19 +401,21 @@
}
}
- RebootEscrowManager(Context context, Callbacks callbacks, LockSettingsStorage storage) {
- this(new Injector(context, storage), callbacks, storage);
+ RebootEscrowManager(Context context, Callbacks callbacks, LockSettingsStorage storage,
+ Handler handler) {
+ this(new Injector(context, storage), callbacks, storage, handler);
}
@VisibleForTesting
RebootEscrowManager(Injector injector, Callbacks callbacks,
- LockSettingsStorage storage) {
+ LockSettingsStorage storage, Handler handler) {
mInjector = injector;
mCallbacks = callbacks;
mStorage = storage;
mUserManager = injector.getUserManager();
mEventLog = injector.getEventLog();
mKeyStoreManager = injector.getKeyStoreManager();
+ mHandler = handler;
}
/** Wrapper function to set error code serialized through handler, */
@@ -937,7 +941,7 @@
private void setRebootEscrowReady(boolean ready) {
if (mRebootEscrowReady != ready) {
- mRebootEscrowListener.onPreparedForReboot(ready);
+ mHandler.post(() -> mRebootEscrowListener.onPreparedForReboot(ready));
}
mRebootEscrowReady = ready;
}
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index b4d467f..eb37302 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -573,5 +573,6 @@
* @throws UserManager.CheckedUserOperationException if no switchable user can be found
*/
- public abstract @UserIdInt int getBootUser() throws UserManager.CheckedUserOperationException;
+ public abstract @UserIdInt int getBootUser(boolean waitUntilSet)
+ throws UserManager.CheckedUserOperationException;
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 3343172..cde8bd7 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -161,7 +161,9 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -278,6 +280,8 @@
static final int WRITE_USER_MSG = 1;
static final int WRITE_USER_DELAY = 2*1000; // 2 seconds
+ private static final long BOOT_USER_SET_TIMEOUT_MS = 300_000;
+
// Tron counters
private static final String TRON_GUEST_CREATED = "users_guest_created";
private static final String TRON_USER_CREATED = "users_user_created";
@@ -333,6 +337,8 @@
/** Indicates that this is the 1st boot after the system user mode was changed by emulation. */
private boolean mUpdatingSystemUserMode;
+ /** Count down latch to wait while boot user is not set.*/
+ private final CountDownLatch mBootUserLatch = new CountDownLatch(1);
/**
* Internal non-parcelable wrapper for UserInfo that is not exposed to other system apps.
*/
@@ -952,18 +958,62 @@
Slogf.i(LOG_TAG, "setBootUser %d", userId);
mBootUser = userId;
}
+ mBootUserLatch.countDown();
}
@Override
public @UserIdInt int getBootUser() {
checkCreateUsersPermission("Get boot user");
try {
- return mLocalService.getBootUser();
+ return getBootUserUnchecked();
} catch (UserManager.CheckedUserOperationException e) {
throw e.toServiceSpecificException();
}
}
+ private @UserIdInt int getBootUserUnchecked() throws UserManager.CheckedUserOperationException {
+ synchronized (mUsersLock) {
+ if (mBootUser != UserHandle.USER_NULL) {
+ final UserData userData = mUsers.get(mBootUser);
+ if (userData != null && userData.info.supportsSwitchToByUser()) {
+ Slogf.i(LOG_TAG, "Using provided boot user: %d", mBootUser);
+ return mBootUser;
+ } else {
+ Slogf.w(LOG_TAG,
+ "Provided boot user cannot be switched to: %d", mBootUser);
+ }
+ }
+ }
+
+ if (isHeadlessSystemUserMode()) {
+ // Return the previous foreground user, if there is one.
+ final int previousUser = getPreviousFullUserToEnterForeground();
+ if (previousUser != UserHandle.USER_NULL) {
+ Slogf.i(LOG_TAG, "Boot user is previous user %d", previousUser);
+ return previousUser;
+ }
+ // No previous user. Return the first switchable user if there is one.
+ synchronized (mUsersLock) {
+ final int userSize = mUsers.size();
+ for (int i = 0; i < userSize; i++) {
+ final UserData userData = mUsers.valueAt(i);
+ if (userData.info.supportsSwitchToByUser()) {
+ int firstSwitchable = userData.info.id;
+ Slogf.i(LOG_TAG,
+ "Boot user is first switchable user %d", firstSwitchable);
+ return firstSwitchable;
+ }
+ }
+ }
+ // No switchable users found. Uh oh!
+ throw new UserManager.CheckedUserOperationException(
+ "No switchable users found", USER_OPERATION_ERROR_UNKNOWN);
+ }
+ // Not HSUM, return system user.
+ return UserHandle.USER_SYSTEM;
+ }
+
+
@Override
public int getPreviousFullUserToEnterForeground() {
checkQueryOrCreateUsersPermission("get previous user");
@@ -7182,47 +7232,29 @@
}
@Override
- public @UserIdInt int getBootUser() throws UserManager.CheckedUserOperationException {
- synchronized (mUsersLock) {
- // TODO(b/242195409): On Automotive, block if boot user not provided.
- if (mBootUser != UserHandle.USER_NULL) {
- final UserData userData = mUsers.get(mBootUser);
- if (userData != null && userData.info.supportsSwitchToByUser()) {
- Slogf.i(LOG_TAG, "Using provided boot user: %d", mBootUser);
- return mBootUser;
- } else {
- Slogf.w(LOG_TAG,
- "Provided boot user cannot be switched to: %d", mBootUser);
+ public @UserIdInt int getBootUser(boolean waitUntilSet)
+ throws UserManager.CheckedUserOperationException {
+ if (waitUntilSet) {
+ final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+ t.traceBegin("wait-boot-user");
+ try {
+ if (mBootUserLatch.getCount() != 0) {
+ Slogf.d(LOG_TAG,
+ "Sleeping for boot user to be set. "
+ + "Max sleep for Time: %d", BOOT_USER_SET_TIMEOUT_MS);
}
+ if (!mBootUserLatch.await(BOOT_USER_SET_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ Slogf.w(LOG_TAG, "Boot user not set. Timeout: %d",
+ BOOT_USER_SET_TIMEOUT_MS);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Slogf.w(LOG_TAG, e, "InterruptedException during wait for boot user.");
}
+ t.traceEnd();
}
- if (isHeadlessSystemUserMode()) {
- // Return the previous foreground user, if there is one.
- final int previousUser = getPreviousFullUserToEnterForeground();
- if (previousUser != UserHandle.USER_NULL) {
- Slogf.i(LOG_TAG, "Boot user is previous user %d", previousUser);
- return previousUser;
- }
- // No previous user. Return the first switchable user if there is one.
- synchronized (mUsersLock) {
- final int userSize = mUsers.size();
- for (int i = 0; i < userSize; i++) {
- final UserData userData = mUsers.valueAt(i);
- if (userData.info.supportsSwitchToByUser()) {
- int firstSwitchable = userData.info.id;
- Slogf.i(LOG_TAG,
- "Boot user is first switchable user %d", firstSwitchable);
- return firstSwitchable;
- }
- }
- }
- // No switchable users found. Uh oh!
- throw new UserManager.CheckedUserOperationException(
- "No switchable users found", USER_OPERATION_ERROR_UNKNOWN);
- }
- // Not HSUM, return system user.
- return UserHandle.USER_SYSTEM;
+ return getBootUserUnchecked();
}
} // class LocalService
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index bc23020..661715c 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -26,6 +26,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
+import android.app.AlarmManager;
import android.app.usage.NetworkStatsManager;
import android.bluetooth.BluetoothActivityEnergyInfo;
import android.bluetooth.UidTraffic;
@@ -500,14 +501,6 @@
}
- /** Handles calls to AlarmManager */
- public interface AlarmInterface {
- /** Schedule an RTC alarm */
- void schedule(long rtcTimeMs, long windowLengthMs);
- /** Cancel the previously scheduled alarm */
- void cancel();
- }
-
private final PlatformIdleStateCallback mPlatformIdleStateCallback;
private final Runnable mDeferSetCharging = new Runnable() {
@@ -1569,8 +1562,15 @@
@GuardedBy("this")
protected BatteryStatsConfig mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
- @VisibleForTesting
- protected AlarmInterface mLongPlugInAlarmInterface = null;
+ @GuardedBy("this")
+ private AlarmManager mAlarmManager = null;
+
+ private final AlarmManager.OnAlarmListener mLongPlugInAlarmHandler = () ->
+ mHandler.post(() -> {
+ synchronized (BatteryStatsImpl.this) {
+ maybeResetWhilePluggedInLocked();
+ }
+ });
/*
* Holds a SamplingTimer associated with each Resource Power Manager state and voter,
@@ -11061,18 +11061,6 @@
}
/**
- * Injects an AlarmInterface for the long plug in alarm.
- */
- public void setLongPlugInAlarmInterface(AlarmInterface longPlugInAlarmInterface) {
- synchronized (this) {
- mLongPlugInAlarmInterface = longPlugInAlarmInterface;
- if (mBatteryPluggedIn) {
- scheduleNextResetWhilePluggedInCheck();
- }
- }
- }
-
- /**
* Starts tracking CPU time-in-state for threads of the system server process,
* keeping a separate account of threads receiving incoming binder calls.
*/
@@ -14173,6 +14161,7 @@
/**
* Might reset battery stats if conditions are met. Assumed the device is currently plugged in.
*/
+ @VisibleForTesting
@GuardedBy("this")
public void maybeResetWhilePluggedInLocked() {
final long elapsedRealtimeMs = mClock.elapsedRealtime();
@@ -14189,28 +14178,31 @@
@GuardedBy("this")
private void scheduleNextResetWhilePluggedInCheck() {
- if (mLongPlugInAlarmInterface != null) {
- final long timeoutMs = mClock.currentTimeMillis()
- + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS
- * DateUtils.HOUR_IN_MILLIS;
- Calendar nextAlarm = Calendar.getInstance();
- nextAlarm.setTimeInMillis(timeoutMs);
+ if (mAlarmManager == null) return;
+ final long timeoutMs = mClock.currentTimeMillis()
+ + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS
+ * DateUtils.HOUR_IN_MILLIS;
+ Calendar nextAlarm = Calendar.getInstance();
+ nextAlarm.setTimeInMillis(timeoutMs);
- // Find the 2 AM the same day as the end of the minimum duration.
- // This logic does not handle a Daylight Savings transition, or a timezone change
- // while the alarm has been set. The need to reset after a long period while plugged
- // in is not strict enough to warrant a well architected out solution.
- nextAlarm.set(Calendar.MILLISECOND, 0);
- nextAlarm.set(Calendar.SECOND, 0);
- nextAlarm.set(Calendar.MINUTE, 0);
- nextAlarm.set(Calendar.HOUR_OF_DAY, 2);
- long nextTimeMs = nextAlarm.getTimeInMillis();
- if (nextTimeMs < timeoutMs) {
- // The 2AM on the day of the timeout, move on the next day.
- nextTimeMs += DateUtils.DAY_IN_MILLIS;
- }
- mLongPlugInAlarmInterface.schedule(nextTimeMs, DateUtils.HOUR_IN_MILLIS);
+ // Find the 2 AM the same day as the end of the minimum duration.
+ // This logic does not handle a Daylight Savings transition, or a timezone change
+ // while the alarm has been set. The need to reset after a long period while plugged
+ // in is not strict enough to warrant a well architected out solution.
+ nextAlarm.set(Calendar.MILLISECOND, 0);
+ nextAlarm.set(Calendar.SECOND, 0);
+ nextAlarm.set(Calendar.MINUTE, 0);
+ nextAlarm.set(Calendar.HOUR_OF_DAY, 2);
+ long possibleNextTimeMs = nextAlarm.getTimeInMillis();
+ if (possibleNextTimeMs < timeoutMs) {
+ // The 2AM on the day of the timeout, move on the next day.
+ possibleNextTimeMs += DateUtils.DAY_IN_MILLIS;
}
+ final long nextTimeMs = possibleNextTimeMs;
+ final AlarmManager am = mAlarmManager;
+ mHandler.post(() -> am.setWindow(AlarmManager.RTC, nextTimeMs,
+ DateUtils.HOUR_IN_MILLIS,
+ TAG, mLongPlugInAlarmHandler, mHandler));
}
@@ -14339,8 +14331,12 @@
initActiveHistoryEventsLocked(mSecRealtime, mSecUptime);
}
mBatteryPluggedIn = false;
- if (mLongPlugInAlarmInterface != null) {
- mLongPlugInAlarmInterface.cancel();
+ if (mAlarmManager != null) {
+ final AlarmManager am = mAlarmManager;
+ mHandler.post(() -> {
+ // No longer plugged in. Cancel the long plug in alarm.
+ am.cancel(mLongPlugInAlarmHandler);
+ });
}
mHistory.recordBatteryState(mSecRealtime, mSecUptime, level, mBatteryPluggedIn);
mDischargeCurrentLevel = mDischargeUnplugLevel = level;
@@ -15178,6 +15174,14 @@
public void systemServicesReady(Context context) {
mConstants.startObserving(context.getContentResolver());
registerUsbStateReceiver(context);
+
+ synchronized (this) {
+ mAlarmManager = context.getSystemService(AlarmManager.class);
+ if (mBatteryPluggedIn) {
+ // Already plugged in. Schedule the long plug in alarm.
+ scheduleNextResetWhilePluggedInCheck();
+ }
+ }
}
/**
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 606f011..4e0f120 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -675,7 +675,7 @@
* needs to be passed/applied in shell because until finish is called, shell owns the surfaces.
* Additionally, this gives shell the ability to better deal with merged transitions.
*/
- private void buildFinishTransaction(SurfaceControl.Transaction t, SurfaceControl rootLeash) {
+ private void buildFinishTransaction(SurfaceControl.Transaction t, TransitionInfo info) {
final Point tmpPos = new Point();
// usually only size 1
final ArraySet<DisplayContent> displays = new ArraySet<>();
@@ -728,8 +728,8 @@
} finally {
mController.mBuildingFinishLayers = false;
}
- if (rootLeash.isValid()) {
- t.reparent(rootLeash, null);
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ t.reparent(info.getRoot(i).getLeash(), null);
}
}
@@ -1066,13 +1066,6 @@
Slog.e(TAG, "Unexpected Sync ID " + syncId + ". Expected " + mSyncId);
return;
}
- if (mTargetDisplays.isEmpty()) {
- mTargetDisplays.add(mController.mAtm.mRootWindowContainer.getDefaultDisplay());
- }
- // While there can be multiple DC's involved. For now, we just use the first one as
- // the "primary" one for most things. Eventually, this will need to change, but, for the
- // time being, we don't have full cross-display transitions so it isn't a problem.
- final DisplayContent dc = mTargetDisplays.get(0);
// Commit the visibility of visible activities before calculateTransitionInfo(), so the
// TaskInfo can be visible. Also it needs to be done before moveToPlaying(), otherwise
@@ -1082,6 +1075,9 @@
if (mState == STATE_ABORT) {
mController.abort(this);
+ // Fall-back to the default display if there isn't one participating.
+ final DisplayContent dc = !mTargetDisplays.isEmpty() ? mTargetDisplays.get(0)
+ : mController.mAtm.mRootWindowContainer.getDefaultDisplay();
dc.getPendingTransaction().merge(transaction);
mSyncId = -1;
mOverrideOptions = null;
@@ -1094,16 +1090,24 @@
mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get();
mController.moveToPlaying(this);
- if (dc.isKeyguardLocked()) {
- mFlags |= TRANSIT_FLAG_KEYGUARD_LOCKED;
- }
-
// Check whether the participants were animated from back navigation.
final boolean markBackAnimated = mController.mAtm.mBackNavigationController
.containsBackAnimationTargets(this);
- // Resolve the animating targets from the participants
+ // Resolve the animating targets from the participants.
mTargets = calculateTargets(mParticipants, mChanges);
final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction);
+
+ // Repopulate the displays based on the resolved targets.
+ mTargetDisplays.clear();
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ final DisplayContent dc = mController.mAtm.mRootWindowContainer.getDisplayContent(
+ info.getRoot(i).getDisplayId());
+ mTargetDisplays.add(dc);
+ if (dc.isKeyguardLocked()) {
+ mFlags |= TRANSIT_FLAG_KEYGUARD_LOCKED;
+ }
+ }
+
if (markBackAnimated) {
mController.mAtm.mBackNavigationController.clearBackAnimations(mStartTransaction);
}
@@ -1125,9 +1129,12 @@
}
// TODO(b/188669821): Move to animation impl in shell.
- handleLegacyRecentsStartBehavior(dc, info);
+ for (int i = 0; i < mTargetDisplays.size(); ++i) {
+ handleLegacyRecentsStartBehavior(mTargetDisplays.get(i), info);
+ if (mRecentsDisplayId != INVALID_DISPLAY) break;
+ }
- handleNonAppWindowsInTransition(dc, mType, mFlags);
+ handleNonAppWindowsInTransition(mType, mFlags);
// The callback is only populated for custom activity-level client animations
sendRemoteCallback(mClientAnimationStartCallback);
@@ -1191,11 +1198,14 @@
// This is non-null only if display has changes. It handles the visible windows that don't
// need to be participated in the transition.
- final AsyncRotationController controller = dc.getAsyncRotationController();
- if (controller != null && containsChangeFor(dc, mTargets)) {
- controller.setupStartTransaction(transaction);
+ for (int i = 0; i < mTargetDisplays.size(); ++i) {
+ final DisplayContent dc = mTargetDisplays.get(i);
+ final AsyncRotationController controller = dc.getAsyncRotationController();
+ if (controller != null && containsChangeFor(dc, mTargets)) {
+ controller.setupStartTransaction(transaction);
+ }
}
- buildFinishTransaction(mFinishTransaction, info.getRootLeash());
+ buildFinishTransaction(mFinishTransaction, info);
if (mController.getTransitionPlayer() != null && mIsPlayerEnabled) {
mController.dispatchLegacyAppTransitionStarting(info, mStatusBarTransitionDelay);
try {
@@ -1216,10 +1226,13 @@
// client, we should finish and apply it here so the transactions aren't lost.
postCleanupOnFailure();
}
- final AccessibilityController accessibilityController =
- dc.mWmService.mAccessibilityController;
- if (accessibilityController.hasCallbacks()) {
- accessibilityController.onWMTransition(dc.getDisplayId(), mType);
+ for (int i = 0; i < mTargetDisplays.size(); ++i) {
+ final DisplayContent dc = mTargetDisplays.get(i);
+ final AccessibilityController accessibilityController =
+ dc.mWmService.mAccessibilityController;
+ if (accessibilityController.hasCallbacks()) {
+ accessibilityController.onWMTransition(dc.getDisplayId(), mType);
+ }
}
} else {
// No player registered or it's not enabled, so just finish/apply immediately
@@ -1296,16 +1309,15 @@
if ((mFlags & TRANSIT_FLAG_IS_RECENTS) == 0) {
return;
}
- mRecentsDisplayId = dc.mDisplayId;
// Recents has an input-consumer to grab input from the "live tile" app. Set that up here
final InputConsumerImpl recentsAnimationInputConsumer =
dc.getInputMonitor().getInputConsumer(INPUT_CONSUMER_RECENTS_ANIMATION);
+ ActivityRecord recentsActivity = null;
if (recentsAnimationInputConsumer != null) {
// find the top-most going-away activity and the recents activity. The top-most
// is used as layer reference while the recents is used for registering the consumer
// override.
- ActivityRecord recentsActivity = null;
ActivityRecord topActivity = null;
for (int i = 0; i < info.getChanges().size(); ++i) {
final TransitionInfo.Change change = info.getChanges().get(i);
@@ -1329,6 +1341,12 @@
}
}
+ if (recentsActivity == null) {
+ // No recents activity on `dc`, its probably on a different display.
+ return;
+ }
+ mRecentsDisplayId = dc.mDisplayId;
+
// The rest of this function handles nav-bar reparenting
if (!dc.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()
@@ -1423,7 +1441,7 @@
}
}
- private void handleNonAppWindowsInTransition(@NonNull DisplayContent dc,
+ private void handleNonAppWindowsInTransition(
@TransitionType int transit, @TransitionFlags int flags) {
if ((flags & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0) {
// If the occlusion changed but the transition isn't an occlude/unocclude transition,
@@ -1801,6 +1819,41 @@
return wc instanceof DisplayContent;
}
+ private static int getDisplayId(@NonNull WindowContainer wc) {
+ return wc.getDisplayContent() != null
+ ? wc.getDisplayContent().getDisplayId() : INVALID_DISPLAY;
+ }
+
+ @VisibleForTesting
+ static void calculateTransitionRoots(@NonNull TransitionInfo outInfo,
+ ArrayList<ChangeInfo> sortedTargets,
+ @NonNull SurfaceControl.Transaction startT) {
+ // There needs to be a root on each display.
+ for (int i = 0; i < sortedTargets.size(); ++i) {
+ final WindowContainer<?> wc = sortedTargets.get(i).mContainer;
+ // Don't include wallpapers since they are in a different DA.
+ if (isWallpaper(wc)) continue;
+ final int endDisplayId = getDisplayId(wc);
+ if (endDisplayId < 0) continue;
+
+ // Check if Root was already created for this display with a higher-Z window
+ if (outInfo.findRootIndex(endDisplayId) >= 0) continue;
+
+ WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, wc);
+
+ // Make leash based on highest (z-order) direct child of ancestor with a participant.
+ WindowContainer leashReference = wc;
+ while (leashReference.getParent() != ancestor) {
+ leashReference = leashReference.getParent();
+ }
+ final SurfaceControl rootLeash = leashReference.makeAnimationLeash().setName(
+ "Transition Root: " + leashReference.getName()).build();
+ startT.setLayer(rootLeash, leashReference.getLastLayer());
+ outInfo.addRootLeash(endDisplayId, rootLeash,
+ ancestor.getBounds().left, ancestor.getBounds().top);
+ }
+ }
+
/**
* Construct a TransitionInfo object from a set of targets and changes. Also populates the
* root surface.
@@ -1811,37 +1864,13 @@
@NonNull
static TransitionInfo calculateTransitionInfo(@TransitionType int type, int flags,
ArrayList<ChangeInfo> sortedTargets,
- @Nullable SurfaceControl.Transaction startT) {
+ @NonNull SurfaceControl.Transaction startT) {
final TransitionInfo out = new TransitionInfo(type, flags);
-
- WindowContainer<?> topApp = null;
- for (int i = 0; i < sortedTargets.size(); i++) {
- final WindowContainer<?> wc = sortedTargets.get(i).mContainer;
- if (!isWallpaper(wc)) {
- topApp = wc;
- break;
- }
- }
- if (topApp == null) {
- out.setRootLeash(new SurfaceControl(), 0, 0);
+ calculateTransitionRoots(out, sortedTargets, startT);
+ if (out.getRootCount() == 0) {
return out;
}
- WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, topApp);
-
- // Make leash based on highest (z-order) direct child of ancestor with a participant.
- // TODO(b/261418859): Handle the case when the target contains window containers which
- // belong to a different display. As a workaround we use topApp, from which wallpaper
- // window container is removed, instead of sortedTargets here.
- WindowContainer leashReference = topApp;
- while (leashReference.getParent() != ancestor) {
- leashReference = leashReference.getParent();
- }
- final SurfaceControl rootLeash = leashReference.makeAnimationLeash().setName(
- "Transition Root: " + leashReference.getName()).build();
- startT.setLayer(rootLeash, leashReference.getLastLayer());
- out.setRootLeash(rootLeash, ancestor.getBounds().left, ancestor.getBounds().top);
-
// Convert all the resolved ChangeInfos into TransactionInfo.Change objects in order.
final int count = sortedTargets.size();
for (int i = 0; i < count; ++i) {
@@ -1861,6 +1890,7 @@
change.setMode(info.getTransitMode(target));
change.setStartAbsBounds(info.mAbsoluteBounds);
change.setFlags(info.getChangeFlags(target));
+ change.setDisplayId(info.mDisplayId, getDisplayId(target));
final Task task = target.asTask();
final TaskFragment taskFragment = target.asTaskFragment();
@@ -1947,6 +1977,15 @@
}
TransitionInfo.AnimationOptions animOptions = null;
+
+ // Check if the top-most app is an activity (ie. activity->activity). If so, make sure to
+ // honor its custom transition options.
+ WindowContainer<?> topApp = null;
+ for (int i = 0; i < sortedTargets.size(); i++) {
+ if (isWallpaper(sortedTargets.get(i).mContainer)) continue;
+ topApp = sortedTargets.get(i).mContainer;
+ break;
+ }
if (topApp.asActivityRecord() != null) {
final ActivityRecord topActivity = topApp.asActivityRecord();
animOptions = addCustomActivityTransition(topActivity, true/* open */, null);
@@ -1997,14 +2036,15 @@
private static WindowContainer<?> findCommonAncestor(
@NonNull ArrayList<ChangeInfo> targets,
@NonNull WindowContainer<?> topApp) {
+ final int displayId = getDisplayId(topApp);
WindowContainer<?> ancestor = topApp.getParent();
// Go up ancestor parent chain until all targets are descendants. Ancestor should never be
// null because all targets are attached.
for (int i = targets.size() - 1; i >= 0; i--) {
final ChangeInfo change = targets.get(i);
final WindowContainer wc = change.mContainer;
- if (isWallpaper(wc)) {
- // Skip the non-app window.
+ if (isWallpaper(wc) || getDisplayId(wc) != displayId) {
+ // Skip the non-app window or windows on a different display
continue;
}
while (!wc.isDescendantOf(ancestor)) {
@@ -2180,6 +2220,7 @@
final Rect mAbsoluteBounds = new Rect();
boolean mShowWallpaper;
int mRotation = ROTATION_UNDEFINED;
+ int mDisplayId = -1;
@ActivityInfo.Config int mKnownConfigChanges;
/** These are just extra info. They aren't used for change-detection. */
@@ -2197,6 +2238,7 @@
mShowWallpaper = origState.showWallpaper();
mRotation = origState.getWindowConfiguration().getRotation();
mStartParent = origState.getParent();
+ mDisplayId = getDisplayId(origState);
}
@VisibleForTesting
@@ -2228,7 +2270,8 @@
// assume no change in windowing-mode.
|| (mWindowingMode != 0 && mContainer.getWindowingMode() != mWindowingMode)
|| !mContainer.getBounds().equals(mAbsoluteBounds)
- || mRotation != mContainer.getWindowConfiguration().getRotation();
+ || mRotation != mContainer.getWindowConfiguration().getRotation()
+ || mDisplayId != getDisplayId(mContainer);
}
@TransitionInfo.TransitionMode
diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
index 94260e20..e09c0a2 100644
--- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
@@ -122,7 +122,7 @@
private void respondToClientWithResponseAndFinish() {
Log.i(TAG, "respondToClientWithResponseAndFinish");
if (isSessionCancelled()) {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode());
logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */
ApiStatus.CLIENT_CANCELED);
@@ -134,7 +134,7 @@
logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */
ApiStatus.SUCCESS);
} catch (RemoteException e) {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode());
Log.i(TAG, "Issue while propagating the response to the client");
logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 47b8c7d..793d83e 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -83,6 +83,7 @@
@Override
protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
+ mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(System.nanoTime());
try {
mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent(
RequestInfo.newCreateRequestInfo(
@@ -90,6 +91,7 @@
mClientAppInfo.getPackageName()),
providerDataList));
} catch (RemoteException e) {
+ mChosenProviderFinalPhaseMetric.setUiReturned(false);
respondToClientWithErrorAndFinish(
CreateCredentialException.TYPE_UNKNOWN,
"Unable to invoke selector");
@@ -99,14 +101,16 @@
@Override
public void onFinalResponseReceived(ComponentName componentName,
@Nullable CreateCredentialResponse response) {
+ mChosenProviderFinalPhaseMetric.setUiReturned(true);
+ mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(System.nanoTime());
Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString());
setChosenMetric(componentName);
if (response != null) {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode());
respondToClientWithResponseAndFinish(response);
} else {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode());
respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREATE_OPTIONS,
"Invalid response");
@@ -138,6 +142,8 @@
private void respondToClientWithResponseAndFinish(CreateCredentialResponse response) {
Log.i(TAG, "respondToClientWithResponseAndFinish");
+ // TODO immediately add exception bit to chosen provider and do final emits across all
+ // including sequenceCounter!
if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
Log.i(TAG, "Request has already been completed. This is strange.");
return;
@@ -162,6 +168,7 @@
private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) {
Log.i(TAG, "respondToClientWithErrorAndFinish");
+
if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
Log.i(TAG, "Request has already been completed. This is strange.");
return;
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 10d3dc0..4c5c366 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -41,6 +41,7 @@
import android.credentials.ICreateCredentialCallback;
import android.credentials.ICredentialManager;
import android.credentials.IGetCredentialCallback;
+import android.credentials.IGetPendingCredentialCallback;
import android.credentials.ISetEnabledProvidersCallback;
import android.credentials.RegisterCredentialDescriptionRequest;
import android.credentials.UnregisterCredentialDescriptionRequest;
@@ -438,6 +439,17 @@
return cancelTransport;
}
+ @Override
+ public ICancellationSignal executeGetPendingCredential(
+ GetCredentialRequest request,
+ IGetPendingCredentialCallback callback,
+ final String callingPackage) {
+ // TODO(b/273308895): implement
+
+ ICancellationSignal cancelTransport = CancellationSignal.createTransport();
+ return cancelTransport;
+ }
+
private void processGetCredential(
GetCredentialRequest request,
IGetCredentialCallback callback,
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index 8e90c09..00fbbba 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -82,12 +82,14 @@
@Override
protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
+ mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(System.nanoTime());
try {
mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent(
RequestInfo.newGetRequestInfo(
mRequestId, mClientRequest, mClientAppInfo.getPackageName()),
providerDataList));
} catch (RemoteException e) {
+ mChosenProviderFinalPhaseMetric.setUiReturned(false);
respondToClientWithErrorAndFinish(
GetCredentialException.TYPE_UNKNOWN, "Unable to instantiate selector");
}
@@ -96,14 +98,16 @@
@Override
public void onFinalResponseReceived(ComponentName componentName,
@Nullable GetCredentialResponse response) {
+ mChosenProviderFinalPhaseMetric.setUiReturned(true);
+ mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(System.nanoTime());
Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString());
setChosenMetric(componentName);
if (response != null) {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode());
respondToClientWithResponseAndFinish(response);
} else {
- mChosenProviderMetric.setChosenProviderStatus(
+ mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode());
respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL,
"Invalid response from provider");
diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
index 1b3e37a..ed139b5 100644
--- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java
+++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
@@ -25,7 +25,7 @@
import com.android.server.credentials.metrics.ApiName;
import com.android.server.credentials.metrics.ApiStatus;
import com.android.server.credentials.metrics.CandidatePhaseMetric;
-import com.android.server.credentials.metrics.ChosenProviderMetric;
+import com.android.server.credentials.metrics.ChosenProviderFinalPhaseMetric;
import com.android.server.credentials.metrics.InitialPhaseMetric;
import java.util.Map;
@@ -89,11 +89,11 @@
* @param apiStatus the api status to log
* @param providers a map with known providers
* @param callingUid the calling UID of the client app
- * @param chosenProviderMetric the metric data type of the final chosen provider
+ * @param chosenProviderFinalPhaseMetric the metric data type of the final chosen provider
*/
protected static void logApiCalled(ApiName apiName, ApiStatus apiStatus,
Map<String, ProviderSession> providers, int callingUid,
- ChosenProviderMetric chosenProviderMetric) {
+ ChosenProviderFinalPhaseMetric chosenProviderFinalPhaseMetric) {
try {
var providerSessions = providers.values();
int providerSize = providerSessions.size();
@@ -102,7 +102,7 @@
int[] candidateStatusList = new int[providerSize];
int index = 0;
for (var session : providerSessions) {
- CandidatePhaseMetric metric = session.mCandidateProviderMetric;
+ CandidatePhaseMetric metric = session.mCandidatePhasePerProviderMetric;
candidateUidList[index] = metric.getCandidateUid();
candidateQueryRoundTripTimeList[index] = metric.getQueryLatencyMicroseconds();
candidateStatusList[index] = metric.getProviderQueryStatus();
@@ -116,12 +116,13 @@
/* repeated_candidate_provider_round_trip_time_query_microseconds */
candidateQueryRoundTripTimeList,
/* repeated_candidate_provider_status */ candidateStatusList,
- /* chosen_provider_uid */ chosenProviderMetric.getChosenUid(),
+ /* chosen_provider_uid */ chosenProviderFinalPhaseMetric.getChosenUid(),
/* chosen_provider_round_trip_time_overall_microseconds */
- chosenProviderMetric.getEntireProviderLatencyMicroseconds(),
+ chosenProviderFinalPhaseMetric.getEntireProviderLatencyMicroseconds(),
/* chosen_provider_final_phase_microseconds (backwards compat only) */
DEFAULT_INT_32,
- /* chosen_provider_status */ chosenProviderMetric.getChosenProviderStatus());
+ /* chosen_provider_status */ chosenProviderFinalPhaseMetric
+ .getChosenProviderStatus());
} catch (Exception e) {
Log.w(TAG, "Unexpected error during metric logging: " + e);
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
index ab29acc..950cf4f 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
@@ -120,7 +120,14 @@
@Override
protected void invokeSession() {
if (mRemoteCredentialService != null) {
- mCandidateProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
+ /*
+ InitialPhaseMetric initMetric = ((RequestSession)mCallbacks).initMetric;
+ TODO immediately once the other change patched through
+ mCandidateProviderMetric.setSessionId(initMetric
+ .mInitialPhaseMetric.getSessionId());
+ mCandidateProviderMetric.setStartTime(initMetric.getStartTime())
+ */
+ mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
mRemoteCredentialService.onClearCredentialState(mProviderRequest, this);
}
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
index 8c9c6cf..3ec0fc0 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
@@ -226,7 +226,14 @@
@Override
protected void invokeSession() {
if (mRemoteCredentialService != null) {
- mCandidateProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
+ /*
+ InitialPhaseMetric initMetric = ((RequestSession)mCallbacks).initMetric;
+ TODO immediately once the other change patched through
+ mCandidateProviderMetric.setSessionId(initMetric
+ .mInitialPhaseMetric.getSessionId());
+ mCandidateProviderMetric.setStartTime(initMetric.getStartTime())
+ */
+ mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
mRemoteCredentialService.onCreateCredential(mProviderRequest, this);
}
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index 955b721..ec8bf22 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -61,13 +61,13 @@
RemoteCredentialService.ProviderCallbacks<BeginGetCredentialResponse> {
private static final String TAG = "ProviderGetSession";
// Key to be used as the entry key for an action entry
- private static final String ACTION_ENTRY_KEY = "action_key";
+ public static final String ACTION_ENTRY_KEY = "action_key";
// Key to be used as the entry key for the authentication entry
- private static final String AUTHENTICATION_ACTION_ENTRY_KEY = "authentication_action_key";
+ public static final String AUTHENTICATION_ACTION_ENTRY_KEY = "authentication_action_key";
// Key to be used as an entry key for a remote entry
- private static final String REMOTE_ENTRY_KEY = "remote_entry_key";
+ public static final String REMOTE_ENTRY_KEY = "remote_entry_key";
// Key to be used as an entry key for a credential entry
- private static final String CREDENTIAL_ENTRY_KEY = "credential_key";
+ public static final String CREDENTIAL_ENTRY_KEY = "credential_key";
@NonNull
private final Map<String, CredentialOption> mBeginGetOptionToCredentialOptionMap;
@@ -269,7 +269,14 @@
@Override
protected void invokeSession() {
if (mRemoteCredentialService != null) {
- mCandidateProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
+ /*
+ InitialPhaseMetric initMetric = ((RequestSession)mCallbacks).initMetric;
+ TODO immediately once the other change patched through
+ mCandidateProviderMetric.setSessionId(initMetric
+ .mInitialPhaseMetric.getSessionId());
+ mCandidateProviderMetric.setStartTime(initMetric.getStartTime())
+ */
+ mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
mRemoteCredentialService.onBeginGetCredential(mProviderRequest, this);
}
}
@@ -427,10 +434,13 @@
private void onSetInitialRemoteResponse(BeginGetCredentialResponse response) {
mProviderResponse = response;
addToInitialRemoteResponse(response, /*isInitialResponse=*/true);
+ // Log the data.
if (mProviderResponseDataHandler.isEmptyResponse(response)) {
updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE);
return;
}
+ // TODO immediately, add to Candidate Phase counts, repeat across all sessions
+ // Use sets to dedup type counts
updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED);
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index 03e2a32..77d4e77 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -38,6 +38,7 @@
/**
* Provider session storing the state of provider response and ui entries.
+ *
* @param <T> The request to be sent to the provider
* @param <R> The response to be expected from the provider
*/
@@ -46,21 +47,35 @@
private static final String TAG = "ProviderSession";
- @NonNull protected final Context mContext;
- @NonNull protected final ComponentName mComponentName;
- @Nullable protected final CredentialProviderInfo mProviderInfo;
- @Nullable protected final RemoteCredentialService mRemoteCredentialService;
- @NonNull protected final int mUserId;
- @NonNull protected Status mStatus = Status.NOT_STARTED;
- @Nullable protected final ProviderInternalCallback mCallbacks;
- @Nullable protected Credential mFinalCredentialResponse;
- @Nullable protected ICancellationSignal mProviderCancellationSignal;
- @NonNull protected final T mProviderRequest;
- @Nullable protected R mProviderResponse;
- @NonNull protected Boolean mProviderResponseSet = false;
+ @NonNull
+ protected final Context mContext;
+ @NonNull
+ protected final ComponentName mComponentName;
+ @Nullable
+ protected final CredentialProviderInfo mProviderInfo;
+ @Nullable
+ protected final RemoteCredentialService mRemoteCredentialService;
+ @NonNull
+ protected final int mUserId;
+ @NonNull
+ protected Status mStatus = Status.NOT_STARTED;
+ @Nullable
+ protected final ProviderInternalCallback mCallbacks;
+ @Nullable
+ protected Credential mFinalCredentialResponse;
+ @Nullable
+ protected ICancellationSignal mProviderCancellationSignal;
+ @NonNull
+ protected final T mProviderRequest;
+ @Nullable
+ protected R mProviderResponse;
+ @NonNull
+ protected Boolean mProviderResponseSet = false;
// Specific candidate provider metric for the provider this session handles
- @Nullable protected CandidatePhaseMetric mCandidateProviderMetric;
- @NonNull private int mProviderSessionUid;
+ @Nullable
+ protected CandidatePhaseMetric mCandidatePhasePerProviderMetric;
+ @NonNull
+ private int mProviderSessionUid;
/**
* Returns true if the given status reflects that the provider state is ready to be shown
@@ -100,6 +115,7 @@
* Interface to be implemented by any class that wishes to get a callback when a particular
* provider session's status changes. Typically, implemented by the {@link RequestSession}
* class.
+ *
* @param <V> the type of the final response expected
*/
public interface ProviderInternalCallback<V> {
@@ -127,7 +143,7 @@
mUserId = userId;
mComponentName = componentName;
mRemoteCredentialService = remoteCredentialService;
- mCandidateProviderMetric = new CandidatePhaseMetric();
+ mCandidatePhasePerProviderMetric = new CandidatePhaseMetric();
mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName);
}
@@ -158,7 +174,7 @@
}
public Credential getFinalCredentialResponse() {
- return mFinalCredentialResponse;
+ return mFinalCredentialResponse;
}
/** Propagates cancellation signal to the remote provider service. */
@@ -192,7 +208,7 @@
return mRemoteCredentialService;
}
- /** Updates the status .*/
+ /** Updates the status . */
protected void updateStatusAndInvokeCallback(@NonNull Status status) {
setStatus(status);
updateCandidateMetric(status);
@@ -200,15 +216,18 @@
}
private void updateCandidateMetric(Status status) {
- mCandidateProviderMetric.setCandidateUid(mProviderSessionUid);
- mCandidateProviderMetric
+ mCandidatePhasePerProviderMetric.setCandidateUid(mProviderSessionUid);
+ // TODO immediately update the candidate phase here to have more new data
+ mCandidatePhasePerProviderMetric
.setQueryFinishTimeNanoseconds(System.nanoTime());
if (isTerminatingStatus(status)) {
- mCandidateProviderMetric.setProviderQueryStatus(ProviderStatusForMetrics.QUERY_FAILURE
- .getMetricCode());
+ mCandidatePhasePerProviderMetric.setProviderQueryStatus(
+ ProviderStatusForMetrics.QUERY_FAILURE
+ .getMetricCode());
} else if (isCompletionStatus(status)) {
- mCandidateProviderMetric.setProviderQueryStatus(ProviderStatusForMetrics.QUERY_SUCCESS
- .getMetricCode());
+ mCandidatePhasePerProviderMetric.setProviderQueryStatus(
+ ProviderStatusForMetrics.QUERY_SUCCESS
+ .getMetricCode());
}
}
@@ -228,7 +247,8 @@
}
/** Update the response state stored with the provider session. */
- @Nullable protected R getProviderResponse() {
+ @Nullable
+ protected R getProviderResponse() {
return mProviderResponse;
}
@@ -265,15 +285,20 @@
return false;
}
- /** Should be overridden to prepare, and stores state for {@link ProviderData} to be
- * shown on the UI. */
- @Nullable protected abstract ProviderData prepareUiData();
+ /**
+ * Should be overridden to prepare, and stores state for {@link ProviderData} to be
+ * shown on the UI.
+ */
+ @Nullable
+ protected abstract ProviderData prepareUiData();
/** Should be overridden to handle the selected entry from the UI. */
protected abstract void onUiEntrySelected(String entryType, String entryId,
ProviderPendingIntentResponse providerPendingIntentResponse);
- /** Should be overridden to invoke the provider at a defined location. Helpful for
- * situations such as metric generation. */
+ /**
+ * Should be overridden to invoke the provider at a defined location. Helpful for
+ * situations such as metric generation.
+ */
protected abstract void invokeSession();
}
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index edddba0..ed42bb2 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -19,6 +19,7 @@
import static com.android.server.credentials.MetricUtilities.logApiCalled;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
@@ -36,12 +37,15 @@
import com.android.internal.R;
import com.android.server.credentials.metrics.ApiName;
import com.android.server.credentials.metrics.ApiStatus;
+import com.android.server.credentials.metrics.CandidateBrowsingPhaseMetric;
import com.android.server.credentials.metrics.CandidatePhaseMetric;
-import com.android.server.credentials.metrics.ChosenProviderMetric;
+import com.android.server.credentials.metrics.ChosenProviderFinalPhaseMetric;
+import com.android.server.credentials.metrics.EntryEnum;
import com.android.server.credentials.metrics.InitialPhaseMetric;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
@@ -75,8 +79,16 @@
protected final CancellationSignal mCancellationSignal;
protected final Map<String, ProviderSession> mProviders = new HashMap<>();
- protected ChosenProviderMetric mChosenProviderMetric = new ChosenProviderMetric();
protected InitialPhaseMetric mInitialPhaseMetric = new InitialPhaseMetric();
+ protected ChosenProviderFinalPhaseMetric
+ mChosenProviderFinalPhaseMetric = new ChosenProviderFinalPhaseMetric();
+
+ // TODO(b/271135048) - Group metrics used in a scope together, such as here in RequestSession
+ // TODO(b/271135048) - Replace this with a new atom per each browsing emit (V4)
+ @Nullable
+ protected List<CandidateBrowsingPhaseMetric> mCandidateBrowsingPhaseMetric;
+ // As emits occur in sequential order, increment this counter and utilize
+ protected int mSequenceCounter = 0;
protected final String mHybridService;
@NonNull
@@ -152,10 +164,22 @@
return;
}
Log.i(TAG, "Provider session found");
+ logBrowsingPhasePerSelect(selection, providerSession);
providerSession.onUiEntrySelected(selection.getEntryKey(),
selection.getEntrySubkey(), selection.getPendingIntentProviderResponse());
}
+ private void logBrowsingPhasePerSelect(UserSelectionDialogResult selection,
+ ProviderSession providerSession) {
+ CandidateBrowsingPhaseMetric browsingPhaseMetric = new CandidateBrowsingPhaseMetric();
+ browsingPhaseMetric.setSessionId(this.mInitialPhaseMetric.getSessionId());
+ browsingPhaseMetric.setEntryEnum(
+ EntryEnum.getMetricCodeFromString(selection.getEntryKey()));
+ browsingPhaseMetric.setProviderUid(providerSession.mCandidatePhasePerProviderMetric
+ .getCandidateUid());
+ this.mCandidateBrowsingPhaseMetric.add(new CandidateBrowsingPhaseMetric());
+ }
+
protected void finishSession(boolean propagateCancellation) {
Log.i(TAG, "finishing session");
if (propagateCancellation) {
@@ -176,7 +200,14 @@
protected void logApiCall(ApiName apiName, ApiStatus apiStatus) {
logApiCalled(apiName, apiStatus, mProviders, mCallingUid,
- mChosenProviderMetric);
+ mChosenProviderFinalPhaseMetric);
+ }
+
+ protected void logApiCall(ChosenProviderFinalPhaseMetric finalPhaseMetric,
+ List<CandidateBrowsingPhaseMetric> browsingPhaseMetrics) {
+ // TODO (b/270403549) - this browsing phase object is fine but also have a new emit
+ // For the returned types by authentication entries - i.e. a CandidatePhase During Browse
+ // TODO call MetricUtilities with new setup
}
protected boolean isSessionCancelled() {
@@ -218,8 +249,10 @@
}
if (!providerDataList.isEmpty()) {
Log.i(TAG, "provider list not empty about to initiate ui");
+ // TODO immediately Add paths to end it (say it fails)
if (isSessionCancelled()) {
Log.i(TAG, "In getProviderDataAndInitiateUi but session has been cancelled");
+ // TODO immedaitely Add paths
} else {
launchUiWithProviderData(providerDataList);
}
@@ -233,11 +266,21 @@
*/
protected void setChosenMetric(ComponentName componentName) {
CandidatePhaseMetric metric = this.mProviders.get(componentName.flattenToString())
- .mCandidateProviderMetric;
- mChosenProviderMetric.setChosenUid(metric.getCandidateUid());
- mChosenProviderMetric.setFinalFinishTimeNanoseconds(System.nanoTime());
- mChosenProviderMetric.setQueryPhaseLatencyMicroseconds(
+ .mCandidatePhasePerProviderMetric;
+
+ mChosenProviderFinalPhaseMetric.setSessionId(metric.getSessionId());
+ mChosenProviderFinalPhaseMetric.setChosenUid(metric.getCandidateUid());
+
+ mChosenProviderFinalPhaseMetric.setQueryPhaseLatencyMicroseconds(
metric.getQueryLatencyMicroseconds());
- mChosenProviderMetric.setQueryStartTimeNanoseconds(metric.getStartQueryTimeNanoseconds());
+
+ mChosenProviderFinalPhaseMetric.setServiceBeganTimeNanoseconds(
+ metric.getServiceBeganTimeNanoseconds());
+ mChosenProviderFinalPhaseMetric.setQueryStartTimeNanoseconds(
+ metric.getStartQueryTimeNanoseconds());
+
+ // TODO immediately update with the entry count numbers from the candidate metrics
+
+ mChosenProviderFinalPhaseMetric.setFinalFinishTimeNanoseconds(System.nanoTime());
}
}
diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java
index 37ec8f0..0e1e0389 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/CandidateBrowsingPhaseMetric.java
@@ -17,23 +17,22 @@
package com.android.server.credentials.metrics;
/**
- * A part of the Candidate Phase, but emitted alongside {@link ChosenProviderMetric}. The user is
- * shown various entries from the provider responses, and may selectively browse through many
- * entries. It is possible that the initial set of browsing is for a provider that is ultimately
- * not chosen. This metric will be gathered PER browsing click, and aggregated, so that we can
- * understand where user interaction is more cumbersome, informing us for future improvements. This
- * can only be complete when the browsing is finished, ending in a final user choice, or possibly
- * a cancellation. Thus, this will be collected and emitted in the final phase, though collection
- * will begin in the candidate phase when the user begins browsing options.
+ * A part of the Candidate Phase, but emitted alongside {@link ChosenProviderFinalPhaseMetric}.
+ * The user is shown various entries from the provider responses, and may selectively browse through
+ * many entries. It is possible that the initial set of browsing is for a provider that is
+ * ultimately not chosen. This metric will be gathered PER browsing click, and aggregated, so that
+ * we can understand where user interaction is more cumbersome, informing us for future
+ * improvements. This can only be complete when the browsing is finished, ending in a final user
+ * choice, or possibly a cancellation. Thus, this will be collected and emitted in the final phase,
+ * though collection will begin in the candidate phase when the user begins browsing options.
*/
public class CandidateBrowsingPhaseMetric {
- private static final String TAG = "CandidateSelectionPhaseMetric";
- private static final int SEQUENCE_ID = 3;
+ private static final String TAG = "CandidateBrowsingPhaseMetric";
// The session id associated with the API Call this candidate provider is a part of, default -1
private int mSessionId = -1;
- // The EntryEnum that was pressed, defaults to -1 (TODO immediately, generate entry enum).
- private int mEntryEnum = -1;
+ // The EntryEnum that was pressed, defaults to -1
+ private int mEntryEnum = EntryEnum.UNKNOWN.getMetricCode();
// The provider associated with the press, defaults to -1
private int mProviderUid = -1;
@@ -47,12 +46,6 @@
return mSessionId;
}
- /* -- The sequence ID -- */
-
- public int getSequenceId() {
- return SEQUENCE_ID;
- }
-
/* -- The Entry of this tap -- */
public void setEntryEnum(int entryEnum) {
diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java
index 1c7fb69..c392d78 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java
@@ -30,10 +30,8 @@
public class CandidatePhaseMetric {
private static final String TAG = "CandidateProviderMetric";
- // Since this will always be the second in the split sequence, this is statically 2
- private static final int SESSION_ID = 2;
- // The sequence number of this emit of the API call, default -1, equal for all candidates
- private int mSequenceId = -1;
+ // The session id of this provider, default set to -1
+ private int mSessionId = -1;
// Indicates if this provider returned from the query phase, default false
private boolean mQueryReturned = false;
@@ -150,18 +148,13 @@
}
/* -------------- Session Id ---------------- */
+
+ public void setSessionId(int sessionId) {
+ mSessionId = sessionId;
+ }
+
public int getSessionId() {
- return SESSION_ID;
- }
-
- /* -------------- Sequence Id ---------------- */
-
- public void setSequenceId(int sequenceId) {
- mSequenceId = sequenceId;
- }
-
- public int getSequenceId() {
- return mSequenceId;
+ return mSessionId;
}
/* -------------- Query Returned Status ---------------- */
diff --git a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderMetric.java b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java
similarity index 80%
rename from services/credentials/java/com/android/server/credentials/metrics/ChosenProviderMetric.java
rename to services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java
index 1a61091..32fe204 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java
@@ -21,16 +21,22 @@
import com.android.server.credentials.MetricUtilities;
/**
- * The central chosen provider metric object that mimics our defined metric setup.
+ * The central chosen provider metric object that mimics our defined metric setup. This is used
+ * in the final phase of the flow and emits final status metrics.
* Some types are redundant across these metric collectors, but that has debug use-cases as
* these data-types are available at different moments of the flow (and typically, one can feed
* into the next).
* TODO(b/270403549) - iterate on this in V3+
+ * TODO(Immediately) - finalize V3 only types
*/
-public class ChosenProviderMetric {
+public class ChosenProviderFinalPhaseMetric {
- // TODO(b/270403549) - applies elsewhere, likely removed or replaced with a count-index (1,2,3)
- private static final String TAG = "ChosenProviderMetric";
+ // TODO(b/270403549) - applies elsewhere, likely removed or replaced w/ some hashed/count index
+ private static final String TAG = "ChosenFinalPhaseMetric";
+ // The session id associated with this API call, used to unite split emits
+ private long mSessionId = -1;
+ // Reveals if the UI was returned, false by default
+ private boolean mUiReturned = false;
private int mChosenUid = -1;
// Latency figures typically fed in from prior CandidateProviderMetric
@@ -39,16 +45,29 @@
private int mQueryPhaseLatencyMicroseconds = -1;
// Timestamps kept in raw nanoseconds. Expected to be converted to microseconds from using
- // reference 'mServiceBeganTimeNanoseconds' during metric log point.
+ // reference 'mServiceBeganTimeNanoseconds' during metric log point
+ // Kept for local reference purposes, the initial timestamp of the service called passed in
private long mServiceBeganTimeNanoseconds = -1;
+ // The first query timestamp, which upon emit is normalized to microseconds using the reference
+ // start timestamp
private long mQueryStartTimeNanoseconds = -1;
+ // The UI call timestamp, which upon emit will be normalized to microseconds using reference
private long mUiCallStartTimeNanoseconds = -1;
+ // The UI return timestamp, which upon emit will be normalized to microseconds using reference
private long mUiCallEndTimeNanoseconds = -1;
+ // The final finish timestamp, which upon emit will be normalized to microseconds with reference
private long mFinalFinishTimeNanoseconds = -1;
- private int mChosenProviderStatus = -1;
+ // The status of this provider after selection
- public ChosenProviderMetric() {
+ // Other General Information, such as final api status, provider status, entry info, etc...
+
+ private int mChosenProviderStatus = -1;
+ // TODO add remaining properties based on the Atom ; specifically, migrate the candidate
+ // Entry information, and store final status here
+
+
+ public ChosenProviderFinalPhaseMetric() {
}
/* ------------------- UID ------------------- */
@@ -200,4 +219,24 @@
public void setChosenProviderStatus(int chosenProviderStatus) {
mChosenProviderStatus = chosenProviderStatus;
}
+
+ /* ----------- Session ID -------------- */
+
+ public void setSessionId(long sessionId) {
+ mSessionId = sessionId;
+ }
+
+ public long getSessionId() {
+ return mSessionId;
+ }
+
+ /* ----------- UI Returned Successfully -------------- */
+
+ public void setUiReturned(boolean uiReturned) {
+ mUiReturned = uiReturned;
+ }
+
+ public boolean isUiReturned() {
+ return mUiReturned;
+ }
}
diff --git a/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java
new file mode 100644
index 0000000..73403a6
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java
@@ -0,0 +1,86 @@
+/*
+ * 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.credentials.metrics;
+
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_CLEAR_CREDENTIAL;
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_CREATE_CREDENTIAL;
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_GET_CREDENTIAL;
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE;
+import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_UNKNOWN;
+import static com.android.server.credentials.ProviderGetSession.ACTION_ENTRY_KEY;
+import static com.android.server.credentials.ProviderGetSession.AUTHENTICATION_ACTION_ENTRY_KEY;
+import static com.android.server.credentials.ProviderGetSession.CREDENTIAL_ENTRY_KEY;
+import static com.android.server.credentials.ProviderGetSession.REMOTE_ENTRY_KEY;
+
+import android.util.Log;
+
+import java.util.AbstractMap;
+import java.util.Map;
+
+public enum EntryEnum {
+ // TODO immediately, update with built entries
+ UNKNOWN(CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_UNKNOWN),
+ ACTION_ENTRY(CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_GET_CREDENTIAL),
+ CREDENTIAL_ENTRY(CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_CREATE_CREDENTIAL),
+ REMOTE_ENTRY(CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_CLEAR_CREDENTIAL),
+ AUTHENTICATION_ENTRY(
+ CREDENTIAL_MANAGER_API_CALLED__API_NAME__API_NAME_IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE
+ );
+
+ private static final String TAG = "EntryEnum";
+
+ private final int mInnerMetricCode;
+
+ private static final Map<String, Integer> sKeyToEntryCode = Map.ofEntries(
+ new AbstractMap.SimpleEntry<>(ACTION_ENTRY_KEY,
+ ACTION_ENTRY.mInnerMetricCode),
+ new AbstractMap.SimpleEntry<>(AUTHENTICATION_ACTION_ENTRY_KEY,
+ AUTHENTICATION_ENTRY.mInnerMetricCode),
+ new AbstractMap.SimpleEntry<>(REMOTE_ENTRY_KEY,
+ REMOTE_ENTRY.mInnerMetricCode),
+ new AbstractMap.SimpleEntry<>(CREDENTIAL_ENTRY_KEY,
+ CREDENTIAL_ENTRY.mInnerMetricCode)
+ );
+
+ EntryEnum(int innerMetricCode) {
+ this.mInnerMetricCode = innerMetricCode;
+ }
+
+ /**
+ * Gives the West-world version of the metric name.
+ *
+ * @return a code corresponding to the west world metric name
+ */
+ public int getMetricCode() {
+ return this.mInnerMetricCode;
+ }
+
+ /**
+ * Given a string key type known to the framework, this returns the known metric code associated
+ * with that string.
+ *
+ * @param stringKey a string key type for a particular entry
+ * @return the metric code associated with this enum
+ */
+ public static int getMetricCodeFromString(String stringKey) {
+ if (!sKeyToEntryCode.containsKey(stringKey)) {
+ Log.w(TAG, "Attempted to use an unsupported string key entry type");
+ return UNKNOWN.mInnerMetricCode;
+ }
+ return sKeyToEntryCode.get(stringKey);
+ }
+}
diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
index 31c6f6f..a73495f 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
@@ -24,16 +24,14 @@
* TODO(b/270403549) - iterate on this in V3+
*/
public class InitialPhaseMetric {
- private static final String TAG = "PreCandidateMetric";
- // A sequence id to order united emits, due to split, this will statically always be 1
- public static final int SEQUENCE_ID = 1;
+ private static final String TAG = "InitialPhaseMetric";
// The api being called, default set to unknown
private int mApiName = ApiName.UNKNOWN.getMetricCode();
// The caller uid of the calling application, default to -1
private int mCallerUid = -1;
// The session id to unite multiple atom emits, default to -1
- private long mSessionId = -1;
+ private int mSessionId = -1;
private int mCountRequestClassType = -1;
// Raw timestamps in nanoseconds, *the only* one logged as such (i.e. 64 bits) since it is a
@@ -100,15 +98,14 @@
/* ------ SessionId ------ */
- public void setSessionId(long sessionId) {
+ public void setSessionId(int sessionId) {
mSessionId = sessionId;
}
- public long getSessionId() {
+ public int getSessionId() {
return mSessionId;
}
-
/* ------ Count Request Class Types ------ */
public void setCountRequestClassType(int countRequestClassType) {
diff --git a/services/java/com/android/server/HsumBootUserInitializer.java b/services/java/com/android/server/HsumBootUserInitializer.java
index 50113fe..b895812 100644
--- a/services/java/com/android/server/HsumBootUserInitializer.java
+++ b/services/java/com/android/server/HsumBootUserInitializer.java
@@ -18,6 +18,7 @@
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ContentResolver;
+import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.os.Handler;
@@ -27,6 +28,7 @@
import android.provider.Settings;
import com.android.server.am.ActivityManagerService;
+import com.android.server.pm.PackageManagerService;
import com.android.server.pm.UserManagerInternal;
import com.android.server.utils.Slogf;
import com.android.server.utils.TimingsTraceAndSlog;
@@ -41,6 +43,7 @@
private final UserManagerInternal mUmi;
private final ActivityManagerService mAms;
+ private final PackageManagerService mPms;
private final ContentResolver mContentResolver;
private final ContentObserver mDeviceProvisionedObserver =
@@ -63,20 +66,23 @@
/** Static factory method for creating a {@link HsumBootUserInitializer} instance. */
public static @Nullable HsumBootUserInitializer createInstance(ActivityManagerService am,
- ContentResolver contentResolver, boolean shouldAlwaysHaveMainUser) {
+ PackageManagerService pms, ContentResolver contentResolver,
+ boolean shouldAlwaysHaveMainUser) {
if (!UserManager.isHeadlessSystemUserMode()) {
return null;
}
return new HsumBootUserInitializer(
LocalServices.getService(UserManagerInternal.class),
- am, contentResolver, shouldAlwaysHaveMainUser);
+ am, pms, contentResolver, shouldAlwaysHaveMainUser);
}
private HsumBootUserInitializer(UserManagerInternal umi, ActivityManagerService am,
- ContentResolver contentResolver, boolean shouldAlwaysHaveMainUser) {
+ PackageManagerService pms, ContentResolver contentResolver,
+ boolean shouldAlwaysHaveMainUser) {
mUmi = umi;
mAms = am;
+ mPms = pms;
mContentResolver = contentResolver;
mShouldAlwaysHaveMainUser = shouldAlwaysHaveMainUser;
}
@@ -131,7 +137,8 @@
try {
t.traceBegin("getBootUser");
- final int bootUser = mUmi.getBootUser();
+ final int bootUser = mUmi.getBootUser(/* waitUntilSet= */ mPms
+ .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, /* version= */0));
t.traceEnd();
t.traceBegin("switchToBootUser-" + bootUser);
switchToBootUser(bootUser);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 4c31645..be2873e 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -2721,7 +2721,7 @@
// on it in their setup, but likely needs to be done after LockSettingsService is ready.
final HsumBootUserInitializer hsumBootUserInitializer =
HsumBootUserInitializer.createInstance(
- mActivityManagerService, mContentResolver,
+ mActivityManagerService, mPackageManagerService, mContentResolver,
context.getResources().getBoolean(R.bool.config_isMainUserPermanentAdmin));
if (hsumBootUserInitializer != null) {
t.traceBegin("HsumBootUserInitializer.init");
@@ -3021,8 +3021,7 @@
mSystemServiceManager.startBootPhase(t, SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
t.traceEnd();
- if (hsumBootUserInitializer != null && !isAutomotive) {
- // TODO(b/261924826): remove isAutomotive check once the workflow is finalized
+ if (hsumBootUserInitializer != null) {
t.traceBegin("HsumBootUserInitializer.systemRunning");
hsumBootUserInitializer.systemRunning(t);
t.traceEnd();
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 564893c..e7b3e6f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -260,7 +260,7 @@
mUms.setBootUser(OTHER_USER_ID);
assertWithMessage("getBootUser")
- .that(mUmi.getBootUser()).isEqualTo(OTHER_USER_ID);
+ .that(mUmi.getBootUser(/* waitUntilSet= */ false)).isEqualTo(OTHER_USER_ID);
}
@Test
@@ -273,7 +273,8 @@
mUms.setBootUser(PROFILE_USER_ID);
assertWithMessage("getBootUser")
- .that(mUmi.getBootUser()).isEqualTo(UserHandle.USER_SYSTEM);
+ .that(mUmi.getBootUser(/* waitUntilSet= */ false))
+ .isEqualTo(UserHandle.USER_SYSTEM);
}
@Test
@@ -289,7 +290,7 @@
// Boot user not switchable so return most recently in foreground.
assertWithMessage("getBootUser")
- .that(mUmi.getBootUser()).isEqualTo(OTHER_USER_ID);
+ .that(mUmi.getBootUser(/* waitUntilSet= */ false)).isEqualTo(OTHER_USER_ID);
}
@Test
@@ -299,7 +300,8 @@
addUser(OTHER_USER_ID);
assertWithMessage("getBootUser")
- .that(mUmi.getBootUser()).isEqualTo(UserHandle.USER_SYSTEM);
+ .that(mUmi.getBootUser(/* waitUntilSet= */ false))
+ .isEqualTo(UserHandle.USER_SYSTEM);
}
@Test
@@ -312,14 +314,15 @@
setLastForegroundTime(OTHER_USER_ID, 2_000_000L);
assertWithMessage("getBootUser")
- .that(mUmi.getBootUser()).isEqualTo(OTHER_USER_ID);
+ .that(mUmi.getBootUser(/* waitUntilSet= */ false)).isEqualTo(OTHER_USER_ID);
}
@Test
public void testGetBootUser_Headless_ThrowsIfOnlySystemUserExists() throws Exception {
setSystemUserHeadless(true);
- assertThrows(UserManager.CheckedUserOperationException.class, () -> mUmi.getBootUser());
+ assertThrows(UserManager.CheckedUserOperationException.class,
+ () -> mUmi.getBootUser(/* waitUntilSet= */ false));
}
private void mockCurrentUser(@UserIdInt int userId) {
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
index d9d0715..64e6236 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
@@ -74,6 +74,8 @@
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import javax.crypto.SecretKey;
@@ -324,16 +326,30 @@
mInjected = mock(MockableRebootEscrowInjected.class);
mMockInjector = new MockInjector(mContext, mUserManager, mRebootEscrow,
mKeyStoreManager, mStorage, mInjected);
- mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage);
HandlerThread thread = new HandlerThread("RebootEscrowManagerTest");
thread.start();
mHandler = new Handler(thread.getLooper());
+ mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage, mHandler);
+
}
private void setServerBasedRebootEscrowProvider() throws Exception {
mMockInjector = new MockInjector(mContext, mUserManager, mServiceConnection,
mKeyStoreManager, mStorage, mInjected);
- mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage);
+ mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage, mHandler);
+ }
+
+ private void waitForHandler() throws InterruptedException {
+ // Wait for handler to complete processing.
+ CountDownLatch latch = new CountDownLatch(1);
+ mHandler.post(latch::countDown);
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+
+ }
+
+ private void callToRebootEscrowIfNeededAndWait(int userId) throws InterruptedException {
+ mService.callToRebootEscrowIfNeeded(userId, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ waitForHandler();
}
@Test
@@ -343,7 +359,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
}
@@ -355,8 +371,7 @@
mService.setRebootEscrowListener(mockListener);
mService.prepareRebootEscrow();
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
- verify(mockListener).onPreparedForReboot(eq(true));
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
assertFalse(mStorage.hasRebootEscrowServerBlob());
}
@@ -366,7 +381,7 @@
RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
mService.setRebootEscrowListener(mockListener);
mService.prepareRebootEscrow();
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
clearInvocations(mRebootEscrow);
@@ -390,7 +405,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -414,7 +429,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -435,7 +450,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -453,10 +468,9 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
- mService.callToRebootEscrowIfNeeded(SECURE_SECONDARY_USER_ID, FAKE_SP_VERSION,
- FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(SECURE_SECONDARY_USER_ID);
verify(mRebootEscrow, never()).storeKey(any());
assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
@@ -488,7 +502,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -511,7 +525,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -554,7 +568,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -598,7 +612,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -643,7 +657,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -689,7 +703,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -738,7 +752,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -791,7 +805,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -846,7 +860,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -893,7 +907,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -949,7 +963,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -1008,7 +1022,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -1068,7 +1082,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -1124,7 +1138,7 @@
mService.prepareRebootEscrow();
clearInvocations(mServiceConnection);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
@@ -1176,7 +1190,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -1207,7 +1221,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -1235,7 +1249,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -1274,7 +1288,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
verify(mRebootEscrow, never()).storeKey(any());
@@ -1309,7 +1323,7 @@
mService.prepareRebootEscrow();
clearInvocations(mRebootEscrow);
- mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
+ callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
verify(mockListener).onPreparedForReboot(eq(true));
assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
verify(mRebootEscrow, never()).storeKey(any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index debfc84..3045812 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -520,6 +520,47 @@
}
@Test
+ public void testCreateInfo_MultiDisplay() {
+ DisplayContent otherDisplay = createNewDisplay();
+ final Transition transition = createTestTransition(TRANSIT_OPEN);
+ ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+ ArraySet<WindowContainer> participants = transition.mParticipants;
+
+ final Task display0Task = createTask(mDisplayContent);
+ final Task display1Task = createTask(otherDisplay);
+ // Start states.
+ changes.put(display0Task,
+ new Transition.ChangeInfo(display0Task, false /* vis */, true /* exChg */));
+ changes.put(display1Task,
+ new Transition.ChangeInfo(display1Task, false /* vis */, true /* exChg */));
+ fillChangeMap(changes, display0Task);
+ fillChangeMap(changes, display1Task);
+ // End states.
+ display0Task.setVisibleRequested(true);
+ display1Task.setVisibleRequested(true);
+
+ final int transit = transition.mType;
+ int flags = 0;
+
+ participants.add(display0Task);
+ participants.add(display1Task);
+ ArrayList<Transition.ChangeInfo> targets =
+ Transition.calculateTargets(participants, changes);
+ TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
+ assertEquals(2, info.getRootCount());
+ // Check that the changes are assigned to the correct display
+ assertEquals(mDisplayContent.getDisplayId(), info.getChange(
+ display0Task.mRemoteToken.toWindowContainerToken()).getEndDisplayId());
+ assertEquals(otherDisplay.getDisplayId(), info.getChange(
+ display1Task.mRemoteToken.toWindowContainerToken()).getEndDisplayId());
+ // Check that roots can be found by display and have the correct display
+ assertEquals(mDisplayContent.getDisplayId(),
+ info.getRoot(info.findRootIndex(mDisplayContent.getDisplayId())).getDisplayId());
+ assertEquals(otherDisplay.getDisplayId(),
+ info.getRoot(info.findRootIndex(otherDisplay.getDisplayId())).getDisplayId());
+ }
+
+ @Test
public void testTargets_noIntermediatesToWallpaper() {
final Transition transition = createTestTransition(TRANSIT_OPEN);
diff --git a/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
index e704cc2..46ebfed 100644
--- a/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
+++ b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
@@ -35,6 +35,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.IllformedLocaleException;
import java.util.List;
import java.util.Locale;
@@ -141,14 +142,118 @@
HashMap<String, LocaleInfo> result =
LocaleStore.convertExplicitLocales(locales, supportedLocale);
-
assertEquals("en", result.get("en").getId());
assertEquals("en-US", result.get("en-US").getId());
assertNull(result.get("en-Latn-US"));
}
+ @Test
+ public void getLevelLocales_languageTier_returnAllSupportLanguages() {
+ LocaleList testSupportedLocales =
+ LocaleList.forLanguageTags(
+ "en-US,zh-Hant-TW,ja-JP,en-GB,bn-IN-u-nu-arab,ks-Arab-IN,bn-IN");
+
+ Set<String> ignorableLocales = new HashSet<>();
+ ignorableLocales.add("zh-Hant-HK");
+ LocaleInfo parent = null;
+
+ Set<LocaleInfo> localeInfos = LocaleStore.getLevelLocales(
+ null, ignorableLocales, parent, false, testSupportedLocales);
+
+ assertEquals(5, localeInfos.size());
+ localeInfos.forEach(localeInfo -> {
+ assertTrue(localeInfo.getLocale().getCountry().isEmpty());
+ });
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("en")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("zh-Hant")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("ja")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("bn")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("ks-Arab")));
+ }
+
+ @Test
+ public void getLevelLocales_regionTierAndParentIsEn_returnEnLocales() {
+ LocaleList testSupportedLocales =
+ LocaleList.forLanguageTags(
+ "en-US,en-GB,bn-IN-u-nu-arab,ks-Arab-IN,en-ZA,bn-IN");
+ Set<String> ignorableLocales = new HashSet<>();
+ ignorableLocales.add("zh-Hant-HK");
+ LocaleInfo parent = LocaleStore.fromLocale(Locale.forLanguageTag("en"));
+
+ Set<LocaleInfo> localeInfos = LocaleStore.getLevelLocales(
+ null, ignorableLocales, parent, false, testSupportedLocales);
+
+ assertEquals(3, localeInfos.size());
+ localeInfos.forEach(localeInfo -> {
+ assertEquals("en", localeInfo.getLocale().getLanguage());
+ });
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("en-US")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("en-GB")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("en-ZA")));
+ }
+
+ @Test
+ public void getLevelLocales_numberingTierAndParentIsBnIn_returnBnInLocales() {
+ LocaleList testSupportedLocales =
+ LocaleList.forLanguageTags(
+ "en-US,zh-Hant-TW,bn-IN-u-nu-arab,ks-Arab-IN,en-ZA,bn-IN,bn-IN-u-nu-adlm");
+ Set<String> ignorableLocales = new HashSet<>();
+ ignorableLocales.add("zh-Hant-HK");
+ LocaleInfo parent = LocaleStore.fromLocale(Locale.forLanguageTag("bn"));
+
+ Set<LocaleInfo> localeInfos = LocaleStore.getLevelLocales(
+ null, ignorableLocales, parent, false, testSupportedLocales);
+
+ assertEquals(1, localeInfos.size());
+ assertEquals("bn-IN", localeInfos.iterator().next().getLocale().toLanguageTag());
+ }
+
+ @Test
+ public void getLevelLocales_regionTierAndParentIsBnInAndIgnoreBn_returnEmpty() {
+ LocaleList testSupportedLocales =
+ LocaleList.forLanguageTags(
+ "en-US,zh-Hant-TW,bn-IN-u-nu-arab,ks-Arab-IN,en-ZA,bn-IN,bn-IN-u-nu-adlm");
+ Set<String> ignorableLocales = new HashSet<>();
+ ignorableLocales.add("bn-IN");
+ LocaleInfo parent = LocaleStore.fromLocale(Locale.forLanguageTag("bn-IN"));
+
+ Set<LocaleInfo> localeInfos = LocaleStore.getLevelLocales(
+ null, ignorableLocales, parent, false, testSupportedLocales);
+
+ assertEquals(0, localeInfos.size());
+ }
+
+ @Test
+ public void getLevelLocales_regionTierAndParentIsBnIn_returnBnLocaleFamily() {
+ LocaleList testSupportedLocales =
+ LocaleList.forLanguageTags(
+ "en-US,zh-Hant-TW,bn-IN-u-nu-arab,ks-Arab-IN,en-ZA,bn-IN,bn-IN-u-nu-adlm");
+ Set<String> ignorableLocales = new HashSet<>();
+ ignorableLocales.add("en-US");
+ LocaleInfo parent = LocaleStore.fromLocale(Locale.forLanguageTag("bn-IN"));
+
+ Set<LocaleInfo> localeInfos = LocaleStore.getLevelLocales(
+ null, ignorableLocales, parent, false, testSupportedLocales);
+
+ assertEquals(3, localeInfos.size());
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("bn-IN-u-nu-adlm")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("bn-IN-u-nu-arab")));
+ assertTrue(localeInfos.stream().anyMatch(
+ info -> info.getLocale().toLanguageTag().equals("bn-IN")));
+ }
+
private ArrayList<LocaleInfo> getFakeSupportedLocales() {
- String[] locales = {"en-US", "zh-Hant-TW", "ja-JP", "en-GB"};
+ String[] locales = {"en-US", "zh-Hant-TW", "ja-JP", "en-GB", "en-US-u-nu-arab"};
ArrayList<LocaleInfo> supportedLocales = new ArrayList<>();
for (String localeTag : locales) {
supportedLocales.add(LocaleStore.fromLocale(Locale.forLanguageTag(localeTag)));