diff options
65 files changed, 1567 insertions, 455 deletions
diff --git a/api/Android.bp b/api/Android.bp index b3b18b66e097..ef64a89a892d 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -115,6 +115,7 @@ combined_apis { "framework-pdf", "framework-permission", "framework-permission-s", + "framework-profiling", "framework-scheduling", "framework-sdkextensions", "framework-statsd", diff --git a/api/ApiDocs.bp b/api/ApiDocs.bp index 7ae3224e7500..7ee43191a80a 100644 --- a/api/ApiDocs.bp +++ b/api/ApiDocs.bp @@ -67,6 +67,7 @@ stubs_defaults { ":framework-ondevicepersonalization-sources", ":framework-permission-sources", ":framework-permission-s-sources", + ":framework-profiling-sources", ":framework-scheduling-sources", ":framework-sdkextensions-sources", ":framework-statsd-sources", diff --git a/boot/Android.bp b/boot/Android.bp index 228d060bf9cf..cdfa7c80bc93 100644 --- a/boot/Android.bp +++ b/boot/Android.bp @@ -122,6 +122,10 @@ custom_platform_bootclasspath { module: "com.android.permission-bootclasspath-fragment", }, { + apex: "com.android.profiling", + module: "com.android.profiling-bootclasspath-fragment", + }, + { apex: "com.android.scheduling", module: "com.android.scheduling-bootclasspath-fragment", }, diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 55ed1f559f51..d33145529f2a 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -187,6 +187,7 @@ package android.media { method @NonNull public static android.media.BluetoothProfileConnectionInfo createA2dpInfo(boolean, int); method @NonNull public static android.media.BluetoothProfileConnectionInfo createA2dpSinkInfo(int); method @NonNull public static android.media.BluetoothProfileConnectionInfo createHearingAidInfo(boolean); + method @FlaggedApi("android.media.audio.sco_managed_by_audio") @NonNull public static android.media.BluetoothProfileConnectionInfo createHfpInfo(); method @NonNull public static android.media.BluetoothProfileConnectionInfo createLeAudioInfo(boolean, boolean); method @NonNull public static android.media.BluetoothProfileConnectionInfo createLeAudioOutputInfo(boolean, int); method public int describeContents(); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 6285eb3b2096..084c71f47603 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -147,6 +147,7 @@ import java.util.function.Consumer; * </p> */ @SystemService(Context.ACTIVITY_SERVICE) +@android.ravenwood.annotation.RavenwoodKeepPartialClass public class ActivityManager { private static String TAG = "ActivityManager"; @@ -966,6 +967,7 @@ public class ActivityManager { * Print capability bits in human-readable form. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static void printCapabilitiesSummary(PrintWriter pw, @ProcessCapability int caps) { pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_LOCATION) != 0 ? 'L' : '-'); pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_CAMERA) != 0 ? 'C' : '-'); @@ -976,6 +978,7 @@ public class ActivityManager { } /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static void printCapabilitiesSummary(StringBuilder sb, @ProcessCapability int caps) { sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_LOCATION) != 0 ? 'L' : '-'); sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_CAMERA) != 0 ? 'C' : '-'); @@ -989,6 +992,7 @@ public class ActivityManager { * Print capability bits in human-readable form. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static void printCapabilitiesFull(PrintWriter pw, @ProcessCapability int caps) { printCapabilitiesSummary(pw, caps); final int remain = caps & ~PROCESS_CAPABILITY_ALL; @@ -999,6 +1003,7 @@ public class ActivityManager { } /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static String getCapabilitiesSummary(@ProcessCapability int caps) { final StringBuilder sb = new StringBuilder(); printCapabilitiesSummary(sb, caps); @@ -1018,6 +1023,7 @@ public class ActivityManager { * @return the value of the corresponding enums.proto ProcessStateEnum value. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static final int processStateAmToProto(int amInt) { switch (amInt) { case PROCESS_STATE_UNKNOWN: @@ -1078,16 +1084,19 @@ public class ActivityManager { public static final int MAX_PROCESS_STATE = PROCESS_STATE_NONEXISTENT; /** @hide Should this process state be considered a background state? */ + @android.ravenwood.annotation.RavenwoodKeep public static final boolean isProcStateBackground(int procState) { return procState >= PROCESS_STATE_TRANSIENT_BACKGROUND; } /** @hide Should this process state be considered in the cache? */ + @android.ravenwood.annotation.RavenwoodKeep public static final boolean isProcStateCached(int procState) { return procState >= PROCESS_STATE_CACHED_ACTIVITY; } /** @hide Is this a foreground service type? */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isForegroundService(int procState) { return procState == PROCESS_STATE_FOREGROUND_SERVICE; } @@ -1161,10 +1170,25 @@ public class ActivityManager { mContext = context; } + private static volatile int sCurrentUser$ravenwood = UserHandle.USER_NULL; + + /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep + public static void init$ravenwood(int currentUser) { + sCurrentUser$ravenwood = currentUser; + } + + /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep + public static void reset$ravenwood() { + sCurrentUser$ravenwood = UserHandle.USER_NULL; + } + /** * Returns whether the launch was successful. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static final boolean isStartResultSuccessful(int result) { return FIRST_START_SUCCESS_CODE <= result && result <= LAST_START_SUCCESS_CODE; } @@ -1173,6 +1197,7 @@ public class ActivityManager { * Returns whether the launch result was a fatal error. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static final boolean isStartResultFatalError(int result) { return FIRST_START_FATAL_ERROR_CODE <= result && result <= LAST_START_FATAL_ERROR_CODE; } @@ -1343,6 +1368,7 @@ public class ActivityManager { public @interface RestrictionLevel{} /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static String restrictionLevelToName(@RestrictionLevel int level) { switch (level) { case RESTRICTION_LEVEL_UNKNOWN: @@ -4779,6 +4805,7 @@ public class ActivityManager { * Returns "true" if the user interface is currently being messed with * by a monkey. */ + @android.ravenwood.annotation.RavenwoodReplace public static boolean isUserAMonkey() { try { return getService().isUserAMonkey(); @@ -4787,6 +4814,12 @@ public class ActivityManager { } } + /** @hide */ + public static boolean isUserAMonkey$ravenwood() { + // Ravenwood environment is never considered a "monkey" + return false; + } + /** * Returns "true" if device is running in a test harness. * @@ -4973,6 +5006,7 @@ public class ActivityManager { "android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL" }) + @android.ravenwood.annotation.RavenwoodReplace public static int getCurrentUser() { try { return getService().getCurrentUserId(); @@ -4981,6 +5015,11 @@ public class ActivityManager { } } + /** @hide */ + public static int getCurrentUser$ravenwood() { + return sCurrentUser$ravenwood; + } + /** * @param userid the user's id. Zero indicates the default user. * @hide @@ -5320,6 +5359,7 @@ public class ActivityManager { /** * @hide */ + @android.ravenwood.annotation.RavenwoodReplace public static boolean isSystemReady() { if (!sSystemReady) { if (ActivityThread.isSystem()) { @@ -5334,6 +5374,12 @@ public class ActivityManager { return sSystemReady; } + /** @hide */ + public static boolean isSystemReady$ravenwood() { + // Ravenwood environment is always considered as booted and ready + return true; + } + /** * @hide */ @@ -5661,11 +5707,13 @@ public class ActivityManager { } /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isProcStateConsideredInteraction(@ProcessState int procState) { return (procState <= PROCESS_STATE_TOP || procState == PROCESS_STATE_BOUND_TOP); } /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static String procStateToString(int procState) { final String procStateStr; switch (procState) { diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index d705eeb706e8..88839071bf6c 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5946,6 +5946,12 @@ public class Notification implements Parcelable // there is enough space to do so (and fall back to the left edge if not). big.setInt(R.id.actions, "setCollapsibleIndentDimen", R.dimen.call_notification_collapsible_indent); + if (CallStyle.USE_NEW_ACTION_LAYOUT) { + if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "setting evenly divided mode on action list"); + } + big.setBoolean(R.id.actions, "setEvenlyDividedMode", true); + } } big.setBoolean(R.id.actions, "setEmphasizedMode", emphasizedMode); if (numActions > 0 && !p.mHideActions) { @@ -6421,7 +6427,15 @@ public class Notification implements Parcelable // Remove full-length color spans and ensure text contrast with the button fill. title = ContrastColorUtil.ensureColorSpanContrast(title, buttonFillColor); } - button.setTextViewText(R.id.action0, ensureColorSpanContrast(title, p)); + final CharSequence label = ensureColorSpanContrast(title, p); + if (p.mCallStyleActions && CallStyle.USE_NEW_ACTION_LAYOUT) { + if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "new action layout enabled, gluing instead of setting text"); + } + button.setCharSequence(R.id.action0, "glueLabel", label); + } else { + button.setTextViewText(R.id.action0, label); + } int textColor = ContrastColorUtil.resolvePrimaryColor(mContext, buttonFillColor, mInNightMode); if (tombstone) { @@ -6438,7 +6452,14 @@ public class Notification implements Parcelable button.setColorStateList(R.id.action0, "setButtonBackground", ColorStateList.valueOf(buttonFillColor)); if (p.mCallStyleActions) { - button.setImageViewIcon(R.id.action0, action.getIcon()); + if (CallStyle.USE_NEW_ACTION_LAYOUT) { + if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "new action layout enabled, gluing instead of setting icon"); + } + button.setIcon(R.id.action0, "glueIcon", action.getIcon()); + } else { + button.setImageViewIcon(R.id.action0, action.getIcon()); + } boolean priority = action.getExtras().getBoolean(CallStyle.KEY_ACTION_PRIORITY); button.setBoolean(R.id.action0, "setIsPriority", priority); int minWidthDimen = @@ -9565,6 +9586,15 @@ public class Notification implements Parcelable * </pre> */ public static class CallStyle extends Style { + /** + * @hide + */ + public static final boolean USE_NEW_ACTION_LAYOUT = false; + + /** + * @hide + */ + public static final boolean DEBUG_NEW_ACTION_LAYOUT = true; /** * @hide diff --git a/core/java/android/ddm/DdmHandleViewDebug.java b/core/java/android/ddm/DdmHandleViewDebug.java index 0f66fcbdbec9..5cbf24f3ba54 100644 --- a/core/java/android/ddm/DdmHandleViewDebug.java +++ b/core/java/android/ddm/DdmHandleViewDebug.java @@ -16,16 +16,12 @@ package android.ddm; -import static com.android.internal.util.Preconditions.checkArgument; - import android.util.Log; import android.view.View; import android.view.ViewDebug; import android.view.ViewRootImpl; import android.view.WindowManagerGlobal; -import com.android.internal.annotations.VisibleForTesting; - import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; @@ -35,10 +31,8 @@ import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; -import java.lang.reflect.Method; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; /** * Handle various requests related to profiling / debugging of the view system. @@ -352,48 +346,17 @@ public class DdmHandleViewDebug extends DdmHandle { * * The return value is encoded the same way as a single parameter (type + value) */ - private Chunk invokeViewMethod(final View rootView, final View targetView, ByteBuffer in) { + private Chunk invokeViewMethod(View rootView, final View targetView, ByteBuffer in) { int l = in.getInt(); String methodName = getString(in, l); - Class<?>[] argTypes; - Object[] args; - if (!in.hasRemaining()) { - argTypes = new Class<?>[0]; - args = new Object[0]; - } else { - int nArgs = in.getInt(); - argTypes = new Class<?>[nArgs]; - args = new Object[nArgs]; - - try { - deserializeMethodParameters(args, argTypes, in); - } catch (ViewMethodInvocationSerializationException e) { - return createFailChunk(ERR_INVALID_PARAM, e.getMessage()); - } - } - - Method method; - try { - method = targetView.getClass().getMethod(methodName, argTypes); - } catch (NoSuchMethodException e) { - Log.e(TAG, "No such method: " + e.getMessage()); - return createFailChunk(ERR_INVALID_PARAM, - "No such method: " + e.getMessage()); - } - try { - Object result = ViewDebug.invokeViewMethod(targetView, method, args); - Class<?> returnType = method.getReturnType(); - byte[] returnValue = serializeReturnValue(returnType, returnType.cast(result)); + byte[] returnValue = ViewDebug.invokeViewMethod(targetView, methodName, in); return new Chunk(CHUNK_VUOP, returnValue, 0, returnValue.length); + } catch (ViewDebug.ViewMethodInvocationSerializationException e) { + return createFailChunk(ERR_INVALID_PARAM, e.getMessage()); } catch (Exception e) { - Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage()); - String msg = e.getCause().getMessage(); - if (msg == null) { - msg = e.getCause().toString(); - } - return createFailChunk(ERR_EXCEPTION, msg); + return createFailChunk(ERR_EXCEPTION, e.getMessage()); } } @@ -431,175 +394,4 @@ public class DdmHandleViewDebug extends DdmHandle { byte[] data = b.toByteArray(); return new Chunk(CHUNK_VUOP, data, 0, data.length); } - - /** - * Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in} - * buffer. - * - * The length of {@code args} determines how many arguments are read. The {@code argTypes} must - * be the same length, and will be set to the argument types of the data read. - * - * @hide - */ - @VisibleForTesting - public static void deserializeMethodParameters( - Object[] args, Class<?>[] argTypes, ByteBuffer in) throws - ViewMethodInvocationSerializationException { - checkArgument(args.length == argTypes.length); - - for (int i = 0; i < args.length; i++) { - char typeSignature = in.getChar(); - boolean isArray = typeSignature == SIG_ARRAY; - if (isArray) { - char arrayType = in.getChar(); - if (arrayType != SIG_BYTE) { - // This implementation only supports byte-arrays for now. - throw new ViewMethodInvocationSerializationException( - "Unsupported array parameter type (" + typeSignature - + ") to invoke view method @argument " + i); - } - - int arrayLength = in.getInt(); - if (arrayLength > in.remaining()) { - // The sender did not actually sent the specified amount of bytes. This - // avoids a malformed packet to trigger an out-of-memory error. - throw new BufferUnderflowException(); - } - - byte[] byteArray = new byte[arrayLength]; - in.get(byteArray); - - argTypes[i] = byte[].class; - args[i] = byteArray; - } else { - switch (typeSignature) { - case SIG_BOOLEAN: - argTypes[i] = boolean.class; - args[i] = in.get() != 0; - break; - case SIG_BYTE: - argTypes[i] = byte.class; - args[i] = in.get(); - break; - case SIG_CHAR: - argTypes[i] = char.class; - args[i] = in.getChar(); - break; - case SIG_SHORT: - argTypes[i] = short.class; - args[i] = in.getShort(); - break; - case SIG_INT: - argTypes[i] = int.class; - args[i] = in.getInt(); - break; - case SIG_LONG: - argTypes[i] = long.class; - args[i] = in.getLong(); - break; - case SIG_FLOAT: - argTypes[i] = float.class; - args[i] = in.getFloat(); - break; - case SIG_DOUBLE: - argTypes[i] = double.class; - args[i] = in.getDouble(); - break; - case SIG_STRING: { - argTypes[i] = String.class; - int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort()); - byte[] rawStringBuffer = new byte[stringUtf8ByteCount]; - in.get(rawStringBuffer); - args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8); - break; - } - default: - Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature); - throw new ViewMethodInvocationSerializationException( - "Unsupported parameter type (" + typeSignature - + ") to invoke view method."); - } - } - - } - } - - /** - * Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD. - * @hide - */ - @VisibleForTesting - public static byte[] serializeReturnValue(Class<?> type, Object value) - throws ViewMethodInvocationSerializationException, IOException { - ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024); - DataOutputStream dos = new DataOutputStream(byteOutStream); - - if (type.isArray()) { - if (!type.equals(byte[].class)) { - // Only byte arrays are supported currently. - throw new ViewMethodInvocationSerializationException( - "Unsupported array return type (" + type + ")"); - } - byte[] byteArray = (byte[]) value; - dos.writeChar(SIG_ARRAY); - dos.writeChar(SIG_BYTE); - dos.writeInt(byteArray.length); - dos.write(byteArray); - } else if (boolean.class.equals(type)) { - dos.writeChar(SIG_BOOLEAN); - dos.write((boolean) value ? 1 : 0); - } else if (byte.class.equals(type)) { - dos.writeChar(SIG_BYTE); - dos.writeByte((byte) value); - } else if (char.class.equals(type)) { - dos.writeChar(SIG_CHAR); - dos.writeChar((char) value); - } else if (short.class.equals(type)) { - dos.writeChar(SIG_SHORT); - dos.writeShort((short) value); - } else if (int.class.equals(type)) { - dos.writeChar(SIG_INT); - dos.writeInt((int) value); - } else if (long.class.equals(type)) { - dos.writeChar(SIG_LONG); - dos.writeLong((long) value); - } else if (double.class.equals(type)) { - dos.writeChar(SIG_DOUBLE); - dos.writeDouble((double) value); - } else if (float.class.equals(type)) { - dos.writeChar(SIG_FLOAT); - dos.writeFloat((float) value); - } else if (String.class.equals(type)) { - dos.writeChar(SIG_STRING); - dos.writeUTF(value != null ? (String) value : ""); - } else { - dos.writeChar(SIG_VOID); - } - - return byteOutStream.toByteArray(); - } - - // Prefixes for simple primitives. These match the JNI definitions. - private static final char SIG_ARRAY = '['; - private static final char SIG_BOOLEAN = 'Z'; - private static final char SIG_BYTE = 'B'; - private static final char SIG_SHORT = 'S'; - private static final char SIG_CHAR = 'C'; - private static final char SIG_INT = 'I'; - private static final char SIG_LONG = 'J'; - private static final char SIG_FLOAT = 'F'; - private static final char SIG_DOUBLE = 'D'; - private static final char SIG_VOID = 'V'; - // Prefixes for some commonly used objects - private static final char SIG_STRING = 'R'; - - /** - * @hide - */ - @VisibleForTesting - public static class ViewMethodInvocationSerializationException extends Exception { - ViewMethodInvocationSerializationException(String message) { - super(message); - } - } } diff --git a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl index d51e62e709c2..1488cff3a35e 100644 --- a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl +++ b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl @@ -15,6 +15,8 @@ */ package android.hardware.biometrics; +import android.hardware.biometrics.BiometricSourceType; + /** * Low-level callback interface between <Biometric>Manager and <Auth>Service. Allows core system * services (e.g. SystemUI) to register a listener for updates about the current state of biometric @@ -49,4 +51,15 @@ oneway interface AuthenticationStateListener { * @param userId The user Id for the requested authentication */ void onAuthenticationFailed(int requestReason, int userId); + + /** + * Defines behavior in response to biometric being acquired. + * @param biometricSourceType identifies [BiometricSourceType] biometric was acquired for + * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication + * @param acquiredInfo [BiometricFingerprintConstants.FingerprintAcquired] int corresponding to + * a known acquired message. + */ + void onAuthenticationAcquired( + in BiometricSourceType biometricSourceType, int requestReason, int acquiredInfo + ); } diff --git a/core/java/android/os/VibrationAttributes.java b/core/java/android/os/VibrationAttributes.java index 5078dc351f6f..46705a31f395 100644 --- a/core/java/android/os/VibrationAttributes.java +++ b/core/java/android/os/VibrationAttributes.java @@ -29,6 +29,7 @@ import java.util.Objects; /** * Encapsulates a collection of attributes describing information about a vibration. */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class VibrationAttributes implements Parcelable { private static final String TAG = "VibrationAttributes"; @@ -463,6 +464,7 @@ public final class VibrationAttributes implements Parcelable { * Builder class for {@link VibrationAttributes} objects. * By default, all information is set to UNKNOWN. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public static final class Builder { private int mUsage = USAGE_UNKNOWN; private int mOriginalAudioUsage = AudioAttributes.USAGE_UNKNOWN; diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 25e0eca12bf3..4f1fb40ab214 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -7,7 +7,7 @@ * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software + * Unless required by applicable law or agreed to in writing, softwareViewDebug * 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 @@ -16,6 +16,8 @@ package android.view; +import static com.android.internal.util.Preconditions.checkArgument; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -34,10 +36,13 @@ import android.os.Debug; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.util.Base64; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; +import com.android.internal.annotations.VisibleForTesting; + import libcore.util.HexEncoding; import java.io.BufferedOutputStream; @@ -54,9 +59,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; import java.util.Arrays; import java.util.HashMap; @@ -67,7 +74,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import java.util.stream.Stream; @@ -76,6 +82,9 @@ import java.util.stream.Stream; * Various debugging/tracing tools related to {@link View} and the view hierarchy. */ public class ViewDebug { + + private static final String TAG = "ViewDebug"; + /** * @deprecated This flag is now unused */ @@ -425,6 +434,7 @@ public class ViewDebug { private static final String REMOTE_PROFILE = "PROFILE"; private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS"; private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST"; + private static final String REMOTE_COMMAND_INVOKE_METHOD = "INVOKE_METHOD"; private static HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> sExportProperties; private static HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]> @@ -555,6 +565,8 @@ public class ViewDebug { requestLayout(view, params[0]); } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) { profile(view, clientStream, params[0]); + } else if (REMOTE_COMMAND_INVOKE_METHOD.equals(command)) { + invokeViewMethod(view, clientStream, params); } } } @@ -1825,46 +1837,84 @@ public class ViewDebug { Log.d(tag, sb.toString()); } + private static void invokeViewMethod(View root, OutputStream clientStream, String[] params) + throws IOException { + BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024); + try { + if (params.length < 2) { + throw new IllegalArgumentException("Missing parameter"); + } + View targetView = findView(root, params[0]); + if (targetView == null) { + throw new IllegalArgumentException("View not found: " + params[0]); + } + String method = params[1]; + ByteBuffer args = ByteBuffer.wrap(params.length < 2 + ? new byte[0] + : Base64.decode(params[2], Base64.NO_WRAP)); + byte[] result = invokeViewMethod(targetView, method, args); + out.write("1"); + out.newLine(); + out.write(Base64.encodeToString(result, Base64.NO_WRAP)); + out.newLine(); + } catch (Exception e) { + out.write("-1"); + out.newLine(); + out.write(e.getMessage()); + out.newLine(); + } finally { + out.close(); + } + } + /** * Invoke a particular method on given view. * The given method is always invoked on the UI thread. The caller thread will stall until the * method invocation is complete. Returns an object equal to the result of the method * invocation, null if the method is declared to return void + * @param params all the method parameters encoded in a byteArray * @throws Exception if the method invocation caused any exception * @hide */ - public static Object invokeViewMethod(final View view, final Method method, - final Object[] args) { - final CountDownLatch latch = new CountDownLatch(1); - final AtomicReference<Object> result = new AtomicReference<Object>(); - final AtomicReference<Throwable> exception = new AtomicReference<Throwable>(); - - view.post(new Runnable() { - @Override - public void run() { - try { - result.set(method.invoke(view, args)); - } catch (InvocationTargetException e) { - exception.set(e.getCause()); - } catch (Exception e) { - exception.set(e); - } + public static byte[] invokeViewMethod(View targetView, String methodName, ByteBuffer params) + throws ViewMethodInvocationSerializationException { + Class<?>[] argTypes; + Object[] args; + if (!params.hasRemaining()) { + argTypes = new Class<?>[0]; + args = new Object[0]; + } else { + int nArgs = params.getInt(); + argTypes = new Class<?>[nArgs]; + args = new Object[nArgs]; - latch.countDown(); - } - }); + deserializeMethodParameters(args, argTypes, params); + } + Method method; try { - latch.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); + method = targetView.getClass().getMethod(methodName, argTypes); + } catch (NoSuchMethodException e) { + Log.e(TAG, "No such method: " + e.getMessage()); + throw new ViewMethodInvocationSerializationException( + "No such method: " + e.getMessage()); } - if (exception.get() != null) { - throw new RuntimeException(exception.get()); + try { + // Invoke the method on Views handler + FutureTask<Object> task = new FutureTask<>(() -> method.invoke(targetView, args)); + targetView.post(task); + Object result = task.get(); + Class<?> returnType = method.getReturnType(); + return serializeReturnValue(returnType, returnType.cast(result)); + } catch (Exception e) { + Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage()); + String msg = e.getCause().getMessage(); + if (msg == null) { + msg = e.getCause().toString(); + } + throw new RuntimeException(msg); } - - return result.get(); } /** @@ -1961,4 +2011,175 @@ public class ViewDebug { */ Bitmap createBitmap(); } + + /** + * Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in} + * buffer. + * + * The length of {@code args} determines how many arguments are read. The {@code argTypes} must + * be the same length, and will be set to the argument types of the data read. + * + * @hide + */ + @VisibleForTesting + public static void deserializeMethodParameters( + Object[] args, Class<?>[] argTypes, ByteBuffer in) throws + ViewMethodInvocationSerializationException { + checkArgument(args.length == argTypes.length); + + for (int i = 0; i < args.length; i++) { + char typeSignature = in.getChar(); + boolean isArray = typeSignature == SIG_ARRAY; + if (isArray) { + char arrayType = in.getChar(); + if (arrayType != SIG_BYTE) { + // This implementation only supports byte-arrays for now. + throw new ViewMethodInvocationSerializationException( + "Unsupported array parameter type (" + typeSignature + + ") to invoke view method @argument " + i); + } + + int arrayLength = in.getInt(); + if (arrayLength > in.remaining()) { + // The sender did not actually sent the specified amount of bytes. This + // avoids a malformed packet to trigger an out-of-memory error. + throw new BufferUnderflowException(); + } + + byte[] byteArray = new byte[arrayLength]; + in.get(byteArray); + + argTypes[i] = byte[].class; + args[i] = byteArray; + } else { + switch (typeSignature) { + case SIG_BOOLEAN: + argTypes[i] = boolean.class; + args[i] = in.get() != 0; + break; + case SIG_BYTE: + argTypes[i] = byte.class; + args[i] = in.get(); + break; + case SIG_CHAR: + argTypes[i] = char.class; + args[i] = in.getChar(); + break; + case SIG_SHORT: + argTypes[i] = short.class; + args[i] = in.getShort(); + break; + case SIG_INT: + argTypes[i] = int.class; + args[i] = in.getInt(); + break; + case SIG_LONG: + argTypes[i] = long.class; + args[i] = in.getLong(); + break; + case SIG_FLOAT: + argTypes[i] = float.class; + args[i] = in.getFloat(); + break; + case SIG_DOUBLE: + argTypes[i] = double.class; + args[i] = in.getDouble(); + break; + case SIG_STRING: { + argTypes[i] = String.class; + int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort()); + byte[] rawStringBuffer = new byte[stringUtf8ByteCount]; + in.get(rawStringBuffer); + args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8); + break; + } + default: + Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature); + throw new ViewMethodInvocationSerializationException( + "Unsupported parameter type (" + typeSignature + + ") to invoke view method."); + } + } + + } + } + + /** + * Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD. + * @hide + */ + @VisibleForTesting + public static byte[] serializeReturnValue(Class<?> type, Object value) + throws ViewMethodInvocationSerializationException, IOException { + ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024); + DataOutputStream dos = new DataOutputStream(byteOutStream); + + if (type.isArray()) { + if (!type.equals(byte[].class)) { + // Only byte arrays are supported currently. + throw new ViewMethodInvocationSerializationException( + "Unsupported array return type (" + type + ")"); + } + byte[] byteArray = (byte[]) value; + dos.writeChar(SIG_ARRAY); + dos.writeChar(SIG_BYTE); + dos.writeInt(byteArray.length); + dos.write(byteArray); + } else if (boolean.class.equals(type)) { + dos.writeChar(SIG_BOOLEAN); + dos.write((boolean) value ? 1 : 0); + } else if (byte.class.equals(type)) { + dos.writeChar(SIG_BYTE); + dos.writeByte((byte) value); + } else if (char.class.equals(type)) { + dos.writeChar(SIG_CHAR); + dos.writeChar((char) value); + } else if (short.class.equals(type)) { + dos.writeChar(SIG_SHORT); + dos.writeShort((short) value); + } else if (int.class.equals(type)) { + dos.writeChar(SIG_INT); + dos.writeInt((int) value); + } else if (long.class.equals(type)) { + dos.writeChar(SIG_LONG); + dos.writeLong((long) value); + } else if (double.class.equals(type)) { + dos.writeChar(SIG_DOUBLE); + dos.writeDouble((double) value); + } else if (float.class.equals(type)) { + dos.writeChar(SIG_FLOAT); + dos.writeFloat((float) value); + } else if (String.class.equals(type)) { + dos.writeChar(SIG_STRING); + dos.writeUTF(value != null ? (String) value : ""); + } else { + dos.writeChar(SIG_VOID); + } + + return byteOutStream.toByteArray(); + } + + // Prefixes for simple primitives. These match the JNI definitions. + private static final char SIG_ARRAY = '['; + private static final char SIG_BOOLEAN = 'Z'; + private static final char SIG_BYTE = 'B'; + private static final char SIG_SHORT = 'S'; + private static final char SIG_CHAR = 'C'; + private static final char SIG_INT = 'I'; + private static final char SIG_LONG = 'J'; + private static final char SIG_FLOAT = 'F'; + private static final char SIG_DOUBLE = 'D'; + private static final char SIG_VOID = 'V'; + // Prefixes for some commonly used objects + private static final char SIG_STRING = 'R'; + + /** + * @hide + */ + @VisibleForTesting + public static class ViewMethodInvocationSerializationException extends Exception { + ViewMethodInvocationSerializationException(String message) { + super(message); + } + } } diff --git a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java index ce6af49eef0b..5cda3f2b2bc0 100644 --- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java +++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java @@ -16,16 +16,30 @@ package com.android.internal.widget; +import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT; +import static android.app.Notification.CallStyle.USE_NEW_ACTION_LAYOUT; +import static android.text.style.DynamicDrawableSpan.ALIGN_CENTER; + +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.TypedArray; import android.graphics.BlendMode; +import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.graphics.drawable.DrawableWrapper; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.graphics.drawable.RippleDrawable; +import android.text.SpannableStringBuilder; +import android.text.TextPaint; +import android.text.style.ImageSpan; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; import android.util.AttributeSet; +import android.util.Log; import android.view.RemotableViewMethod; import android.widget.Button; import android.widget.RemoteViews; @@ -43,6 +57,14 @@ public class EmphasizedNotificationButton extends Button { private final GradientDrawable mBackground; private boolean mPriority; + private int mInitialDrawablePadding; + private int mIconSize; + + private Drawable mIconToGlue; + private CharSequence mLabelToGlue; + private int mGluedLayoutDirection = LAYOUT_DIRECTION_UNDEFINED; + private boolean mGluePending; + public EmphasizedNotificationButton(Context context) { this(context, null); } @@ -58,10 +80,25 @@ public class EmphasizedNotificationButton extends Button { public EmphasizedNotificationButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + mRipple = (RippleDrawable) getBackground(); mRipple.mutate(); DrawableWrapper inset = (DrawableWrapper) mRipple.getDrawable(0); mBackground = (GradientDrawable) inset.getDrawable(); + + mIconSize = mContext.getResources().getDimensionPixelSize( + R.dimen.notification_actions_icon_drawable_size); + + try (TypedArray typedArray = context.obtainStyledAttributes( + attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes)) { + mInitialDrawablePadding = typedArray.getDimensionPixelSize( + android.R.styleable.TextView_drawablePadding, 0); + } + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "iconSize = " + mIconSize + "px, " + + "initialDrawablePadding = " + mInitialDrawablePadding + "px"); + } } @RemotableViewMethod @@ -95,19 +132,248 @@ public class EmphasizedNotificationButton extends Button { return () -> setImageDrawable(drawable); } - private void setImageDrawable(Drawable drawable) { + private void setImageDrawable(@Nullable Drawable drawable) { if (drawable != null) { - drawable.mutate(); - drawable.setTintList(getTextColors()); - drawable.setTintBlendMode(BlendMode.SRC_IN); - int iconSize = mContext.getResources().getDimensionPixelSize( - R.dimen.notification_actions_icon_drawable_size); - drawable.setBounds(0, 0, iconSize, iconSize); + prepareIcon(drawable); } setCompoundDrawablesRelative(drawable, null, null, null); } /** + * Sets an icon to be 'glued' to the label when this button is displayed, so the icon will stay + * with the text if the button is wider than needed and the text isn't start-aligned. + * + * As with {@link #setImageIcon(Icon)}, the Icon will have its size constrained and will be set + * to the same color as the text, and this must be called after {@link #setTextColor(int)} for + * the latter to work. + * + * This must be called along with {@link #glueLabel(CharSequence)}, in any order, before the + * button is displayed. + */ + @RemotableViewMethod(asyncImpl = "glueIconAsync") + public void glueIcon(@Nullable Icon icon) { + final Drawable drawable = icon == null ? null : icon.loadDrawable(mContext); + setIconToGlue(drawable); + } + + /** + * @hide + */ + @RemotableViewMethod + public Runnable glueIconAsync(@Nullable Icon icon) { + final Drawable drawable = icon == null ? null : icon.loadDrawable(mContext); + return () -> setIconToGlue(drawable); + } + + private void setIconToGlue(@Nullable Drawable icon) { + if (!USE_NEW_ACTION_LAYOUT) { + Log.e(TAG, "glueIcon: new action layout disabled; doing nothing"); + return; + } + + prepareIcon(icon); + + mIconToGlue = icon; + mGluePending = true; + + glueIconAndLabelIfNeeded(); + } + + private void prepareIcon(@NonNull Drawable drawable) { + drawable.mutate(); + drawable.setTintList(getTextColors()); + drawable.setTintBlendMode(BlendMode.SRC_IN); + drawable.setBounds(0, 0, mIconSize, mIconSize); + } + + /** + * Sets a label to be 'glued' to the icon when this button is displayed, so the icon will stay + * with the text if the button is wider than needed and the text isn't start-aligned. + * + * This must be called along with {@link #glueIcon(Icon)}, in any order, before the button is + * displayed. + */ + @RemotableViewMethod(asyncImpl = "glueLabelAsync") + public void glueLabel(@Nullable CharSequence label) { + setLabelToGlue(label); + } + + /** + * @hide + */ + @RemotableViewMethod + public Runnable glueLabelAsync(@Nullable CharSequence label) { + return () -> setLabelToGlue(label); + } + + private void setLabelToGlue(@Nullable CharSequence label) { + if (!USE_NEW_ACTION_LAYOUT) { + Log.e(TAG, "glueLabel: new action layout disabled; doing nothing"); + return; + } + + mLabelToGlue = label; + mGluePending = true; + + glueIconAndLabelIfNeeded(); + } + + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + super.onRtlPropertiesChanged(layoutDirection); + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "onRtlPropertiesChanged: layoutDirection = " + layoutDirection + ", " + + "gluedLayoutDirection = " + mGluedLayoutDirection); + } + + if (layoutDirection != mGluedLayoutDirection) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "onRtlPropertiesChanged: layout direction changed; regluing"); + } + mGluePending = true; + } + + glueIconAndLabelIfNeeded(); + } + + private void glueIconAndLabelIfNeeded() { + // Don't need to glue: + + if (!mGluePending) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "glueIconAndLabelIfNeeded: glue not pending; doing nothing"); + } + return; + } + + if (mIconToGlue == null && mLabelToGlue == null) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "glueIconAndLabelIfNeeded: no icon or label to glue; doing nothing"); + } + mGluePending = false; + return; + } + + if (!USE_NEW_ACTION_LAYOUT) { + Log.e(TAG, "glueIconAndLabelIfNeeded: new action layout disabled; doing nothing"); + return; + } + + // Not ready to glue yet: + + if (!isLayoutDirectionResolved()) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "glueIconAndLabelIfNeeded: " + + "layout direction not resolved; doing nothing"); + } + return; + } + + // Ready to glue but don't have an icon *and* a label: + // + // (Note that this will *not* happen while the button is being initialized, since we won't + // be ready to glue. This can only happen if the button is initialized and displayed and + // *then* someone calls glueIcon or glueLabel. + + if (mIconToGlue == null) { + Log.w(TAG, "glueIconAndLabelIfNeeded: label glued without icon; doing nothing"); + return; + } + + if (mLabelToGlue == null) { + Log.w(TAG, "glueIconAndLabelIfNeeded: icon glued without label; doing nothing"); + return; + } + + // Can't glue: + + final int layoutDirection = getLayoutDirection(); + if (layoutDirection != LAYOUT_DIRECTION_LTR && layoutDirection != LAYOUT_DIRECTION_RTL) { + Log.e(TAG, "glueIconAndLabelIfNeeded: " + + "resolved layout direction neither LTR nor RTL; " + + "doing nothing"); + return; + } + + // No excuses left, let's glue it! + + glueIconAndLabel(layoutDirection); + + mGluePending = false; + mGluedLayoutDirection = layoutDirection; + } + + // Unicode replacement character + private static final String IMAGE_SPAN_TEXT = "\ufffd"; + + // Unicode no-break space + private static final String SPACER_SPAN_TEXT = "\u00a0"; + + private static final String LEFT_TO_RIGHT_ISOLATE = "\u2066"; + private static final String RIGHT_TO_LEFT_ISOLATE = "\u2067"; + private static final String FIRST_STRONG_ISOLATE = "\u2068"; + private static final String POP_DIRECTIONAL_ISOLATE = "\u2069"; + + private void glueIconAndLabel(int layoutDirection) { + final boolean rtlLayout = layoutDirection == LAYOUT_DIRECTION_RTL; + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "glueIconAndLabel: " + + "icon = " + mIconToGlue + ", " + + "iconSize = " + mIconSize + "px, " + + "initialDrawablePadding = " + mInitialDrawablePadding + "px, " + + "labelToGlue.length = " + mLabelToGlue.length() + ", " + + "rtlLayout = " + rtlLayout); + } + + logIfTextDirectionNotFirstStrong(); + + final SpannableStringBuilder builder = new SpannableStringBuilder(); + + // The text direction of the label might not match the layout direction of the button, so + // wrap the entire string in a LEFT-TO-RIGHT ISOLATE or RIGHT-TO-LEFT ISOLATE to match the + // layout direction. This puts the icon, padding, and label in the right order. + builder.append(rtlLayout ? RIGHT_TO_LEFT_ISOLATE : LEFT_TO_RIGHT_ISOLATE); + + appendSpan(builder, IMAGE_SPAN_TEXT, new ImageSpan(mIconToGlue, ALIGN_CENTER)); + appendSpan(builder, SPACER_SPAN_TEXT, new SpacerSpan(mInitialDrawablePadding)); + + // If the text and layout directions are different, we would end up with the *label* in the + // wrong direction, so wrap the label in a FIRST STRONG ISOLATE. This triggers the same + // automatic text direction heuristic that Android uses by default. + builder.append(FIRST_STRONG_ISOLATE); + + appendSpan(builder, mLabelToGlue, new CenterBesideImageSpan(mIconSize)); + + builder.append(POP_DIRECTIONAL_ISOLATE); + builder.append(POP_DIRECTIONAL_ISOLATE); + + setText(builder); + } + + private void logIfTextDirectionNotFirstStrong() { + if (!isTextDirectionResolved()) { + Log.e(TAG, "glueIconAndLabel: text direction not resolved; " + + "letting View assume FIRST STRONG"); + } + final int textDirection = getTextDirection(); + if (textDirection != TEXT_DIRECTION_FIRST_STRONG) { + Log.w(TAG, "glueIconAndLabel: " + + "expected text direction TEXT_DIRECTION_FIRST_STRONG " + + "but found " + textDirection + "; " + + "will use a FIRST STRONG ISOLATE regardless"); + } + } + + private void appendSpan(SpannableStringBuilder builder, CharSequence text, Object span) { + final int spanStart = builder.length(); + builder.append(text); + final int spanEnd = builder.length(); + builder.setSpan(span, spanStart, spanEnd, 0); + } + + /** * Sets whether this view is a priority over its peers (which affects width). * Specifically, this is used by {@link NotificationActionListLayout} to give this view width * priority ahead of user-defined buttons when allocating horizontal space. @@ -123,4 +389,104 @@ public class EmphasizedNotificationButton extends Button { public boolean isPriority() { return mPriority; } + + private static class SpacerSpan extends ReplacementSpan { + private int mWidth; + + SpacerSpan(int width) { + mWidth = width; + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "width = " + mWidth + "px"); + } + } + + + @Override + public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, + @Nullable Paint.FontMetricsInt fontMetrics) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "getSize returning " + mWidth + "px"); + } + + return mWidth; + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, + float x, int top, int y, int bottom, @NonNull Paint paint) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "drawing nothing"); + } + + // Draw nothing, it's a spacer. + } + + private static final String TAG = "SpacerSpan"; + } + + private static class CenterBesideImageSpan extends MetricAffectingSpan { + private int mImageHeight; + + private boolean mMeasured; + private int mBaselineShiftOffset; + + CenterBesideImageSpan(int imageHeight) { + mImageHeight = imageHeight; + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "imageHeight = " + mImageHeight + "px"); + } + } + + @Override + public void updateMeasureState(@NonNull TextPaint textPaint) { + final int textHeight = (int) -textPaint.ascent(); + + /* + * We only need to shift the text *up* if the text is shorter than the image; ImageSpan + * with ALIGN_CENTER will shift the *image* up if the text is taller than the image. + */ + if (textHeight < mImageHeight) { + mBaselineShiftOffset = -(mImageHeight - textHeight) / 2; + } else { + mBaselineShiftOffset = 0; + } + + mMeasured = true; + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "updateMeasureState: " + + "imageHeight = " + mImageHeight + "px, " + + "textHeight = " + textHeight + "px, " + + "baselineShiftOffset = " + mBaselineShiftOffset + "px"); + } + + textPaint.baselineShift += mBaselineShiftOffset; + } + + @Override + public void updateDrawState(TextPaint textPaint) { + if (textPaint == null) { + Log.e(TAG, "updateDrawState: textPaint is null; doing nothing"); + return; + } + + if (!mMeasured) { + Log.e(TAG, "updateDrawState: called without measure; doing nothing"); + return; + } + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "updateDrawState: " + + "baselineShiftOffset = " + mBaselineShiftOffset + "px"); + } + + textPaint.baselineShift += mBaselineShiftOffset; + } + + private static final String TAG = "CenterBesideImageSpan"; + } + + private static final String TAG = "EmphasizedNotificationButton"; } diff --git a/core/java/com/android/internal/widget/NotificationActionListLayout.java b/core/java/com/android/internal/widget/NotificationActionListLayout.java index a7a69c9e43fb..69d254499ef4 100644 --- a/core/java/com/android/internal/widget/NotificationActionListLayout.java +++ b/core/java/com/android/internal/widget/NotificationActionListLayout.java @@ -16,12 +16,16 @@ package com.android.internal.widget; +import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT; +import static android.app.Notification.CallStyle.USE_NEW_ACTION_LAYOUT; + import android.annotation.DimenRes; import android.app.Notification; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; +import android.util.Log; import android.view.Gravity; import android.view.RemotableViewMethod; import android.view.View; @@ -41,13 +45,13 @@ import java.util.Comparator; */ @RemoteViews.RemoteView public class NotificationActionListLayout extends LinearLayout { - private final int mGravity; private int mTotalWidth = 0; private int mExtraStartPadding = 0; private ArrayList<TextViewInfo> mMeasureOrderTextViews = new ArrayList<>(); private ArrayList<View> mMeasureOrderOther = new ArrayList<>(); private boolean mEmphasizedMode; + private boolean mEvenlyDividedMode; private int mDefaultPaddingBottom; private int mDefaultPaddingTop; private int mEmphasizedPaddingTop; @@ -124,6 +128,42 @@ public class NotificationActionListLayout extends LinearLayout { } } + private int measureAndReturnEvenlyDividedWidth(int heightMeasureSpec, int innerWidth) { + final int numChildren = getChildCount(); + int childMarginSum = 0; + for (int i = 0; i < numChildren; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + childMarginSum += lp.leftMargin + lp.rightMargin; + } + } + + final int innerWidthMinusChildMargins = innerWidth - childMarginSum; + final int childWidth = innerWidthMinusChildMargins / mNumNotGoneChildren; + final int childWidthMeasureSpec = + MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "measuring evenly divided width: " + + "numChildren = " + numChildren + ", " + + "innerWidth = " + innerWidth + "px, " + + "childMarginSum = " + childMarginSum + "px, " + + "innerWidthMinusChildMargins = " + innerWidthMinusChildMargins + "px, " + + "childWidth = " + childWidth + "px, " + + "childWidthMeasureSpec = " + MeasureSpec.toString(childWidthMeasureSpec)); + } + + for (int i = 0; i < numChildren; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.measure(childWidthMeasureSpec, heightMeasureSpec); + } + } + + return innerWidth; + } + private int measureAndGetUsedWidth(int widthMeasureSpec, int heightMeasureSpec, int innerWidth, boolean collapsePriorityActions) { final int numChildren = getChildCount(); @@ -208,11 +248,16 @@ public class NotificationActionListLayout extends LinearLayout { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { countAndRebuildMeasureOrder(); final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight; - int usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth, - false /* collapsePriorityButtons */); - if (mNumPriorityChildren != 0 && usedWidth >= innerWidth) { + int usedWidth; + if (mEvenlyDividedMode) { + usedWidth = measureAndReturnEvenlyDividedWidth(heightMeasureSpec, innerWidth); + } else { usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth, - true /* collapsePriorityButtons */); + false /* collapsePriorityButtons */); + if (mNumPriorityChildren != 0 && usedWidth >= innerWidth) { + usedWidth = measureAndGetUsedWidth(widthMeasureSpec, heightMeasureSpec, innerWidth, + true /* collapsePriorityButtons */); + } } mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft + mExtraStartPadding; @@ -352,6 +397,38 @@ public class NotificationActionListLayout extends LinearLayout { } /** + * Sets whether the available width should be distributed evenly among the action buttons. + * + * When enabled, the available width (after subtracting this layout's padding and all of the + * buttons' margins) is divided by the number of (not-GONE) buttons, and each button is forced + * to that exact width, even if it is less <em>or more</em> width than they need. + * + * When disabled, the available width is allocated as buttons need; if that exceeds the + * available width, priority buttons are collapsed to just their icon to save space. + * + * @param evenlyDividedMode whether to enable evenly divided mode + */ + @RemotableViewMethod + public void setEvenlyDividedMode(boolean evenlyDividedMode) { + if (evenlyDividedMode && !USE_NEW_ACTION_LAYOUT) { + Log.e(TAG, "setEvenlyDividedMode(true) called with new action layout disabled; " + + "leaving evenly divided mode disabled"); + return; + } + + if (evenlyDividedMode == mEvenlyDividedMode) { + return; + } + + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.v(TAG, "evenlyDividedMode changed to " + evenlyDividedMode + "; " + + "requesting layout"); + } + mEvenlyDividedMode = evenlyDividedMode; + requestLayout(); + } + + /** * Set whether the list is in a mode where some actions are emphasized. This will trigger an * equal measuring where all actions are full height and change a few parameters like * the padding. @@ -410,4 +487,5 @@ public class NotificationActionListLayout extends LinearLayout { } } + private static final String TAG = "NotificationActionListLayout"; } diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index 513e022f8069..1b25d7fa32a4 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -208,6 +208,7 @@ android_ravenwood_test { "testng", ], srcs: [ + "src/android/app/ActivityManagerTest.java", "src/android/content/pm/PackageManagerTest.java", "src/android/content/pm/UserInfoTest.java", "src/android/database/CursorWindowTest.java", diff --git a/core/tests/coretests/src/android/app/ActivityManagerTest.java b/core/tests/coretests/src/android/app/ActivityManagerTest.java new file mode 100644 index 000000000000..d930e4d79a3a --- /dev/null +++ b/core/tests/coretests/src/android/app/ActivityManagerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import static android.app.ActivityManager.PROCESS_STATE_SERVICE; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.os.UserHandle; +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; + +@RunWith(AndroidJUnit4.class) +public class ActivityManagerTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Test + public void testSimple() throws Exception { + assertTrue(ActivityManager.isSystemReady()); + assertFalse(ActivityManager.isUserAMonkey()); + assertNotEquals(UserHandle.USER_NULL, ActivityManager.getCurrentUser()); + } + + @Test + public void testCapabilities() throws Exception { + // For the moment mostly want to confirm we don't crash + assertNotNull(ActivityManager.getCapabilitiesSummary(~0)); + ActivityManager.printCapabilitiesFull(new PrintWriter(new ByteArrayOutputStream()), ~0); + ActivityManager.printCapabilitiesSummary(new PrintWriter(new ByteArrayOutputStream()), ~0); + ActivityManager.printCapabilitiesSummary(new StringBuilder(), ~0); + } + + @Test + public void testProcState() throws Exception { + // For the moment mostly want to confirm we don't crash + assertNotNull(ActivityManager.procStateToString(PROCESS_STATE_SERVICE)); + assertNotNull(ActivityManager.processStateAmToProto(PROCESS_STATE_SERVICE)); + assertTrue(ActivityManager.isProcStateBackground(PROCESS_STATE_SERVICE)); + assertFalse(ActivityManager.isProcStateCached(PROCESS_STATE_SERVICE)); + assertFalse(ActivityManager.isForegroundService(PROCESS_STATE_SERVICE)); + assertFalse(ActivityManager.isProcStateConsideredInteraction(PROCESS_STATE_SERVICE)); + } + + @Test + public void testStartResult() throws Exception { + // For the moment mostly want to confirm we don't crash + assertTrue(ActivityManager.isStartResultSuccessful(50)); + assertTrue(ActivityManager.isStartResultFatalError(-50)); + } + + @Test + public void testRestrictionLevel() throws Exception { + // For the moment mostly want to confirm we don't crash + assertNotNull(ActivityManager.restrictionLevelToName( + ActivityManager.RESTRICTION_LEVEL_HIBERNATION)); + } +} diff --git a/core/tests/coretests/src/android/ddm/OWNERS b/core/tests/coretests/src/android/ddm/OWNERS deleted file mode 100644 index c8be1919fb93..000000000000 --- a/core/tests/coretests/src/android/ddm/OWNERS +++ /dev/null @@ -1 +0,0 @@ -michschn@google.com diff --git a/core/tests/coretests/src/android/os/HandlerThreadTest.java b/core/tests/coretests/src/android/os/HandlerThreadTest.java index 0bac1c728f3b..1ad71da7c2d1 100644 --- a/core/tests/coretests/src/android/os/HandlerThreadTest.java +++ b/core/tests/coretests/src/android/os/HandlerThreadTest.java @@ -28,15 +28,20 @@ import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; +import org.junit.Assume; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class HandlerThreadTest { private static final int TEST_WHAT = 1; - @Rule + @Rule(order = 1) + public ExpectedException mThrown = ExpectedException.none(); + + @Rule(order = 2) public final RavenwoodRule mRavenwood = new RavenwoodRule(); private boolean mGotMessage = false; @@ -112,4 +117,28 @@ public class HandlerThreadTest { assertTrue(mGotMessage); assertEquals(TEST_WHAT, mGotMessageWhat); } + + /** + * Confirm that a background handler thread throwing an exception during a test results in a + * test failure being reported. + */ + @Test + public void testUncaughtExceptionFails() throws Exception { + // For the moment we can only test Ravenwood; on a physical device uncaught exceptions + // are detected, but reported as test failures at a higher level where we can't inspect + Assume.assumeTrue(RavenwoodRule.isOnRavenwood()); + mThrown.expect(IllegalStateException.class); + + final HandlerThread thread = new HandlerThread("HandlerThreadTest"); + thread.start(); + thread.getThreadHandler().post(() -> { + throw new IllegalStateException(); + }); + + // Wait until we've drained past the message above, then terminate test without throwing + // directly; the test harness should notice and report the uncaught exception + while (!thread.getThreadHandler().getLooper().getQueue().isIdle()) { + SystemClock.sleep(10); + } + } } diff --git a/core/tests/coretests/src/android/os/VibrationAttributesTest.java b/core/tests/coretests/src/android/os/VibrationAttributesTest.java new file mode 100644 index 000000000000..f5a81c582b28 --- /dev/null +++ b/core/tests/coretests/src/android/os/VibrationAttributesTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import static org.junit.Assert.assertEquals; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VibrationAttributesTest { + @Test + public void testSimple() throws Exception { + final VibrationAttributes attr = new VibrationAttributes.Builder() + .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) + .setUsage(VibrationAttributes.USAGE_ALARM) + .build(); + + assertEquals(VibrationAttributes.CATEGORY_KEYBOARD, attr.getCategory()); + assertEquals(VibrationAttributes.USAGE_ALARM, attr.getUsage()); + } +} diff --git a/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java b/core/tests/coretests/src/android/view/ViewDebugTest.java index 7248983c741c..45228422b97b 100644 --- a/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java +++ b/core/tests/coretests/src/android/view/ViewDebugTest.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package android.ddm; +package android.view; -import static android.ddm.DdmHandleViewDebug.deserializeMethodParameters; -import static android.ddm.DdmHandleViewDebug.serializeReturnValue; +import static android.view.ViewDebug.deserializeMethodParameters; +import static android.view.ViewDebug.serializeReturnValue; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; -import android.ddm.DdmHandleViewDebug.ViewMethodInvocationSerializationException; import android.platform.test.annotations.Presubmit; +import android.view.ViewDebug.ViewMethodInvocationSerializationException; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -39,7 +39,7 @@ import java.util.Arrays; @RunWith(AndroidJUnit4.class) @SmallTest @Presubmit -public final class DdmHandleViewDebugTest { +public final class ViewDebugTest { // true private static final byte[] SERIALIZED_BOOLEAN_TRUE = {0x00, 0x5A, 1}; diff --git a/media/java/android/media/BluetoothProfileConnectionInfo.java b/media/java/android/media/BluetoothProfileConnectionInfo.java index e4dc1521ae70..0613fc655521 100644 --- a/media/java/android/media/BluetoothProfileConnectionInfo.java +++ b/media/java/android/media/BluetoothProfileConnectionInfo.java @@ -15,6 +15,9 @@ */ package android.media; +import static android.media.audio.Flags.FLAG_SCO_MANAGED_BY_AUDIO; + +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; import android.bluetooth.BluetoothProfile; @@ -174,4 +177,13 @@ public final class BluetoothProfileConnectionInfo implements Parcelable { public boolean isLeOutput() { return mIsLeOutput; } + + /** + * Factory method for <code>BluetoothProfileConnectionInfo</code> for an HFP device. + */ + @FlaggedApi(FLAG_SCO_MANAGED_BY_AUDIO) + public static @NonNull BluetoothProfileConnectionInfo createHfpInfo() { + return new BluetoothProfileConnectionInfo(BluetoothProfile.HEADSET, false, + -1, false); + } } diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 1046d8e9aebb..9742d46b8ae9 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -64,10 +64,8 @@ package android.nfc { } public final class NfcAdapter { - method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean allowTransaction(); method public void disableForegroundDispatch(android.app.Activity); method public void disableReaderMode(android.app.Activity); - method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean disallowTransaction(); method public void enableForegroundDispatch(android.app.Activity, android.app.PendingIntent, android.content.IntentFilter[], String[][]); method public void enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle); method public static android.nfc.NfcAdapter getDefaultAdapter(android.content.Context); @@ -83,6 +81,7 @@ package android.nfc { method @FlaggedApi("android.nfc.enable_nfc_charging") public boolean isWlcEnabled(); method @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public void resetDiscoveryTechnology(@NonNull android.app.Activity); method @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public void setDiscoveryTechnology(@NonNull android.app.Activity, int, int); + method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setTransactionAllowed(boolean); field public static final String ACTION_ADAPTER_STATE_CHANGED = "android.nfc.action.ADAPTER_STATE_CHANGED"; field public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED"; field @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static final String ACTION_PREFERRED_PAYMENT_CHANGED = "android.nfc.action.PREFERRED_PAYMENT_CHANGED"; diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 4d56c11d1c39..55506a1a7579 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -1204,37 +1204,21 @@ public final class NfcAdapter { } } - /** - * Disables observe mode to allow the transaction to proceed. See - * {@link #isObserveModeSupported()} for a description of observe mode and - * use {@link #disallowTransaction()} to enable observe mode and block - * transactions again. - * - * @return boolean indicating success or failure. - */ - @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) - public boolean allowTransaction() { - try { - return sService.setObserveMode(false); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - return false; - } - } - /** - * Signals that the transaction has completed and observe mode may be - * reenabled. See {@link #isObserveModeSupported()} for a description of - * observe mode and use {@link #allowTransaction()} to disable observe - * mode and allow transactions to proceed. - * - * @return boolean indicating success or failure. - */ + * Controls whether the NFC adapter will allow transactions to proceed or be in observe mode + * and simply observe and notify the APDU service of polling loop frames. See + * {@link #isObserveModeSupported()} for a description of observe mode. + * + * @param allowed true disables observe mode to allow the transaction to proceed while false + * enables observe mode and does not allow transactions to proceed. + * + * @return boolean indicating success or failure. + */ @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) - public boolean disallowTransaction() { + public boolean setTransactionAllowed(boolean allowed) { try { - return sService.setObserveMode(true); + return sService.setObserveMode(!allowed); } catch (RemoteException e) { attemptDeadServiceRecovery(e); return false; diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index a7d6bfba7861..ba3026ec18f8 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -104,7 +104,7 @@ flag { " standard background color is desired. This was the behavior before we discovered" " a resources threading issue, which we worked around by tinting the notification" " backgrounds and footer buttons." - bug: "294347738" + bug: "294830092" } flag { @@ -368,9 +368,9 @@ flag { } flag { - name: "enable_keyguard_compose" + name: "compose_lockscreen" namespace: "systemui" - description: "Enables the compose version of keyguard." + description: "Enables the compose version of lockscreen that runs standalone, outside of Flexiglass." bug: "301968149" } @@ -387,3 +387,11 @@ flag { description: "Enables on-screen contextual tip about how to take screenshot." bug: "322891421" } + +flag { + name: "shaderlib_loading_effect_refactor" + namespace: "systemui" + description: "Extend shader library to provide the common loading effects." + bug: "282007590" +} + diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt index 68e57b5d51b8..071433eba98d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt @@ -53,7 +53,7 @@ fun StickyKeysIndicator(stickyKeys: Map<ModifierKey, Locked>, modifier: Modifier stickyKeys.forEach { (key, isLocked) -> key(key) { Text( - text = key.text, + text = key.displayedText, fontWeight = if (isLocked.locked) FontWeight.Bold else FontWeight.Normal ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt index 1642e527aa15..45f98be2ca12 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt @@ -72,7 +72,7 @@ class CommunalMediaRepositoryImplTest : SysuiTestCase() { testScope.runTest { val mediaModel = collectLastValue(underTest.mediaModel) runCurrent() - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse() + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isFalse() } @Test @@ -84,16 +84,16 @@ class CommunalMediaRepositoryImplTest : SysuiTestCase() { // Initial value is false. val mediaModel = collectLastValue(underTest.mediaModel) runCurrent() - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse() + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isFalse() // Change to media available and notify the listener. - whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) whenever(mediaData.createdTimestampMillis).thenReturn(1234L) mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData) runCurrent() // Media active now returns true. - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue() + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isTrue() assertThat(mediaModel()?.createdTimestampMillis).isEqualTo(1234L) } @@ -104,20 +104,20 @@ class CommunalMediaRepositoryImplTest : SysuiTestCase() { verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture()) // Change to media available and notify the listener. - whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData) runCurrent() // Media active now returns true. val mediaModel = collectLastValue(underTest.mediaModel) - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue() + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isTrue() // Change to media unavailable and notify the listener. - whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(false) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false) mediaDataListenerCaptor.value.onMediaDataRemoved("key") runCurrent() // Media active now returns false. - assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse() + assertThat(mediaModel()?.hasActiveMediaOrRecommendation).isFalse() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt index 2fe4ef78bfdc..f400cb18295c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt @@ -25,9 +25,11 @@ import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository +import com.android.systemui.biometrics.domain.interactor.biometricStatusInteractor import com.android.systemui.biometrics.domain.interactor.displayStateInteractor import com.android.systemui.biometrics.domain.interactor.sideFpsSensorInteractor import com.android.systemui.biometrics.fakeFingerprintInteractiveToAuthProvider +import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectLastValue @@ -146,6 +148,7 @@ class SideFpsProgressBarViewModelTest : SysuiTestCase() { kosmos.fakeKeyguardRepository.setIsDozing(false) kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.DeviceEntryAuthentication, BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START ) ) @@ -165,6 +168,7 @@ class SideFpsProgressBarViewModelTest : SysuiTestCase() { kosmos.fakeKeyguardRepository.setIsDozing(true) kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.DeviceEntryAuthentication, BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START ) ) @@ -177,6 +181,7 @@ class SideFpsProgressBarViewModelTest : SysuiTestCase() { private fun createViewModel() = SideFpsProgressBarViewModel( kosmos.applicationContext, + kosmos.biometricStatusInteractor, kosmos.deviceEntryFingerprintAuthInteractor, kosmos.sideFpsSensorInteractor, kosmos.dozeServiceHost, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index d6d25096ba14..189ba7b1965a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -522,14 +522,18 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } @Test - fun factoryResetProtectionActive_isNotVisible() = + fun deviceProvisioningAndFactoryResetProtection() = testScope.runTest { val isVisible by collectLastValue(sceneContainerViewModel.isVisible) - assertThat(isVisible).isTrue() - - kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(isActive = true) + kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(false) + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(true) + assertThat(isVisible).isFalse() + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(false) assertThat(isVisible).isFalse() + + kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(true) + assertThat(isVisible).isTrue() } /** diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 1abbc92a539b..12dbf11255b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -49,6 +49,7 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock @@ -164,6 +165,30 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + fun hydrateVisibility_basedOnDeviceProvisioningAndFactoryResetProtection() = + testScope.runTest { + val isVisible by collectLastValue(sceneInteractor.isVisible) + prepareState( + isDeviceUnlocked = true, + initialSceneKey = SceneKey.Lockscreen, + isDeviceProvisioned = false, + isFrpActive = true, + ) + + underTest.start() + assertThat(isVisible).isFalse() + + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(false) + assertThat(isVisible).isFalse() + + kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(true) + assertThat(isVisible).isTrue() + + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(true) + assertThat(isVisible).isFalse() + } + + @Test fun startsInLockscreenScene() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) @@ -745,6 +770,8 @@ class SceneContainerStartableTest : SysuiTestCase() { authenticationMethod: AuthenticationMethodModel? = null, isLockscreenEnabled: Boolean = true, startsAwake: Boolean = true, + isDeviceProvisioned: Boolean = true, + isFrpActive: Boolean = false, ): MutableStateFlow<ObservableTransitionState> { if (authenticationMethod?.isSecure == true) { assert(isLockscreenEnabled) { @@ -781,6 +808,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } else { powerInteractor.setAsleepForTest() } + + kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(isDeviceProvisioned) + kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(isFrpActive) + runCurrent() return transitionStateFlow diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt index d28dbc0ae06f..27bb023e3366 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt @@ -24,17 +24,24 @@ import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR +import android.hardware.biometrics.BiometricSourceType import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations +import com.android.systemui.biometrics.shared.model.AuthenticationState import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn /** A repository for the state of biometric authentication. */ @@ -44,6 +51,9 @@ interface BiometricStatusRepository { * [NotRunning]. */ val fingerprintAuthenticationReason: Flow<AuthenticationReason> + + /** The current status of an acquired fingerprint. */ + val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> } @SysUISingleton @@ -54,53 +64,53 @@ constructor( private val biometricManager: BiometricManager? ) : BiometricStatusRepository { - override val fingerprintAuthenticationReason: Flow<AuthenticationReason> = + private val authenticationState: Flow<AuthenticationState> = conflatedCallbackFlow { - val updateFingerprintAuthenticateReason = { reason: AuthenticationReason -> - trySendWithFailureLogging( - reason, - TAG, - "Error sending fingerprintAuthenticateReason reason" - ) + val updateAuthenticationState = { state: AuthenticationState -> + trySendWithFailureLogging(state, TAG, "Error sending AuthenticationState state") } val authenticationStateListener = object : AuthenticationStateListener.Stub() { override fun onAuthenticationStarted(requestReason: Int) { - val authenticationReason = - when (requestReason) { - REASON_AUTH_BP -> - AuthenticationReason.BiometricPromptAuthentication - REASON_AUTH_KEYGUARD -> - AuthenticationReason.DeviceEntryAuthentication - REASON_AUTH_OTHER -> AuthenticationReason.OtherAuthentication - REASON_AUTH_SETTINGS -> - AuthenticationReason.SettingsAuthentication( - SettingsOperations.OTHER - ) - REASON_ENROLL_ENROLLING -> - AuthenticationReason.SettingsAuthentication( - SettingsOperations.ENROLL_ENROLLING - ) - REASON_ENROLL_FIND_SENSOR -> - AuthenticationReason.SettingsAuthentication( - SettingsOperations.ENROLL_FIND_SENSOR - ) - else -> AuthenticationReason.Unknown - } - updateFingerprintAuthenticateReason(authenticationReason) + val authenticationReason = requestReason.toAuthenticationReason() + updateAuthenticationState( + AuthenticationState.AuthenticationStarted(authenticationReason) + ) } override fun onAuthenticationStopped() { - updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning) + updateAuthenticationState( + AuthenticationState.AuthenticationStopped( + AuthenticationReason.NotRunning + ) + ) } override fun onAuthenticationSucceeded(requestReason: Int, userId: Int) {} override fun onAuthenticationFailed(requestReason: Int, userId: Int) {} + + override fun onAuthenticationAcquired( + biometricSourceType: BiometricSourceType, + requestReason: Int, + acquiredInfo: Int + ) { + val authReason = requestReason.toAuthenticationReason() + + updateAuthenticationState( + AuthenticationState.AuthenticationAcquired( + biometricSourceType, + authReason, + acquiredInfo + ) + ) + } } - updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning) + updateAuthenticationState( + AuthenticationState.AuthenticationStarted(AuthenticationReason.NotRunning) + ) biometricManager?.registerAuthenticationStateListener(authenticationStateListener) awaitClose { biometricManager?.unregisterAuthenticationStateListener( @@ -110,7 +120,36 @@ constructor( } .shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1) + override val fingerprintAuthenticationReason: Flow<AuthenticationReason> = + authenticationState.map { it.requestReason } + + override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> = + authenticationState + .filterIsInstance<AuthenticationState.AuthenticationAcquired>() + .filter { + it.biometricSourceType == BiometricSourceType.FINGERPRINT && + // TODO(b/322555228) This check will be removed after consolidating device + // entry auth messages (currently in DeviceEntryFingerprintAuthRepository) + // with BP auth messages (here) + it.requestReason == AuthenticationReason.BiometricPromptAuthentication + } + .map { AcquiredFingerprintAuthenticationStatus(it.requestReason, it.acquiredInfo) } + companion object { private const val TAG = "BiometricStatusRepositoryImpl" } } + +private fun Int.toAuthenticationReason(): AuthenticationReason = + when (this) { + REASON_AUTH_BP -> AuthenticationReason.BiometricPromptAuthentication + REASON_AUTH_KEYGUARD -> AuthenticationReason.DeviceEntryAuthentication + REASON_AUTH_OTHER -> AuthenticationReason.OtherAuthentication + REASON_AUTH_SETTINGS -> + AuthenticationReason.SettingsAuthentication(SettingsOperations.OTHER) + REASON_ENROLL_ENROLLING -> + AuthenticationReason.SettingsAuthentication(SettingsOperations.ENROLL_ENROLLING) + REASON_ENROLL_FIND_SENSOR -> + AuthenticationReason.SettingsAuthentication(SettingsOperations.ENROLL_FIND_SENSOR) + else -> AuthenticationReason.Unknown + } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt index 55a2d3d7563e..ed1557cccd01 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt @@ -20,6 +20,7 @@ import android.app.ActivityTaskManager import com.android.systemui.biometrics.data.repository.BiometricStatusRepository import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations +import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -31,6 +32,9 @@ interface BiometricStatusInteractor { * filtered for when the overlay should be shown, otherwise [NotRunning]. */ val sfpsAuthenticationReason: Flow<AuthenticationReason> + + /** The current status of an acquired fingerprint. */ + val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> } class BiometricStatusInteractorImpl @@ -50,6 +54,9 @@ constructor( } } + override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> = + biometricStatusRepository.fingerprintAcquiredStatus + companion object { private const val TAG = "BiometricStatusInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt new file mode 100644 index 000000000000..77cf8406725f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.shared.model + +import android.hardware.biometrics.BiometricSourceType + +/** + * Describes the current state of biometric authentication, including whether authentication is + * started, stopped, or acquired and relevant parameters, and the [AuthenticationReason] for + * authentication. + */ +sealed interface AuthenticationState { + val requestReason: AuthenticationReason + + /** + * Authentication started + * + * @param requestReason [AuthenticationReason] for starting authentication + */ + data class AuthenticationStarted(override val requestReason: AuthenticationReason) : + AuthenticationState + + /** + * Authentication stopped + * + * @param requestReason [AuthenticationReason.NotRunning] + */ + data class AuthenticationStopped(override val requestReason: AuthenticationReason) : + AuthenticationState + + /** + * Authentication acquired + * + * @param biometricSourceType indicates [BiometricSourceType] of acquired authentication + * @param requestReason indicates [AuthenticationReason] for requesting auth + * @param acquiredInfo indicates + */ + data class AuthenticationAcquired( + val biometricSourceType: BiometricSourceType, + override val requestReason: AuthenticationReason, + val acquiredInfo: Int + ) : AuthenticationState +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt index 80d37b4741e4..7b4be0220ff2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt @@ -50,10 +50,12 @@ import com.android.systemui.util.kotlin.sample import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch /** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class SideFpsOverlayViewBinder @Inject diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt index ce726034913f..cfda75c7851a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt @@ -41,12 +41,14 @@ import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlay import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel import com.android.systemui.res.R import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged /** Models UI of the side fingerprint sensor indicator view. */ +@OptIn(ExperimentalCoroutinesApi::class) class SideFpsOverlayViewModel @Inject constructor( @@ -176,8 +178,8 @@ constructor( val lottieCallbacks: Flow<List<LottieCallback>> = combine( biometricStatusInteractor.sfpsAuthenticationReason, - deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry.distinctUntilChanged(), - sideFpsProgressBarViewModel.isVisible, + deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry, + sideFpsProgressBarViewModel.isVisible ) { reason: AuthenticationReason, showIndicatorForDeviceEntry: Boolean, progressBarIsVisible -> val callbacks = mutableListOf<LottieCallback>() diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt index c46f0d1cfcb6..33edb800756d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt @@ -21,21 +21,21 @@ import com.android.systemui.log.table.TableRowLogger /** Data model of media on the communal hub. */ data class CommunalMediaModel( - val hasAnyMediaOrRecommendation: Boolean, + val hasActiveMediaOrRecommendation: Boolean, val createdTimestampMillis: Long = 0L, ) : Diffable<CommunalMediaModel> { companion object { val INACTIVE = CommunalMediaModel( - hasAnyMediaOrRecommendation = false, + hasActiveMediaOrRecommendation = false, ) } override fun logDiffs(prevVal: CommunalMediaModel, row: TableRowLogger) { - if (hasAnyMediaOrRecommendation != prevVal.hasAnyMediaOrRecommendation) { + if (hasActiveMediaOrRecommendation != prevVal.hasActiveMediaOrRecommendation) { row.logChange( columnName = "isMediaActive", - value = hasAnyMediaOrRecommendation, + value = hasActiveMediaOrRecommendation, ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt index 2b66491613ab..201be51b873c 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt @@ -73,10 +73,10 @@ constructor( ) private fun updateMediaModel(data: MediaData? = null) { - if (mediaDataManager.hasAnyMediaOrRecommendation()) { + if (mediaDataManager.hasActiveMediaOrRecommendation()) { _mediaModel.value = CommunalMediaModel( - hasAnyMediaOrRecommendation = true, + hasActiveMediaOrRecommendation = true, createdTimestampMillis = data?.createdTimestampMillis ?: 0L, ) } else { diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 75a27a278116..950ac3c3aae6 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -318,7 +318,7 @@ constructor( ) // Add UMO - if (media.hasAnyMediaOrRecommendation) { + if (media.hasActiveMediaOrRecommendation) { ongoingContent.add( CommunalContentModel.Umo( createdTimestampMillis = media.createdTimestampMillis, diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 0c12841aa42b..40d2d1656fbc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -87,7 +87,7 @@ constructor( with(mediaHost) { expansion = MediaHostState.EXPANDED expandedMatchesParentHeight = true - showsOnlyActiveMedia = false + showsOnlyActiveMedia = true falsingProtectionNeeded = false init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index df0566e246a8..41ce3fd11e8a 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -23,9 +23,12 @@ import com.android.server.notification.Flags.crossAppPoliteNotifications import com.android.server.notification.Flags.politeNotifications import com.android.server.notification.Flags.vibrateWhileUnlocked import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR +import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.Flags.keyguardBottomAreaRefactor +import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW +import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor @@ -55,6 +58,11 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // SceneContainer dependencies SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta } SceneContainerFlag.getMainStaticFlag() dependsOn MIGRATE_KEYGUARD_STATUS_BAR_VIEW + + // ComposeLockscreen dependencies + ComposeLockscreen.token dependsOn KeyguardShadeMigrationNssl.token + ComposeLockscreen.token dependsOn keyguardBottomAreaRefactor + ComposeLockscreen.token dependsOn migrateClocksToBlueprint } private inline val politeNotifications @@ -65,4 +73,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked()) private inline val keyguardBottomAreaRefactor get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()) + private inline val migrateClocksToBlueprint + get() = FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint()) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt index d5f082a2566f..72a81cbac9d5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt @@ -19,10 +19,10 @@ package com.android.systemui.keyboard.stickykeys.shared.model @JvmInline value class Locked(val locked: Boolean) -enum class ModifierKey(val text: String) { +enum class ModifierKey(val displayedText: String) { ALT("ALT LEFT"), ALT_GR("ALT RIGHT"), CTRL("CTRL"), - META("META"), + META("ACTION"), SHIFT("SHIFT"), } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt index 9a13558d3327..b152eea63028 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt @@ -22,6 +22,7 @@ import android.hardware.biometrics.BiometricSourceType import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -174,6 +175,8 @@ constructor( mainDispatcher ) // keyguardUpdateMonitor requires registration on main thread. + // TODO(b/322555228) Remove after consolidating device entry auth messages with BP auth messages + // in BiometricStatusRepository override val authenticationStatus: Flow<FingerprintAuthenticationStatus> get() = conflatedCallbackFlow { val callback = @@ -236,7 +239,8 @@ constructor( sendUpdateIfFingerprint( biometricSourceType, AcquiredFingerprintAuthenticationStatus( - acquireInfo, + AuthenticationReason.DeviceEntryAuthentication, + acquireInfo ), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt index b1a2297526ce..e017129bd5c5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt @@ -94,6 +94,7 @@ constructor( context.resources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer) val sfpsDetectionRunning = keyguardUpdateMonitor.isFingerprintDetectionRunning val isUnlockingWithFpAllowed = keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed + return primaryBouncerInteractor.isBouncerShowing() && sfpsEnabled && sfpsDetectionRunning && diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt new file mode 100644 index 000000000000..7f0b483919b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.shared + +import com.android.systemui.Flags +import com.android.systemui.compose.ComposeFacade +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the compose lockscreen flag state. */ +@Suppress("NOTHING_TO_INLINE") +object ComposeLockscreen { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_COMPOSE_LOCKSCREEN + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.composeLockscreen() && ComposeFacade.isComposeAvailable() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt index cc385a8eea85..474de77f09ab 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt @@ -20,6 +20,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQ import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START import android.hardware.fingerprint.FingerprintManager import android.os.SystemClock.elapsedRealtime +import com.android.systemui.biometrics.shared.model.AuthenticationReason /** * Fingerprint authentication status provided by @@ -40,8 +41,10 @@ data class HelpFingerprintAuthenticationStatus( ) : FingerprintAuthenticationStatus() /** Fingerprint acquired message. */ -data class AcquiredFingerprintAuthenticationStatus(val acquiredInfo: Int) : - FingerprintAuthenticationStatus() { +data class AcquiredFingerprintAuthenticationStatus( + val authenticationReason: AuthenticationReason, + val acquiredInfo: Int +) : FingerprintAuthenticationStatus() { val fingerprintCaptureStarted: Boolean = acquiredInfo == FINGERPRINT_ACQUIRED_START diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt index ca9c8571f6b9..67c42f0fe343 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt @@ -22,8 +22,10 @@ import android.graphics.Point import androidx.annotation.VisibleForTesting import androidx.core.animation.addListener import com.android.systemui.Flags +import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor +import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.isDefaultOrientation import com.android.systemui.dagger.SysUISingleton @@ -34,6 +36,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R @@ -49,10 +52,12 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.launch @@ -62,7 +67,8 @@ class SideFpsProgressBarViewModel @Inject constructor( private val context: Context, - private val fpAuthRepository: DeviceEntryFingerprintAuthInteractor, + private val biometricStatusInteractor: BiometricStatusInteractor, + private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, private val sfpsSensorInteractor: SideFpsSensorInteractor, // todo (b/317432075) Injecting DozeServiceHost directly instead of using it through // DozeInteractor as DozeServiceHost already depends on DozeInteractor. @@ -86,6 +92,23 @@ constructor( private val additionalSensorLengthPadding = context.resources.getDimension(R.dimen.sfps_progress_bar_length_extra_padding).toInt() + // Merged [FingerprintAuthenticationStatus] from BiometricPrompt acquired messages and + // device entry authentication messages + private val mergedFingerprintAuthenticationStatus = + merge( + biometricStatusInteractor.fingerprintAcquiredStatus, + deviceEntryFingerprintAuthInteractor.authenticationStatus + ) + .filter { + if (it is AcquiredFingerprintAuthenticationStatus) { + it.authenticationReason == AuthenticationReason.DeviceEntryAuthentication || + it.authenticationReason == + AuthenticationReason.BiometricPromptAuthentication + } else { + true + } + } + val isVisible: Flow<Boolean> = _visible.asStateFlow() val progress: Flow<Float> = _progress.asStateFlow() @@ -147,7 +170,14 @@ constructor( viewLeftTop } - val isFingerprintAuthRunning: Flow<Boolean> = fpAuthRepository.isRunning + val isFingerprintAuthRunning: Flow<Boolean> = + combine( + deviceEntryFingerprintAuthInteractor.isRunning, + biometricStatusInteractor.sfpsAuthenticationReason + ) { deviceEntryAuthIsRunning, sfpsAuthReason -> + deviceEntryAuthIsRunning || + sfpsAuthReason == AuthenticationReason.BiometricPromptAuthentication + } val rotation: Flow<Float> = combine(displayStateInteractor.currentRotation, sfpsSensorInteractor.sensorLocation, ::Pair) @@ -185,7 +215,8 @@ constructor( sfpsSensorInteractor.authenticationDuration .flatMapLatest { authDuration -> _animator?.cancel() - fpAuthRepository.authenticationStatus.map { authStatus -> + mergedFingerprintAuthenticationStatus.map { + authStatus: FingerprintAuthenticationStatus -> when (authStatus) { is AcquiredFingerprintAuthenticationStatus -> { if (authStatus.fingerprintCaptureStarted) { diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 37abc4066e54..56c0ca910bd7 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -52,6 +52,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.emptyFlow @@ -115,11 +116,15 @@ constructor( private fun hydrateVisibility() { applicationScope.launch { // TODO(b/296114544): Combine with some global hun state to make it visible! - deviceProvisioningInteractor.isFactoryResetProtectionActive - .flatMapLatest { isFrpActive -> - if (isFrpActive) { - flowOf(false to "Factory Reset Protection is active") - } else { + combine( + deviceProvisioningInteractor.isDeviceProvisioned, + deviceProvisioningInteractor.isFactoryResetProtectionActive, + ) { isDeviceProvisioned, isFrpActive -> + isDeviceProvisioned && !isFrpActive + } + .distinctUntilChanged() + .flatMapLatest { isAllowedToBeVisible -> + if (isAllowedToBeVisible) { sceneInteractor.transitionState .mapNotNull { state -> when (state) { @@ -140,6 +145,8 @@ constructor( } } .distinctUntilChanged() + } else { + flowOf(false to "Device not provisioned or Factory Reset Protection active") } } .collect { (isVisible, loggingReason) -> diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index a01ac705c0df..f7fed537a167 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -183,7 +183,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mBackgroundExecutor = backgroundExecutor; mColorExtractor = colorExtractor; mScreenOffAnimationController = screenOffAnimationController; - dumpManager.registerDumpable(this); + // prefix with {slow} to make sure this dumps at the END of the critical section. + dumpManager.registerCriticalDumpable("{slow}NotificationShadeWindowControllerImpl", this); mAuthController = authController; mUserInteractor = userInteractor; mSceneContainerFlags = sceneContainerFlags; diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 10b9db0a349b..4e8b4039cc79 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -20,6 +20,7 @@ import android.view.MotionEvent import com.android.systemui.assist.AssistManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.ShadeTouchLog @@ -34,11 +35,13 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Implementation of ShadeController backed by scenes instead of NPVC. @@ -50,6 +53,7 @@ import kotlinx.coroutines.launch class ShadeControllerSceneImpl @Inject constructor( + @Main private val mainDispatcher: CoroutineDispatcher, @Background private val scope: CoroutineScope, private val shadeInteractor: ShadeInteractor, private val sceneInteractor: SceneInteractor, @@ -193,7 +197,11 @@ constructor( } override fun setVisibilityListener(listener: ShadeVisibilityListener) { - scope.launch { sceneInteractor.isVisible.collect { listener.expandedVisibleChanged(it) } } + scope.launch { + sceneInteractor.isVisible.collect { isVisible -> + withContext(mainDispatcher) { listener.expandedVisibleChanged(isVisible) } + } + } } @ExperimentalCoroutinesApi diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt index 8f0e910271c7..8fbeb6f93360 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.data.repository import android.hardware.biometrics.AuthenticationStateListener +import android.hardware.biometrics.BiometricFingerprintConstants import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD @@ -24,11 +25,13 @@ import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR +import android.hardware.biometrics.BiometricSourceType import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat @@ -167,6 +170,28 @@ class BiometricStatusRepositoryTest : SysuiTestCase() { listener.onAuthenticationStopped() assertThat(fingerprintAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning) } + + @Test + fun updatesFingerprintAcquiredStatusWhenBiometricPromptAuthenticationAcquired() = + testScope.runTest { + val fingerprintAcquiredStatus by collectLastValue(underTest.fingerprintAcquiredStatus) + runCurrent() + + val listener = biometricManager.captureListener() + listener.onAuthenticationAcquired( + BiometricSourceType.FINGERPRINT, + REASON_AUTH_BP, + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START + ) + + assertThat(fingerprintAcquiredStatus) + .isEqualTo( + AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.BiometricPromptAuthentication, + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START + ) + ) + } } private fun BiometricManager.captureListener() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt index d7b7d79425c8..5c34fd9a1bc5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt @@ -19,12 +19,14 @@ package com.android.systemui.biometrics.domain.interactor import android.app.ActivityManager import android.app.ActivityTaskManager import android.content.ComponentName +import android.hardware.biometrics.BiometricFingerprintConstants import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -162,6 +164,27 @@ class BiometricStatusInteractorImplTest : SysuiTestCase() { ) assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning) } + + @Test + fun updatesFingerprintAcquiredStatusWhenBiometricPromptAuthenticationAcquired() = + testScope.runTest { + val fingerprintAcquiredStatus by collectLastValue(underTest.fingerprintAcquiredStatus) + runCurrent() + + biometricStatusRepository.setFingerprintAcquiredStatus( + AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.BiometricPromptAuthentication, + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START + ) + ) + assertThat(fingerprintAcquiredStatus) + .isEqualTo( + AcquiredFingerprintAuthenticationStatus( + AuthenticationReason.BiometricPromptAuthentication, + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START + ) + ) + } } private fun fpSettingsTask() = settingsTask(".biometrics.fingerprint.FingerprintSettings") diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt index 3603c3c6c46a..5509c048b0da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt @@ -58,6 +58,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository @@ -100,6 +101,7 @@ import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.any +import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.spy @@ -253,7 +255,8 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, - mock(), + biometricStatusInteractor, + kosmos.deviceEntryFingerprintAuthInteractor, sfpsSensorInteractor, kosmos.dozeServiceHost, kosmos.keyguardInteractor, @@ -426,6 +429,54 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { } } + // On progress bar shown - hide indicator + // On progress bar hidden - show indicator + @Test + fun verifyIndicatorProgressBarInteraction() { + testScope.runTest { + // Pre-auth conditions + setupTestConfiguration( + DeviceConfig.X_ALIGNED, + rotation = DisplayRotation.ROTATION_0, + isInRearDisplayMode = false + ) + biometricStatusRepository.setFingerprintAuthenticationReason( + AuthenticationReason.NotRunning + ) + sideFpsProgressBarViewModel.setVisible(false) + + // Show primary bouncer + updatePrimaryBouncer( + isShowing = true, + isAnimatingAway = false, + fpsDetectionRunning = true, + isUnlockingWithFpAllowed = true + ) + runCurrent() + + val inOrder = inOrder(windowManager) + + // Verify indicator shown + inOrder.verify(windowManager).addView(any(), any()) + + // Set progress bar visible + sideFpsProgressBarViewModel.setVisible(true) + + runCurrent() + + // Verify indicator hidden + inOrder.verify(windowManager).removeView(any()) + + // Set progress bar invisible + sideFpsProgressBarViewModel.setVisible(false) + + runCurrent() + + // Verify indicator shown + inOrder.verify(windowManager).addView(any(), any()) + } + } + private fun updatePrimaryBouncer( isShowing: Boolean, isAnimatingAway: Boolean, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt index 3c430316ffbe..2014755bd964 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.biometrics.ui.viewmodel -import android.app.ActivityTaskManager import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.graphics.Color @@ -39,10 +38,10 @@ import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository -import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor -import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractorImpl +import com.android.systemui.biometrics.data.repository.biometricStatusRepository import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor +import com.android.systemui.biometrics.domain.interactor.biometricStatusInteractor import com.android.systemui.biometrics.shared.model.AuthenticationReason import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.FingerprintSensorType @@ -80,7 +79,6 @@ import com.android.systemui.testKosmos import com.android.systemui.unfold.compat.ScreenSizeFoldProvider import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -109,7 +107,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule() - @Mock private lateinit var activityTaskManager: ActivityTaskManager @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor @Mock private lateinit var fingerprintInteractiveToAuthProvider: FingerprintInteractiveToAuthProvider @@ -147,7 +144,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { context.getColor(com.android.settingslib.color.R.color.settingslib_color_blue400) private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor - private lateinit var biometricStatusInteractor: BiometricStatusInteractor private lateinit var deviceEntrySideFpsOverlayInteractor: DeviceEntrySideFpsOverlayInteractor private lateinit var displayStateInteractor: DisplayStateInteractorImpl private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor @@ -184,6 +180,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { .thenReturn( Display(mock(DisplayManagerGlobal::class.java), 1, contextDisplayInfo, resources) ) + kosmos.biometricStatusRepository = biometricStatusRepository alternateBouncerInteractor = AlternateBouncerInteractor( @@ -197,9 +194,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { testScope.backgroundScope, ) - biometricStatusInteractor = - BiometricStatusInteractorImpl(activityTaskManager, biometricStatusRepository) - displayStateInteractor = DisplayStateInteractorImpl( testScope.backgroundScope, @@ -256,6 +250,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, + kosmos.biometricStatusInteractor, kosmos.deviceEntryFingerprintAuthInteractor, sfpsSensorInteractor, kosmos.dozeServiceHost, @@ -263,13 +258,13 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { displayStateInteractor, kosmos.testDispatcher, testScope.backgroundScope, - kosmos.powerInteractor, + kosmos.powerInteractor ) underTest = SideFpsOverlayViewModel( mContext, - biometricStatusInteractor, + kosmos.biometricStatusInteractor, deviceEntrySideFpsOverlayInteractor, displayStateInteractor, sfpsSensorInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt index 961022f0f426..a4f28f395a63 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt @@ -20,4 +20,4 @@ package com.android.systemui.biometrics.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.biometricStatusRepository by Fixture { FakeBiometricStatusRepository() } +var Kosmos.biometricStatusRepository by Fixture { FakeBiometricStatusRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt index 1c8bd3b58dfe..e9b7a69d1421 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt @@ -18,9 +18,12 @@ package com.android.systemui.biometrics.data.repository import com.android.systemui.biometrics.shared.model.AuthenticationReason +import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull class FakeBiometricStatusRepository : BiometricStatusRepository { private val _fingerprintAuthenticationReason = @@ -28,7 +31,16 @@ class FakeBiometricStatusRepository : BiometricStatusRepository { override val fingerprintAuthenticationReason: StateFlow<AuthenticationReason> = _fingerprintAuthenticationReason.asStateFlow() + private val _fingerprintAcquiredStatus = + MutableStateFlow<FingerprintAuthenticationStatus?>(null) + override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> = + _fingerprintAcquiredStatus.asStateFlow().filterNotNull() + fun setFingerprintAuthenticationReason(reason: AuthenticationReason) { _fingerprintAuthenticationReason.value = reason } + + fun setFingerprintAcquiredStatus(status: FingerprintAuthenticationStatus) { + _fingerprintAcquiredStatus.value = status + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt index 3ea3ccfb2909..1884a3264ed6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt @@ -28,7 +28,7 @@ class FakeCommunalMediaRepository : CommunalMediaRepository { fun mediaActive(timestamp: Long = 0L) { _mediaModel.value = CommunalMediaModel( - hasAnyMediaOrRecommendation = true, + hasActiveMediaOrRecommendation = true, createdTimestampMillis = timestamp, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt index e13fa5207b33..82e0b8e83f24 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.log.LogBuffer import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -44,6 +45,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.shadeControllerSceneImpl by Kosmos.Fixture { ShadeControllerSceneImpl( + mainDispatcher = testDispatcher, scope = applicationCoroutineScope, shadeInteractor = shadeInteractor, sceneInteractor = sceneInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt index 300229954044..fc6a800e186c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @SysUISingleton class FakeDeviceProvisioningRepository @Inject constructor() : DeviceProvisioningRepository { - private val _isDeviceProvisioned = MutableStateFlow(false) + private val _isDeviceProvisioned = MutableStateFlow(true) override val isDeviceProvisioned: Flow<Boolean> = _isDeviceProvisioned private val _isFactoryResetProtectionActive = MutableStateFlow(false) override val isFactoryResetProtectionActive: Flow<Boolean> = _isFactoryResetProtectionActive diff --git a/ravenwood/bulk_enable.py b/ravenwood/bulk_enable.py index 36d398cc160c..83fda9e72c3c 100644 --- a/ravenwood/bulk_enable.py +++ b/ravenwood/bulk_enable.py @@ -34,6 +34,8 @@ import sys re_result = re.compile("I/ModuleListener.+?null-device-0 (.+?)#(.+?) ([A-Z_]+)(.*)$") +DRY_RUN = "-n" in sys.argv + ANNOTATION = "@android.platform.test.annotations.EnabledOnRavenwood" SED_ARG = "s/^((public )?class )/%s\\n\\1/g" % (ANNOTATION) @@ -46,7 +48,7 @@ stats_total = collections.defaultdict(int) stats_class = collections.defaultdict(lambda: collections.defaultdict(int)) stats_method = collections.defaultdict() -with open(sys.argv[1]) as f: +with open(sys.argv[-1]) as f: for line in f.readlines(): result = re_result.search(line) if result: @@ -67,7 +69,7 @@ for clazz in stats_class.keys(): clazz_match = re.compile("%s\.(kt|java)" % (clazz.split(".")[-1])) for root, dirs, files in os.walk("."): for f in files: - if clazz_match.match(f): + if clazz_match.match(f) and not DRY_RUN: path = os.path.join(root, f) subprocess.run(["sed", "-i", "-E", SED_ARG, path]) diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 3670459c0300..8af561fbd273 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -16,7 +16,9 @@ package android.platform.test.ravenwood; +import android.app.ActivityManager; import android.app.Instrumentation; +import android.os.Build; import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; @@ -30,10 +32,12 @@ import org.junit.runner.Description; import java.io.PrintStream; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; public class RavenwoodRuleImpl { private static final String MAIN_THREAD_NAME = "RavenwoodMain"; @@ -50,11 +54,27 @@ public class RavenwoodRuleImpl { private static ScheduledFuture<?> sPendingTimeout; + /** + * When set, an unhandled exception was discovered (typically on a background thread), and we + * capture it here to ensure it's reported as a test failure. + */ + private static final AtomicReference<Throwable> sPendingUncaughtException = + new AtomicReference<>(); + + private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler = + (thread, throwable) -> { + // Remember the first exception we discover + sPendingUncaughtException.compareAndSet(null, throwable); + }; + public static boolean isOnRavenwood() { return true; } public static void init(RavenwoodRule rule) { + maybeThrowPendingUncaughtException(false); + Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler); + RuntimeInit.redirectLogStreams(); android.os.Process.init$ravenwood(rule.mUid, rule.mPid); @@ -64,6 +84,8 @@ public class RavenwoodRuleImpl { rule.mSystemProperties.getKeyReadablePredicate(), rule.mSystemProperties.getKeyWritablePredicate()); + ActivityManager.init$ravenwood(rule.mCurrentUser); + com.android.server.LocalServices.removeAllServicesForTest(); if (rule.mProvideMainThread) { @@ -78,6 +100,10 @@ public class RavenwoodRuleImpl { sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks, TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } + + // Touch some references early to ensure they're <clinit>'ed + Objects.requireNonNull(Build.IS_USERDEBUG); + Objects.requireNonNull(Build.VERSION.SDK); } public static void reset(RavenwoodRule rule) { @@ -94,9 +120,13 @@ public class RavenwoodRuleImpl { com.android.server.LocalServices.removeAllServicesForTest(); + ActivityManager.reset$ravenwood(); + android.os.SystemProperties.reset$ravenwood(); android.os.Binder.reset$ravenwood(); android.os.Process.reset$ravenwood(); + + maybeThrowPendingUncaughtException(true); } public static void logTestRunner(String label, Description description) { @@ -120,4 +150,21 @@ public class RavenwoodRuleImpl { } out.println("-----END ALL THREAD STACKS-----"); } + + /** + * If there's a pending uncaught exception, consume and throw it now. Typically used to + * report an exception on a background thread as a failure for the currently running test. + */ + private static void maybeThrowPendingUncaughtException(boolean duringReset) { + final Throwable pending = sPendingUncaughtException.getAndSet(null); + if (pending != null) { + if (duringReset) { + throw new IllegalStateException( + "Found an uncaught exception during this test", pending); + } else { + throw new IllegalStateException( + "Found an uncaught exception before this test started", pending); + } + } + } } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 0285b386ed13..daed457fb695 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -16,6 +16,10 @@ package android.platform.test.ravenwood; +import static android.os.Process.FIRST_APPLICATION_UID; +import static android.os.Process.SYSTEM_UID; +import static android.os.UserHandle.USER_SYSTEM; + import static org.junit.Assert.fail; import android.platform.test.annotations.DisabledOnRavenwood; @@ -94,12 +98,12 @@ public class RavenwoodRule implements TestRule { } } - private static final int SYSTEM_UID = 1000; private static final int NOBODY_UID = 9999; - private static final int FIRST_APPLICATION_UID = 10000; private static final AtomicInteger sNextPid = new AtomicInteger(100); + int mCurrentUser = USER_SYSTEM; + /** * Unless the test author requests differently, run as "nobody", and give each collection of * tests its own unique PID. diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index e49b64ebdbef..a5ecd20b8b4f 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -82,6 +82,8 @@ android.os.UidBatteryConsumer android.os.UidBatteryConsumer$Builder android.os.UserHandle android.os.UserManager +android.os.VibrationAttributes +android.os.VibrationAttributes$Builder android.os.WorkSource android.content.ClipData @@ -144,6 +146,7 @@ android.graphics.RectF android.content.ContentProvider +android.app.ActivityManager android.app.Instrumentation android.metrics.LogMaker diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java index 1ae4d6465c57..1dc882e5ed2b 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java @@ -18,6 +18,8 @@ package com.android.server.biometrics.sensors; import android.annotation.NonNull; import android.hardware.biometrics.AuthenticationStateListener; +import android.hardware.biometrics.BiometricFingerprintConstants; +import android.hardware.biometrics.BiometricSourceType; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; @@ -115,7 +117,7 @@ public class AuthenticationStateListeners implements IBinder.DeathRecipient { * @param userId The user Id for the requested authentication */ public void onAuthenticationFailed(int requestReason, int userId) { - for (AuthenticationStateListener listener: mAuthenticationStateListeners) { + for (AuthenticationStateListener listener : mAuthenticationStateListeners) { try { listener.onAuthenticationFailed(requestReason, userId); } catch (RemoteException e) { @@ -125,6 +127,27 @@ public class AuthenticationStateListeners implements IBinder.DeathRecipient { } } + /** + * Defines behavior in response to biometric being acquired. + * @param biometricSourceType identifies [BiometricSourceType] biometric was acquired for + * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication + * @param acquiredInfo [BiometricFingerprintConstants.FingerprintAcquired] int corresponding to + * a known acquired message. + */ + public void onAuthenticationAcquired( + BiometricSourceType biometricSourceType, int requestReason, + @BiometricFingerprintConstants.FingerprintAcquired int acquiredInfo + ) { + for (AuthenticationStateListener listener: mAuthenticationStateListeners) { + try { + listener.onAuthenticationAcquired(biometricSourceType, requestReason, acquiredInfo); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception in notifying listener that authentication " + + "stopped", e); + } + } + } + @Override public void binderDied() { // Do nothing, handled below diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index e0fd44b9f6bb..8121a639ab0a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -29,6 +29,7 @@ import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired; import android.hardware.biometrics.BiometricManager.Authenticators; +import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.common.ICancellationSignal; import android.hardware.biometrics.common.OperationState; import android.hardware.biometrics.fingerprint.PointerContext; @@ -102,6 +103,7 @@ public class FingerprintAuthenticationClient private Runnable mAuthSuccessRunnable; private final Clock mClock; + public FingerprintAuthenticationClient( @NonNull Context context, @NonNull Supplier<AidlSession> lazyDaemon, @@ -280,6 +282,8 @@ public class FingerprintAuthenticationClient public void onAcquired(@FingerprintAcquired int acquiredInfo, int vendorCode) { // For UDFPS, notify SysUI with acquiredInfo, so that the illumination can be turned off // for most ACQUIRED messages. See BiometricFingerprintConstants#FingerprintAcquired + mAuthenticationStateListeners.onAuthenticationAcquired( + BiometricSourceType.FINGERPRINT, getRequestReason(), acquiredInfo); mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo)); super.onAcquired(acquiredInfo, vendorCode); PerformanceTracker pt = PerformanceTracker.getInstanceForSensorId(getSensorId()); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java index 60c532c26f5d..b6311afb5ea1 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java @@ -28,6 +28,7 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricManager.Authenticators; +import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.fingerprint.PointerContext; import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; import android.hardware.fingerprint.FingerprintAuthenticateOptions; @@ -201,6 +202,8 @@ class FingerprintAuthenticationClient @Override public void onAcquired(int acquiredInfo, int vendorCode) { + mAuthenticationStateListeners.onAuthenticationAcquired( + BiometricSourceType.FINGERPRINT, getRequestReason(), acquiredInfo); super.onAcquired(acquiredInfo, vendorCode); @LockoutTracker.LockoutMode final int lockoutMode = diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 1128d0c903cf..2e0546eee8e7 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -1319,33 +1319,9 @@ class ActivityClientController extends IActivityClientController.Stub { try { synchronized (mGlobalLock) { final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token); - if (r == null) return; - final TransitionController controller = r.mTransitionController; - if (!controller.isShellTransitionsEnabled()) { - r.setShowWhenLocked(showWhenLocked); - return; - } - if (controller.isCollecting() - && !mService.mKeyguardController.isKeyguardLocked(r.getDisplayId())) { - // Keyguard isn't locked, so this can be done as part of the collecting - // transition. + if (r != null) { r.setShowWhenLocked(showWhenLocked); - return; } - final Transition transition = new Transition( - showWhenLocked ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, - 0 /* flags */, controller, mService.mWindowManager.mSyncEngine); - r.mTransitionController.startCollectOrQueue(transition, (deferred) -> { - transition.collect(r); - r.setShowWhenLocked(showWhenLocked); - if (transition.isNoOp()) { - transition.abort(); - return; - } - controller.requestStartTransition(transition, null /* trigger */, - null /* remoteTransition */, null /* displayChange */); - transition.setReady(r, true); - }); } } finally { Binder.restoreCallingIdentity(origId); @@ -1358,34 +1334,9 @@ class ActivityClientController extends IActivityClientController.Stub { try { synchronized (mGlobalLock) { final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token); - if (r == null) return; - final TransitionController controller = r.mTransitionController; - // If shell transitions is not enabled just set it directly. - if (!controller.isShellTransitionsEnabled()) { - r.setInheritShowWhenLocked(inheritShowWhenLocked); - return; - } - if (controller.isCollecting() - && !mService.mKeyguardController.isKeyguardLocked(r.getDisplayId())) { - // Keyguard isn't locked, so this can be done as part of the collecting - // transition. + if (r != null) { r.setInheritShowWhenLocked(inheritShowWhenLocked); - return; } - final Transition transition = new Transition( - inheritShowWhenLocked ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, - 0 /* flags */, controller, mService.mWindowManager.mSyncEngine); - r.mTransitionController.startCollectOrQueue(transition, (deferred) -> { - transition.collect(r); - r.setInheritShowWhenLocked(inheritShowWhenLocked); - if (transition.isNoOp()) { - transition.abort(); - return; - } - controller.requestStartTransition(transition, null /* trigger */, - null /* remoteTransition */, null /* displayChange */); - transition.setReady(r, true); - }); } } finally { Binder.restoreCallingIdentity(origId); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 1e58306ef8d8..2accf9a2a43a 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2917,26 +2917,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_WINDOW_MANAGER, TAG, cookie); } - /** - * Get whether the transition, in its current state, is a no-op. This should be avoided. It is - * only here for legacy usages where we can't tell ahead-of-time whether something will - * generate a change. - */ - boolean isNoOp() { - for (int i = mParticipants.size() - 1; i >= 0; --i) { - // This is the same criteria as the rejection logic in calculateTargets - final WindowContainer<?> wc = mParticipants.valueAt(i); - if (!wc.isAttached()) continue; - // The level of transition target should be at least window token. - if (wc.asWindowState() != null) continue; - final ChangeInfo changeInfo = mChanges.get(wc); - // Reject no-ops - if (!changeInfo.hasChanged()) continue; - return false; - } - return true; - } - @VisibleForTesting static class ChangeInfo { private static final int FLAG_NONE = 0; diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index f96d9e841eba..9cdaec647a43 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -18,6 +18,7 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS; import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED; +import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START; import static com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR; @@ -517,6 +518,16 @@ public class FingerprintAuthenticationClientTest { } @Test + public void testAuthenticationStateListeners_onAuthenticationAcquired() + throws RemoteException { + final FingerprintAuthenticationClient client = createClient(); + client.start(mCallback); + client.onAcquired(FINGERPRINT_ACQUIRED_START, 0); + + verify(mAuthenticationStateListeners).onAuthenticationAcquired(any(), anyInt(), anyInt()); + } + + @Test public void testAuthenticationStateListeners_onAuthenticationSucceeded() throws RemoteException { mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS); |