diff options
299 files changed, 8067 insertions, 2586 deletions
diff --git a/Android.bp b/Android.bp index d69e51c6e46f..eae0d73f449f 100644 --- a/Android.bp +++ b/Android.bp @@ -359,6 +359,7 @@ java_defaults { "contacts-provider-platform-compat-config", ], libs: [ + "androidx.annotation_annotation", "app-compat-annotations", "ext", "framework-updatable-stubs-module_libs_api", diff --git a/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java index a27e16a46a04..279681bc0d15 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java @@ -51,7 +51,7 @@ public class MessageDigestPerfTest { @Parameterized.Parameter(0) public Algorithm mAlgorithm; - public String mProvider = "AndroidSSL"; + public String mProvider = "AndroidOpenSSL"; private static final int DATA_SIZE = 8192; private static final byte[] DATA = new byte[DATA_SIZE]; diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 0de0a1cf9c8e..d6b246a9e2e3 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -112,6 +112,7 @@ import android.text.TextUtils; import android.text.format.DateFormat; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.EventLog; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.LongArrayQueue; @@ -2299,7 +2300,11 @@ public class AlarmManagerService extends SystemService { + " reached for uid: " + UserHandle.formatUid(callingUid) + ", callingPackage: " + callingPackage; Slog.w(TAG, errorMsg); - throw new IllegalStateException(errorMsg); + if (callingUid != Process.SYSTEM_UID) { + throw new IllegalStateException(errorMsg); + } else { + EventLog.writeEvent(0x534e4554, "234441463", -1, errorMsg); + } } setImplLocked(type, triggerAtTime, triggerElapsed, windowLength, interval, operation, directReceiver, listenerTag, flags, workSource, alarmClock, callingUid, diff --git a/api/Android.bp b/api/Android.bp index 7729a7f75d53..e9930e3a3b5b 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -194,6 +194,37 @@ genrule { "$(location :frameworks-base-api-module-lib-current.txt)", } +// This produces the same annotations.zip as framework-doc-stubs, but by using +// outputs from individual modules instead of all the source code. +genrule_defaults { + name: "sdk-annotations-defaults", + out: ["annotations.zip"], + tools: [ + "merge_annotation_zips", + "soong_zip", + ], + cmd: "$(location merge_annotation_zips) $(genDir)/out $(in) && " + + "$(location soong_zip) -o $(out) -C $(genDir)/out -D $(genDir)/out", +} + +genrule { + name: "sdk-annotations.zip", + defaults: ["sdk-annotations-defaults"], + srcs: [ + ":android-non-updatable-doc-stubs{.annotations.zip}", + ":all-modules-public-annotations", + ], +} + +genrule { + name: "sdk-annotations-system.zip", + defaults: ["sdk-annotations-defaults"], + srcs: [ + ":android-non-updatable-doc-stubs-system{.annotations.zip}", + ":all-modules-system-annotations", + ], +} + genrule { name: "combined-removed-dex", visibility: [ diff --git a/api/api.go b/api/api.go index ca0fc28cdb9d..f15804156bdf 100644 --- a/api/api.go +++ b/api/api.go @@ -148,17 +148,18 @@ func createMergedStubsSrcjar(ctx android.LoadHookContext, modules []string) { ctx.CreateModule(genrule.GenRuleFactory, &props) } -// This produces the same annotations.zip as framework-doc-stubs, but by using -// outputs from individual modules instead of all the source code. -func createMergedAnnotations(ctx android.LoadHookContext, modules []string) { - props := genruleProps{} - props.Name = proptools.StringPtr("sdk-annotations.zip") - props.Tools = []string{"merge_annotation_zips", "soong_zip"} - props.Out = []string{"annotations.zip"} - props.Cmd = proptools.StringPtr("$(location merge_annotation_zips) $(genDir)/out $(in) && " + - "$(location soong_zip) -o $(out) -C $(genDir)/out -D $(genDir)/out") - props.Srcs = append([]string{":android-non-updatable-doc-stubs{.annotations.zip}"}, createSrcs(modules, "{.public.annotations.zip}")...) - ctx.CreateModule(genrule.GenRuleFactory, &props) +func createMergedPublicAnnotationsFilegroup(ctx android.LoadHookContext, modules []string) { + props := fgProps{} + props.Name = proptools.StringPtr("all-modules-public-annotations") + props.Srcs = createSrcs(modules, "{.public.annotations.zip}") + ctx.CreateModule(android.FileGroupFactory, &props) +} + +func createMergedSystemAnnotationsFilegroup(ctx android.LoadHookContext, modules []string) { + props := fgProps{} + props.Name = proptools.StringPtr("all-modules-system-annotations") + props.Srcs = createSrcs(modules, "{.system.annotations.zip}") + ctx.CreateModule(android.FileGroupFactory, &props) } func createFilteredApiVersions(ctx android.LoadHookContext, modules []string) { @@ -291,7 +292,8 @@ func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { createMergedFrameworkModuleLibStubs(ctx, bootclasspath) createMergedFrameworkImpl(ctx, bootclasspath) - createMergedAnnotations(ctx, bootclasspath) + createMergedPublicAnnotationsFilegroup(ctx, bootclasspath) + createMergedSystemAnnotationsFilegroup(ctx, bootclasspath) createFilteredApiVersions(ctx, bootclasspath) diff --git a/cmds/idmap2/idmap2d/Idmap2Service.cpp b/cmds/idmap2/idmap2d/Idmap2Service.cpp index 1b2d905aec0a..073d987f5dad 100644 --- a/cmds/idmap2/idmap2d/Idmap2Service.cpp +++ b/cmds/idmap2/idmap2d/Idmap2Service.cpp @@ -234,7 +234,11 @@ Status Idmap2Service::createFabricatedOverlay( } for (const auto& res : overlay.entries) { - builder.SetResourceValue(res.resourceName, res.dataType, res.data); + if (res.dataType == Res_value::TYPE_STRING) { + builder.SetResourceValue(res.resourceName, res.dataType, res.stringData.value()); + } else { + builder.SetResourceValue(res.resourceName, res.dataType, res.data); + } } // Generate the file path of the fabricated overlay and ensure it does not collide with an diff --git a/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl b/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl index 6c2af274ae32..a6824da8c424 100644 --- a/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl +++ b/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl @@ -23,4 +23,5 @@ parcelable FabricatedOverlayInternalEntry { @utf8InCpp String resourceName; int dataType; int data; + @nullable @utf8InCpp String stringData; }
\ No newline at end of file diff --git a/cmds/idmap2/include/idmap2/FabricatedOverlay.h b/cmds/idmap2/include/idmap2/FabricatedOverlay.h index 375671881e5f..65916d7ebf49 100644 --- a/cmds/idmap2/include/idmap2/FabricatedOverlay.h +++ b/cmds/idmap2/include/idmap2/FabricatedOverlay.h @@ -41,6 +41,9 @@ struct FabricatedOverlay { Builder& SetResourceValue(const std::string& resource_name, uint8_t data_type, uint32_t data_value); + Builder& SetResourceValue(const std::string& resource_name, uint8_t data_type, + const std::string& data_string_value); + WARN_UNUSED Result<FabricatedOverlay> Build(); private: @@ -48,6 +51,7 @@ struct FabricatedOverlay { std::string resource_name; DataType data_type; DataValue data_value; + std::string data_string_value; }; std::string package_name_; diff --git a/cmds/idmap2/include/idmap2/ResourceUtils.h b/cmds/idmap2/include/idmap2/ResourceUtils.h index a0202dfee473..414aa064ada7 100644 --- a/cmds/idmap2/include/idmap2/ResourceUtils.h +++ b/cmds/idmap2/include/idmap2/ResourceUtils.h @@ -40,6 +40,7 @@ using DataValue = uint32_t; // Res_value::data struct TargetValue { DataType data_type; DataValue data_value; + std::string data_string_value; }; namespace utils { diff --git a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp index 8352dbb7b619..4d49674efce3 100644 --- a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp +++ b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp @@ -65,7 +65,13 @@ FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetOverlayable(const std FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetResourceValue( const std::string& resource_name, uint8_t data_type, uint32_t data_value) { - entries_.emplace_back(Entry{resource_name, data_type, data_value}); + entries_.emplace_back(Entry{resource_name, data_type, data_value, ""}); + return *this; +} + +FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetResourceValue( + const std::string& resource_name, uint8_t data_type, const std::string& data_string_value) { + entries_.emplace_back(Entry{resource_name, data_type, 0, data_string_value}); return *this; } @@ -111,7 +117,8 @@ Result<FabricatedOverlay> FabricatedOverlay::Builder::Build() { entry = type->second.insert(std::make_pair(entry_name.to_string(), TargetValue())).first; } - entry->second = TargetValue{res_entry.data_type, res_entry.data_value}; + entry->second = TargetValue{ + res_entry.data_type, res_entry.data_value, res_entry.data_string_value}; } pb::FabricatedOverlay overlay_pb; diff --git a/core/api/current.txt b/core/api/current.txt index b4f75d4fb193..10957ff8bb58 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -78,6 +78,7 @@ package android { field public static final String CHANGE_WIFI_MULTICAST_STATE = "android.permission.CHANGE_WIFI_MULTICAST_STATE"; field public static final String CHANGE_WIFI_STATE = "android.permission.CHANGE_WIFI_STATE"; field public static final String CLEAR_APP_CACHE = "android.permission.CLEAR_APP_CACHE"; + field public static final String CONFIGURE_WIFI_DISPLAY = "android.permission.CONFIGURE_WIFI_DISPLAY"; field public static final String CONTROL_LOCATION_UPDATES = "android.permission.CONTROL_LOCATION_UPDATES"; field public static final String DELETE_CACHE_FILES = "android.permission.DELETE_CACHE_FILES"; field public static final String DELETE_PACKAGES = "android.permission.DELETE_PACKAGES"; @@ -191,6 +192,7 @@ package android { field public static final String SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE = "android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE"; field public static final String SYSTEM_ALERT_WINDOW = "android.permission.SYSTEM_ALERT_WINDOW"; field public static final String TRANSMIT_IR = "android.permission.TRANSMIT_IR"; + field public static final String TURN_SCREEN_ON = "android.permission.TURN_SCREEN_ON"; field public static final String UNINSTALL_SHORTCUT = "com.android.launcher.permission.UNINSTALL_SHORTCUT"; field public static final String UPDATE_DEVICE_STATS = "android.permission.UPDATE_DEVICE_STATS"; field public static final String UPDATE_PACKAGES_WITHOUT_USER_ACTION = "android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION"; @@ -9008,7 +9010,8 @@ package android.companion { method @Deprecated public boolean hasNotificationAccess(android.content.ComponentName); method public void requestNotificationAccess(android.content.ComponentName); method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException; - method public void startSystemDataTransfer(int) throws android.companion.DeviceNotAssociatedException; + method @Deprecated public void startSystemDataTransfer(int) throws android.companion.DeviceNotAssociatedException; + method public void startSystemDataTransfer(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.companion.CompanionException>) throws android.companion.DeviceNotAssociatedException; method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException; field public static final String EXTRA_ASSOCIATION = "android.companion.extra.ASSOCIATION"; field @Deprecated public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE"; @@ -9034,6 +9037,9 @@ package android.companion { field public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService"; } + public class CompanionException extends java.lang.RuntimeException { + } + public interface DeviceFilter<D extends android.os.Parcelable> extends android.os.Parcelable { } @@ -18154,7 +18160,7 @@ package android.hardware.camera2.params { } public final class Capability { - ctor public Capability(int, int, int, float, float); + ctor public Capability(int, @NonNull android.util.Size, @NonNull android.util.Range<java.lang.Float>); method @NonNull public android.util.Size getMaxStreamingSize(); method public int getMode(); method @NonNull public android.util.Range<java.lang.Float> getZoomRatioRange(); @@ -18169,12 +18175,17 @@ package android.hardware.camera2.params { } public final class DeviceStateSensorOrientationMap { - ctor public DeviceStateSensorOrientationMap(@NonNull long[]); method public int getSensorOrientation(long); field public static final long FOLDED = 4L; // 0x4L field public static final long NORMAL = 0L; // 0x0L } + public static final class DeviceStateSensorOrientationMap.Builder { + ctor public DeviceStateSensorOrientationMap.Builder(); + method @NonNull public android.hardware.camera2.params.DeviceStateSensorOrientationMap.Builder addOrientationForState(long, long); + method @NonNull public android.hardware.camera2.params.DeviceStateSensorOrientationMap build(); + } + public final class DynamicRangeProfiles { ctor public DynamicRangeProfiles(@NonNull long[]); method @NonNull public java.util.Set<java.lang.Long> getProfileCaptureRequestConstraints(long); @@ -31900,7 +31911,7 @@ package android.os { method public android.os.PowerManager.WakeLock newWakeLock(int, String); method @RequiresPermission(android.Manifest.permission.REBOOT) public void reboot(@Nullable String); method public void removeThermalStatusListener(@NonNull android.os.PowerManager.OnThermalStatusChangedListener); - field @Deprecated public static final int ACQUIRE_CAUSES_WAKEUP = 268435456; // 0x10000000 + field @Deprecated @RequiresPermission(value=android.Manifest.permission.TURN_SCREEN_ON, conditional=true) public static final int ACQUIRE_CAUSES_WAKEUP = 268435456; // 0x10000000 field public static final String ACTION_DEVICE_IDLE_MODE_CHANGED = "android.os.action.DEVICE_IDLE_MODE_CHANGED"; field public static final String ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED = "android.os.action.LIGHT_DEVICE_IDLE_MODE_CHANGED"; field public static final String ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED = "android.os.action.LOW_POWER_STANDBY_ENABLED_CHANGED"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index e1ce5bd71423..8a0acd509727 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -100,7 +100,6 @@ package android { field public static final String COMPANION_APPROVE_WIFI_CONNECTIONS = "android.permission.COMPANION_APPROVE_WIFI_CONNECTIONS"; field public static final String CONFIGURE_DISPLAY_BRIGHTNESS = "android.permission.CONFIGURE_DISPLAY_BRIGHTNESS"; field public static final String CONFIGURE_INTERACT_ACROSS_PROFILES = "android.permission.CONFIGURE_INTERACT_ACROSS_PROFILES"; - field public static final String CONFIGURE_WIFI_DISPLAY = "android.permission.CONFIGURE_WIFI_DISPLAY"; field @Deprecated public static final String CONNECTIVITY_INTERNAL = "android.permission.CONNECTIVITY_INTERNAL"; field public static final String CONNECTIVITY_USE_RESTRICTED_NETWORKS = "android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"; field public static final String CONTROL_DEVICE_LIGHTS = "android.permission.CONTROL_DEVICE_LIGHTS"; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index cb64173b7809..125f5e4e1a21 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1906,6 +1906,7 @@ public class AppOpsManager { OP_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER, OP_SCHEDULE_EXACT_ALARM, OP_MANAGE_MEDIA, + OP_TURN_SCREEN_ON, }; /** @@ -2362,7 +2363,7 @@ public class AppOpsManager { null, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, - null, // no permission for turning the screen on + Manifest.permission.TURN_SCREEN_ON, Manifest.permission.GET_ACCOUNTS, null, // no permission for running in background null, // no permission for changing accessibility volume @@ -2491,7 +2492,7 @@ public class AppOpsManager { null, // MOCK_LOCATION null, // READ_EXTERNAL_STORAGE null, // WRITE_EXTERNAL_STORAGE - null, // TURN_ON_SCREEN + null, // TURN_SCREEN_ON null, // GET_ACCOUNTS null, // RUN_IN_BACKGROUND UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_ACCESSIBILITY_VOLUME @@ -2619,7 +2620,7 @@ public class AppOpsManager { null, // MOCK_LOCATION null, // READ_EXTERNAL_STORAGE null, // WRITE_EXTERNAL_STORAGE - null, // TURN_ON_SCREEN + null, // TURN_SCREEN_ON null, // GET_ACCOUNTS null, // RUN_IN_BACKGROUND null, // AUDIO_ACCESSIBILITY_VOLUME @@ -2746,7 +2747,7 @@ public class AppOpsManager { AppOpsManager.MODE_ERRORED, // MOCK_LOCATION AppOpsManager.MODE_ALLOWED, // READ_EXTERNAL_STORAGE AppOpsManager.MODE_ALLOWED, // WRITE_EXTERNAL_STORAGE - AppOpsManager.MODE_ALLOWED, // TURN_SCREEN_ON + AppOpsManager.MODE_ERRORED, // TURN_SCREEN_ON AppOpsManager.MODE_ALLOWED, // GET_ACCOUNTS AppOpsManager.MODE_ALLOWED, // RUN_IN_BACKGROUND AppOpsManager.MODE_ALLOWED, // AUDIO_ACCESSIBILITY_VOLUME @@ -7563,10 +7564,15 @@ public class AppOpsManager { @SystemApi @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) public @NonNull List<AppOpsManager.PackageOps> getPackagesForOps(@Nullable String[] ops) { - final int opCount = ops.length; - final int[] opCodes = new int[opCount]; - for (int i = 0; i < opCount; i++) { - opCodes[i] = sOpStrToOp.get(ops[i]); + final int[] opCodes; + if (ops != null) { + final int opCount = ops.length; + opCodes = new int[opCount]; + for (int i = 0; i < opCount; i++) { + opCodes[i] = sOpStrToOp.get(ops[i]); + } + } else { + opCodes = null; } final List<AppOpsManager.PackageOps> result = getPackagesForOps(opCodes); return (result != null) ? result : Collections.emptyList(); diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING index 908d3f802764..5b0bd9614f83 100644 --- a/core/java/android/app/TEST_MAPPING +++ b/core/java/android/app/TEST_MAPPING @@ -121,6 +121,9 @@ "include-annotation": "android.platform.test.annotations.Presubmit" }, { + "exclude-annotation": "android.platform.test.annotations.LargeTest" + }, + { "exclude-annotation": "androidx.test.filters.FlakyTest" }, { diff --git a/core/java/android/app/admin/SecurityLog.java b/core/java/android/app/admin/SecurityLog.java index b90408d4395e..904db5f86c02 100644 --- a/core/java/android/app/admin/SecurityLog.java +++ b/core/java/android/app/admin/SecurityLog.java @@ -518,7 +518,7 @@ public class SecurityLog { /** * Indicates that an event occurred as the device attempted to connect to - * a WiFi network. The log entry contains the following information about the + * a managed WiFi network. The log entry contains the following information about the * event, encapsulated in an {@link Object} array and accessible via * {@link SecurityEvent#getData()}: * <li> [0] Last 2 octets of the network BSSID ({@code String}, in the form "xx:xx:xx:xx:AA:BB") @@ -530,7 +530,7 @@ public class SecurityLog { public static final int TAG_WIFI_CONNECTION = SecurityLogTags.SECURITY_WIFI_CONNECTION; /** - * Indicates that the device disconnects from a connected WiFi network. + * Indicates that the device disconnects from a managed WiFi network. * The log entry contains the following information about the * event, encapsulated in an {@link Object} array and accessible via * {@link SecurityEvent#getData()}: diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 14c671f32c21..1b51faf3d429 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -38,15 +38,23 @@ import android.content.IntentSender; import android.content.pm.PackageManager; import android.net.MacAddress; import android.os.Handler; +import android.os.OutcomeReceiver; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.util.ExceptionUtils; import android.util.Log; +import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.CollectionUtils; +import libcore.io.IoUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -273,6 +281,9 @@ public final class CompanionDeviceManager { @GuardedBy("mListeners") private final ArrayList<OnAssociationsChangedListenerProxy> mListeners = new ArrayList<>(); + @GuardedBy("mTransports") + private final SparseArray<Transport> mTransports = new SparseArray<>(); + /** @hide */ public CompanionDeviceManager( @Nullable ICompanionDeviceManager service, @NonNull Context context) { @@ -800,6 +811,36 @@ public final class CompanionDeviceManager { } } + /** {@hide} */ + public final void attachSystemDataTransport(int associationId, @NonNull InputStream in, + @NonNull OutputStream out) throws DeviceNotAssociatedException { + synchronized (mTransports) { + if (mTransports.contains(associationId)) { + detachSystemDataTransport(associationId); + } + + try { + final Transport transport = new Transport(associationId, in, out); + mTransports.put(associationId, transport); + transport.start(); + } catch (IOException e) { + throw new RuntimeException("Failed to attach transport", e); + } + } + } + + /** {@hide} */ + public final void detachSystemDataTransport(int associationId) + throws DeviceNotAssociatedException { + synchronized (mTransports) { + final Transport transport = mTransports.get(associationId); + if (transport != null) { + mTransports.delete(associationId); + transport.stop(); + } + } + } + /** * Associates given device with given app for the given user directly, without UI prompt. * @@ -924,12 +965,44 @@ public final class CompanionDeviceManager { * @param associationId The unique {@link AssociationInfo#getId ID} assigned to the Association * of the companion device recorded by CompanionDeviceManager * @throws DeviceNotAssociatedException Exception if the companion device is not associated + * + * @deprecated Use {@link #startSystemDataTransfer(int, Executor, OutcomeReceiver)} instead. */ + @Deprecated @UserHandleAware public void startSystemDataTransfer(int associationId) throws DeviceNotAssociatedException { try { mService.startSystemDataTransfer(mContext.getOpPackageName(), mContext.getUserId(), - associationId); + associationId, null); + } catch (RemoteException e) { + ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); + throw e.rethrowFromSystemServer(); + } + } + + /** + * Start system data transfer which has been previously approved by the user. + * + * <p>Before calling this method, the app needs to make sure there's a communication channel + * between two devices, and has prompted user consent dialogs built by one of these methods: + * {@link #buildPermissionTransferUserConsentIntent(int)}. + * The transfer may fail if the communication channel is disconnected during the transfer.</p> + * + * @param associationId The unique {@link AssociationInfo#getId ID} assigned to the Association + * of the companion device recorded by CompanionDeviceManager + * @param executor The executor which will be used to invoke the result callback. + * @param result The callback to notify the app of the result of the system data transfer. + * @throws DeviceNotAssociatedException Exception if the companion device is not associated + */ + @UserHandleAware + public void startSystemDataTransfer( + int associationId, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Void, CompanionException> result) + throws DeviceNotAssociatedException { + try { + mService.startSystemDataTransfer(mContext.getOpPackageName(), mContext.getUserId(), + associationId, new SystemDataTransferCallbackProxy(executor, result)); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); throw e.rethrowFromSystemServer(); @@ -1004,4 +1077,114 @@ public final class CompanionDeviceManager { mExecutor.execute(() -> mListener.onAssociationsChanged(associations)); } } + + private static class SystemDataTransferCallbackProxy extends ISystemDataTransferCallback.Stub { + private final Executor mExecutor; + private final OutcomeReceiver<Void, CompanionException> mCallback; + + private SystemDataTransferCallbackProxy(Executor executor, + OutcomeReceiver<Void, CompanionException> callback) { + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onResult() { + mExecutor.execute(() -> mCallback.onResult(null)); + } + + @Override + public void onError(String error) { + mExecutor.execute(() -> mCallback.onError(new CompanionException(error))); + } + } + + /** + * Representation of an active system data transport. + * <p> + * Internally uses two threads to shuttle bidirectional data between a + * remote device and a {@code socketpair} that the system is listening to. + * This design ensures that data payloads are transported efficiently + * without adding Binder traffic contention. + */ + private class Transport { + private final int mAssociationId; + private final InputStream mRemoteIn; + private final OutputStream mRemoteOut; + + private InputStream mLocalIn; + private OutputStream mLocalOut; + + private volatile boolean mStopped; + + public Transport(int associationId, InputStream remoteIn, OutputStream remoteOut) { + mAssociationId = associationId; + mRemoteIn = remoteIn; + mRemoteOut = remoteOut; + } + + public void start() throws IOException { + final ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair(); + mLocalIn = new ParcelFileDescriptor.AutoCloseInputStream(pair[0]); + mLocalOut = new ParcelFileDescriptor.AutoCloseOutputStream(pair[0]); + + try { + mService.attachSystemDataTransport(mContext.getPackageName(), + mContext.getUserId(), mAssociationId, pair[1]); + } catch (RemoteException e) { + throw new IOException("Failed to configure transport", e); + } + + new Thread(() -> { + try { + copyWithFlushing(mLocalIn, mRemoteOut); + } catch (IOException e) { + if (!mStopped) { + Log.w(LOG_TAG, "Trouble during outgoing transport", e); + stop(); + } + } + }).start(); + new Thread(() -> { + try { + copyWithFlushing(mRemoteIn, mLocalOut); + } catch (IOException e) { + if (!mStopped) { + Log.w(LOG_TAG, "Trouble during incoming transport", e); + stop(); + } + } + }).start(); + } + + public void stop() { + mStopped = true; + + IoUtils.closeQuietly(mRemoteIn); + IoUtils.closeQuietly(mRemoteOut); + IoUtils.closeQuietly(mLocalIn); + IoUtils.closeQuietly(mLocalOut); + + try { + mService.detachSystemDataTransport(mContext.getPackageName(), + mContext.getUserId(), mAssociationId); + } catch (RemoteException e) { + Log.w(LOG_TAG, "Failed to detach transport", e); + } + } + + /** + * Copy all data from the first stream to the second stream, flushing + * after every write to ensure that we quickly deliver all pending data. + */ + private void copyWithFlushing(@NonNull InputStream in, @NonNull OutputStream out) + throws IOException { + byte[] buffer = new byte[8192]; + int c; + while ((c = in.read(buffer)) != -1) { + out.write(buffer, 0, c); + out.flush(); + } + } + } } diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java index 791fc2aaa2cb..83d2713ea114 100644 --- a/core/java/android/companion/CompanionDeviceService.java +++ b/core/java/android/companion/CompanionDeviceService.java @@ -23,11 +23,14 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.app.Service; +import android.bluetooth.BluetoothSocket; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.util.Log; +import java.io.InputStream; +import java.io.OutputStream; import java.util.Objects; import java.util.concurrent.Executor; @@ -201,6 +204,45 @@ public abstract class CompanionDeviceService extends Service { } /** + * Attach the given bidirectional communication streams to be used for + * transporting system data between associated devices. + * <p> + * The companion service providing these streams is responsible for ensuring + * that all data is transported accurately and in-order between the two + * devices, including any fragmentation and re-assembly when carried over a + * size-limited transport. + * <p> + * As an example, it's valid to provide streams obtained from a + * {@link BluetoothSocket} to this method, since {@link BluetoothSocket} + * meets the API contract described above. + * + * @param associationId id of the associated device + * @param in already connected stream of data incoming from remote + * associated device + * @param out already connected stream of data outgoing to remote associated + * device + * @hide + */ + public final void attachSystemDataTransport(int associationId, @NonNull InputStream in, + @NonNull OutputStream out) throws DeviceNotAssociatedException { + getSystemService(CompanionDeviceManager.class) + .attachSystemDataTransport(associationId, in, out); + } + + /** + * Detach any bidirectional communication streams previously configured + * through {@link #attachSystemDataTransport}. + * + * @param associationId id of the associated device + * @hide + */ + public final void detachSystemDataTransport(int associationId) + throws DeviceNotAssociatedException { + getSystemService(CompanionDeviceManager.class) + .detachSystemDataTransport(associationId); + } + + /** * Called by system whenever a device associated with this app is connected. * * @param associationInfo A record for the companion device. diff --git a/core/java/android/companion/CompanionException.java b/core/java/android/companion/CompanionException.java new file mode 100644 index 000000000000..fb78e8df446e --- /dev/null +++ b/core/java/android/companion/CompanionException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.companion; + +import android.annotation.NonNull; + +/** + * {@code CompanionException} can be thrown during the companion system data transfer process. + */ +public class CompanionException extends RuntimeException { + /** @hide */ + public CompanionException(@NonNull String message) { + super(message); + } +} diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index 085fd1b4c388..42f908396799 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -19,6 +19,7 @@ package android.companion; import android.app.PendingIntent; import android.companion.IAssociationRequestCallback; import android.companion.IOnAssociationsChangedListener; +import android.companion.ISystemDataTransferCallback; import android.companion.AssociationInfo; import android.companion.AssociationRequest; import android.content.ComponentName; @@ -75,5 +76,10 @@ interface ICompanionDeviceManager { PendingIntent buildPermissionTransferUserConsentIntent(String callingPackage, int userId, int associationId); - void startSystemDataTransfer(String packageName, int userId, int associationId); + void startSystemDataTransfer(String packageName, int userId, int associationId, + in ISystemDataTransferCallback callback); + + void attachSystemDataTransport(String packageName, int userId, int associationId, in ParcelFileDescriptor fd); + + void detachSystemDataTransport(String packageName, int userId, int associationId); } diff --git a/core/java/android/companion/ISystemDataTransferCallback.aidl b/core/java/android/companion/ISystemDataTransferCallback.aidl new file mode 100644 index 000000000000..1ae5376942e2 --- /dev/null +++ b/core/java/android/companion/ISystemDataTransferCallback.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing per missions and + * limitations under the License. + */ + +package android.companion; + +/** @hide */ +interface ISystemDataTransferCallback { + oneway void onResult(); + + oneway void onError(String error); +}
\ No newline at end of file diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index fce23cf6819a..97da2daf6e59 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3990,7 +3990,7 @@ public abstract class Context { * sockets and networks. * <dt> {@link #WIFI_SERVICE} ("wifi") * <dd> A {@link android.net.wifi.WifiManager WifiManager} for management of Wi-Fi - * connectivity. On releases before NYC, it should only be obtained from an application + * connectivity. On releases before Android 7, it should only be obtained from an application * context, and not from any other derived context to avoid memory leaks within the calling * process. * <dt> {@link #WIFI_AWARE_SERVICE} ("wifiaware") diff --git a/core/java/android/content/TEST_MAPPING b/core/java/android/content/TEST_MAPPING index 5bb845d5a1a1..dac79e7124bd 100644 --- a/core/java/android/content/TEST_MAPPING +++ b/core/java/android/content/TEST_MAPPING @@ -7,6 +7,9 @@ "include-annotation": "android.platform.test.annotations.Presubmit" }, { + "exclude-annotation": "android.platform.test.annotations.LargeTest" + }, + { "exclude-annotation": "androidx.test.filters.FlakyTest" }, { diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java index d62b47b34dcb..5efc1f9f7cca 100644 --- a/core/java/android/content/om/FabricatedOverlay.java +++ b/core/java/android/content/om/FabricatedOverlay.java @@ -88,7 +88,7 @@ public class FabricatedOverlay { } /** - * Sets the value of + * Sets the value of the fabricated overlay * * @param resourceName name of the target resource to overlay (in the form * [package]:type/entry) @@ -106,6 +106,25 @@ public class FabricatedOverlay { return this; } + /** + * Sets the value of the fabricated overlay + * + * @param resourceName name of the target resource to overlay (in the form + * [package]:type/entry) + * @param dataType the data type of the new value + * @param value the string representing the new value + * + * @see android.util.TypedValue#type + */ + public Builder setResourceValue(@NonNull String resourceName, int dataType, String value) { + final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry(); + entry.resourceName = resourceName; + entry.dataType = dataType; + entry.stringData = value; + mEntries.add(entry); + return this; + } + /** Builds an immutable fabricated overlay. */ public FabricatedOverlay build() { final FabricatedOverlayInternal overlay = new FabricatedOverlayInternal(); diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 20a4fdf658c6..10d6f2d6d04b 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -542,14 +542,17 @@ public class ApkLiteParseUtils { int minVer = DEFAULT_MIN_SDK_VERSION; String minCode = null; + boolean minAssigned = false; int targetVer = DEFAULT_TARGET_SDK_VERSION; String targetCode = null; if (!TextUtils.isEmpty(minSdkVersionString)) { try { minVer = Integer.parseInt(minSdkVersionString); + minAssigned = true; } catch (NumberFormatException ignored) { minCode = minSdkVersionString; + minAssigned = !TextUtils.isEmpty(minCode); } } @@ -558,7 +561,7 @@ public class ApkLiteParseUtils { targetVer = Integer.parseInt(targetSdkVersionString); } catch (NumberFormatException ignored) { targetCode = targetSdkVersionString; - if (minCode == null) { + if (!minAssigned) { minCode = targetCode; } } diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java index 216c9c26424d..db0cac3eb9c7 100644 --- a/core/java/android/database/sqlite/SQLiteConnectionPool.java +++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java @@ -170,8 +170,8 @@ public final class SQLiteConnectionPool implements Closeable { // If timeout is set, setup idle connection handler // In case of MAX_VALUE - idle connections are never closed if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) { - setupIdleConnectionHandler(Looper.getMainLooper(), - mConfiguration.idleConnectionTimeoutMs); + setupIdleConnectionHandler( + Looper.getMainLooper(), mConfiguration.idleConnectionTimeoutMs, null); } } @@ -425,7 +425,7 @@ public final class SQLiteConnectionPool implements Closeable { mAvailablePrimaryConnection = connection; } wakeConnectionWaitersLocked(); - } else if (mAvailableNonPrimaryConnections.size() >= mMaxConnectionPoolSize - 1) { + } else if (mAvailableNonPrimaryConnections.size() >= mMaxConnectionPoolSize) { closeConnectionAndLogExceptionsLocked(connection); } else { if (recycleConnectionLocked(connection, status)) { @@ -456,6 +456,11 @@ public final class SQLiteConnectionPool implements Closeable { return true; } + @VisibleForTesting + public boolean hasAnyAvailableNonPrimaryConnection() { + return mAvailableNonPrimaryConnections.size() > 0; + } + /** * Returns true if the session should yield the connection due to * contention over available database connections. @@ -1061,9 +1066,11 @@ public final class SQLiteConnectionPool implements Closeable { * Set up the handler based on the provided looper and timeout. */ @VisibleForTesting - public void setupIdleConnectionHandler(Looper looper, long timeoutMs) { + public void setupIdleConnectionHandler( + Looper looper, long timeoutMs, Runnable onAllConnectionsIdle) { synchronized (mLock) { - mIdleConnectionHandler = new IdleConnectionHandler(looper, timeoutMs); + mIdleConnectionHandler = + new IdleConnectionHandler(looper, timeoutMs, onAllConnectionsIdle); } } @@ -1228,10 +1235,12 @@ public final class SQLiteConnectionPool implements Closeable { private class IdleConnectionHandler extends Handler { private final long mTimeout; + private final Runnable mOnAllConnectionsIdle; - IdleConnectionHandler(Looper looper, long timeout) { + IdleConnectionHandler(Looper looper, long timeout, Runnable onAllConnectionsIdle) { super(looper); mTimeout = timeout; + this.mOnAllConnectionsIdle = onAllConnectionsIdle; } @Override @@ -1247,6 +1256,9 @@ public final class SQLiteConnectionPool implements Closeable { + " after " + mTimeout); } } + if (mOnAllConnectionsIdle != null) { + mOnAllConnectionsIdle.run(); + } } } diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java index 468e6041eb73..ee12df547291 100644 --- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java +++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java @@ -88,8 +88,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -1734,12 +1734,12 @@ public class CameraMetadataNative implements Parcelable { int height = maxSizes[3 * i + 2]; if (mode != CameraMetadata.CONTROL_EXTENDED_SCENE_MODE_DISABLED && j < numExtendedSceneModeZoomRanges) { - capabilities[i] = new Capability(mode, width, height, zoomRanges[2 * j], - zoomRanges[2 * j + 1]); + capabilities[i] = new Capability(mode, new Size(width, height), + new Range<Float>(zoomRanges[2 * j], zoomRanges[2 * j + 1])); j++; } else { - capabilities[i] = new Capability(mode, width, height, modeOffMinZoomRatio, - modeOffMaxZoomRatio); + capabilities[i] = new Capability(mode, new Size(width, height), + new Range<Float>(modeOffMinZoomRatio, modeOffMaxZoomRatio)); } } diff --git a/core/java/android/hardware/camera2/params/Capability.java b/core/java/android/hardware/camera2/params/Capability.java index fd71c82817f5..a015f720241f 100644 --- a/core/java/android/hardware/camera2/params/Capability.java +++ b/core/java/android/hardware/camera2/params/Capability.java @@ -40,10 +40,8 @@ public final class Capability { public static final int COUNT = 3; private final int mMode; - private final int mMaxStreamingWidth; - private final int mMaxStreamingHeight; - private final float mMinZoomRatio; - private final float mMaxZoomRatio; + private final Size mMaxStreamingSize; + private final Range<Float> mZoomRatioRange; /** * Create a new Capability object. @@ -57,29 +55,30 @@ public final class Capability { * objects during normal use of the camera API.</p> * * @param mode supported mode for a camera capability. - * @param maxStreamingWidth The width of the maximum streaming size for this mode - * @param maxStreamingHeight The height of the maximum streaming size for this mode - * @param minZoomRatio the minimum zoom ratio this mode supports - * @param maxZoomRatio the maximum zoom ratio this mode supports + * @param maxStreamingSize The maximum streaming size for this mode + * @param zoomRatioRange the minimum/maximum zoom ratio this mode supports * - * @throws IllegalArgumentException if any of the argument is not valid + * @throws IllegalArgumentException if any of the arguments are not valid */ - public Capability(int mode, int maxStreamingWidth, int maxStreamingHeight, - float minZoomRatio, float maxZoomRatio) { + public Capability(int mode, @NonNull Size maxStreamingSize, + @NonNull Range<Float> zoomRatioRange) { mMode = mode; - mMaxStreamingWidth = checkArgumentNonnegative(maxStreamingWidth, - "maxStreamingWidth must be nonnegative"); - mMaxStreamingHeight = checkArgumentNonnegative(maxStreamingHeight, - "maxStreamingHeight must be nonnegative"); + checkArgumentNonnegative(maxStreamingSize.getWidth(), + "maxStreamingSize.getWidth() must be nonnegative"); + checkArgumentNonnegative(maxStreamingSize.getHeight(), + "maxStreamingSize.getHeight() must be nonnegative"); + mMaxStreamingSize = maxStreamingSize; - if (minZoomRatio > maxZoomRatio) { - throw new IllegalArgumentException("minZoomRatio " + minZoomRatio - + " is greater than maxZoomRatio " + maxZoomRatio); + if (zoomRatioRange.getLower() > zoomRatioRange.getUpper()) { + throw new IllegalArgumentException("zoomRatioRange.getLower() " + + zoomRatioRange.getLower() + " is greater than zoomRatioRange.getUpper() " + + zoomRatioRange.getUpper()); } - mMinZoomRatio = checkArgumentPositive(minZoomRatio, - "minZoomRatio must be positive"); - mMaxZoomRatio = checkArgumentPositive(maxZoomRatio, - "maxZoomRatio must be positive"); + checkArgumentPositive(zoomRatioRange.getLower(), + "zoomRatioRange.getLower() must be positive"); + checkArgumentPositive(zoomRatioRange.getUpper(), + "zoomRatioRange.getUpper() must be positive"); + mZoomRatioRange = zoomRatioRange; } /** @@ -100,7 +99,7 @@ public final class Capability { * @return a new {@link Size} with non-negative width and height */ public @NonNull Size getMaxStreamingSize() { - return new Size(mMaxStreamingWidth, mMaxStreamingHeight); + return mMaxStreamingSize; } /** @@ -109,7 +108,7 @@ public final class Capability { * @return The supported zoom ratio range supported by this capability */ public @NonNull Range<Float> getZoomRatioRange() { - return new Range<Float>(mMinZoomRatio, mMaxZoomRatio); + return mZoomRatioRange; } @@ -132,10 +131,8 @@ public final class Capability { if (obj instanceof Capability) { final Capability other = (Capability) obj; return (mMode == other.mMode - && mMaxStreamingWidth == other.mMaxStreamingWidth - && mMaxStreamingHeight == other.mMaxStreamingHeight - && mMinZoomRatio == other.mMinZoomRatio - && mMaxZoomRatio == other.mMaxZoomRatio); + && mMaxStreamingSize.equals(other.mMaxStreamingSize) + && mZoomRatioRange.equals(other.mZoomRatioRange)); } return false; } @@ -145,8 +142,9 @@ public final class Capability { */ @Override public int hashCode() { - return HashCodeHelpers.hashCode(mMode, mMaxStreamingWidth, mMaxStreamingHeight, - mMinZoomRatio, mMaxZoomRatio); + return HashCodeHelpers.hashCode(mMode, mMaxStreamingSize.getWidth(), + mMaxStreamingSize.getHeight(), mZoomRatioRange.getLower(), + mZoomRatioRange.getUpper()); } /** @@ -158,7 +156,7 @@ public final class Capability { @Override public String toString() { return String.format("(mode:%d, maxStreamingSize:%d x %d, zoomRatio: %f-%f)", - mMode, mMaxStreamingWidth, mMaxStreamingHeight, mMinZoomRatio, - mMaxZoomRatio); + mMode, mMaxStreamingSize.getWidth(), mMaxStreamingSize.getHeight(), + mZoomRatioRange.getLower(), mZoomRatioRange.getUpper()); } } diff --git a/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java b/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java index c4c8eb9ef6f0..b9a327b5b61d 100644 --- a/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java +++ b/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java @@ -18,11 +18,13 @@ package android.hardware.camera2.params; import android.annotation.LongDef; import android.annotation.NonNull; +import android.annotation.SuppressLint; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.utils.HashCodeHelpers; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Objects; @@ -68,24 +70,20 @@ public final class DeviceStateSensorOrientationMap { FOLDED }) public @interface DeviceState {}; - private final HashMap<Long, Integer> mDeviceStateOrientationMap = new HashMap<>(); + private final HashMap<Long, Integer> mDeviceStateOrientationMap; /** * Create a new immutable DeviceStateOrientationMap instance. * - * <p>The array is a list of pairs of elements (angle, deviceState):</p> + * <p>The array is a list of pairs of elements (deviceState, angle):</p> * - * <code>[angle0, state0, angle1, state1,..., angleN, stateN]</code> + * <code>[state0, angle0, state1, angle1,..., stateN, angleN]</code> * * <p>Each pair describes the camera sensor orientation when the device is in the * matching deviceState. The angle is in degrees, and must be a multiple of 90.</p> * * <p>This constructor takes over the array; do not write to the array afterwards.</p> * - * <p>This constructor is public to allow for easier application testing by - * creating custom object instances. It's not necessary to construct these - * objects during normal use of the camera API.</p> - * * @param elements * An array of elements describing the map * @@ -94,9 +92,12 @@ public final class DeviceStateSensorOrientationMap { * invalid element values * @throws NullPointerException * if {@code elements} is {@code null} + * + * @hide */ public DeviceStateSensorOrientationMap(@NonNull final long[] elements) { mElements = Objects.requireNonNull(elements, "elements must not be null"); + mDeviceStateOrientationMap = new HashMap<>(); if ((elements.length % 2) != 0) { throw new IllegalArgumentException("Device state sensor orientation map length " + elements.length + " is not even!"); @@ -113,6 +114,20 @@ public final class DeviceStateSensorOrientationMap { } /** + * Used by the Builder only. + * + * @hide + */ + private DeviceStateSensorOrientationMap(@NonNull final ArrayList<Long> elements, + @NonNull final HashMap<Long, Integer> deviceStateOrientationMap) { + mElements = new long[elements.size()]; + for (int i = 0; i < elements.size(); i++) { + mElements[i] = elements.get(i); + } + mDeviceStateOrientationMap = deviceStateOrientationMap; + } + + /** * Return the logical camera sensor orientation given a specific device fold state. * * @param deviceState Device fold state @@ -163,4 +178,58 @@ public final class DeviceStateSensorOrientationMap { } private final long[] mElements; + + /** + * Builds a DeviceStateSensorOrientationMap object. + * + * <p>This builder is public to allow for easier application testing by + * creating custom object instances. It's not necessary to construct these + * objects during normal use of the camera API.</p> + */ + public static final class Builder { + public Builder() { + // Empty + } + + /** + * Add a sensor orientation for a given device state. + * + * <p>Each pair of deviceState and angle describes the camera sensor orientation when the + * device is in the matching deviceState. The angle is in degrees, and must be a multiple + * of 90.</p> + * + * @param deviceState The deviceState. + * @param angle The orientation angle in degrees. + * @return This builder. + * + */ + @SuppressLint("MissingGetterMatchingBuilder") + public @NonNull Builder addOrientationForState(long deviceState, long angle) { + if (angle % 90 != 0) { + throw new IllegalArgumentException("Sensor orientation not divisible by 90: " + + angle); + } + mDeviceStateOrientationMap.put(deviceState, Math.toIntExact(angle)); + mElements.add(deviceState); + mElements.add(angle); + return this; + } + + /** + * Returns an instance of <code>DeviceStateSensorOrientationMap</code> created from the + * fields set on this builder. + * + * @return A DeviceStateSensorOrientationMap. + */ + public @NonNull DeviceStateSensorOrientationMap build() { + if (mElements.size() == 0) { + throw new IllegalStateException("Cannot build a DeviceStateSensorOrientationMap" + + " with zero elements."); + } + return new DeviceStateSensorOrientationMap(mElements, mDeviceStateOrientationMap); + } + + private final ArrayList<Long> mElements = new ArrayList<>(); + private final HashMap<Long, Integer> mDeviceStateOrientationMap = new HashMap<>(); + } } diff --git a/core/java/android/hardware/hdmi/HdmiPlaybackClient.java b/core/java/android/hardware/hdmi/HdmiPlaybackClient.java index d06bc1d2053d..3e41d63b0365 100644 --- a/core/java/android/hardware/hdmi/HdmiPlaybackClient.java +++ b/core/java/android/hardware/hdmi/HdmiPlaybackClient.java @@ -122,6 +122,9 @@ public final class HdmiPlaybackClient extends HdmiClient { } private IHdmiControlCallback getCallbackWrapper(final OneTouchPlayCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("OneTouchPlayCallback cannot be null."); + } return new IHdmiControlCallback.Stub() { @Override public void onComplete(int result) { @@ -131,6 +134,9 @@ public final class HdmiPlaybackClient extends HdmiClient { } private IHdmiControlCallback getCallbackWrapper(final DisplayStatusCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("DisplayStatusCallback cannot be null."); + } return new IHdmiControlCallback.Stub() { @Override public void onComplete(int status) { diff --git a/core/java/android/hardware/radio/OWNERS b/core/java/android/hardware/radio/OWNERS index ea4421eae96a..d2bdd643b0a2 100644 --- a/core/java/android/hardware/radio/OWNERS +++ b/core/java/android/hardware/radio/OWNERS @@ -1,2 +1,3 @@ -twasilczyk@google.com -randolphs@google.com +xuweilin@google.com +oscarazu@google.com +keunyoung@google.com diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java index 3a042a5dee4d..e8e4fc988937 100644 --- a/core/java/android/hardware/radio/ProgramList.java +++ b/core/java/android/hardware/radio/ProgramList.java @@ -26,7 +26,6 @@ import android.os.Parcelable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -173,38 +172,63 @@ public final class ProgramList implements AutoCloseable { } } - void apply(@NonNull Chunk chunk) { + void apply(Chunk chunk) { + List<ProgramSelector.Identifier> removedList = new ArrayList<>(); + List<ProgramSelector.Identifier> changedList = new ArrayList<>(); + List<ProgramList.ListCallback> listCallbacksCopied; + List<OnCompleteListener> onCompleteListenersCopied = new ArrayList<>(); synchronized (mLock) { if (mIsClosed) return; mIsComplete = false; + listCallbacksCopied = new ArrayList<>(mListCallbacks); if (chunk.isPurge()) { - new HashSet<>(mPrograms.keySet()).stream().forEach(id -> removeLocked(id)); + for (ProgramSelector.Identifier id : mPrograms.keySet()) { + removeLocked(id, removedList); + } } - chunk.getRemoved().stream().forEach(id -> removeLocked(id)); - chunk.getModified().stream().forEach(info -> putLocked(info)); + chunk.getRemoved().stream().forEach(id -> removeLocked(id, removedList)); + chunk.getModified().stream().forEach(info -> putLocked(info, changedList)); if (chunk.isComplete()) { mIsComplete = true; - mOnCompleteListeners.forEach(cb -> cb.onComplete()); + onCompleteListenersCopied = new ArrayList<>(mOnCompleteListeners); + } + } + + for (int i = 0; i < removedList.size(); i++) { + for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) { + listCallbacksCopied.get(cbIndex).onItemRemoved(removedList.get(i)); + } + } + for (int i = 0; i < changedList.size(); i++) { + for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) { + listCallbacksCopied.get(cbIndex).onItemChanged(changedList.get(i)); + } + } + if (chunk.isComplete()) { + for (int cbIndex = 0; cbIndex < onCompleteListenersCopied.size(); cbIndex++) { + onCompleteListenersCopied.get(cbIndex).onComplete(); } } } - private void putLocked(@NonNull RadioManager.ProgramInfo value) { + private void putLocked(RadioManager.ProgramInfo value, + List<ProgramSelector.Identifier> changedIdentifierList) { ProgramSelector.Identifier key = value.getSelector().getPrimaryId(); mPrograms.put(Objects.requireNonNull(key), value); ProgramSelector.Identifier sel = value.getSelector().getPrimaryId(); - mListCallbacks.forEach(cb -> cb.onItemChanged(sel)); + changedIdentifierList.add(sel); } - private void removeLocked(@NonNull ProgramSelector.Identifier key) { + private void removeLocked(ProgramSelector.Identifier key, + List<ProgramSelector.Identifier> removedIdentifierList) { RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key)); if (removed == null) return; ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId(); - mListCallbacks.forEach(cb -> cb.onItemRemoved(sel)); + removedIdentifierList.add(sel); } /** diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index b47e92d09989..8f241722a445 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -195,18 +195,13 @@ class IInputMethodWrapper extends IInputMethod.Stub case DO_START_INPUT: { final SomeArgs args = (SomeArgs) msg.obj; final IBinder startInputToken = (IBinder) args.arg1; - final IRemoteInputConnection remoteIc = (IRemoteInputConnection) args.arg2; + final InputConnection ic = (InputConnection) args.arg2; final EditorInfo info = (EditorInfo) args.arg3; final ImeOnBackInvokedDispatcher imeDispatcher = (ImeOnBackInvokedDispatcher) args.arg4; - final CancellationGroup cancellationGroup = (CancellationGroup) args.arg5; final boolean restarting = args.argi1 == 1; @InputMethodNavButtonFlags final int navButtonFlags = args.argi2; - final InputConnection ic = remoteIc != null - ? new RemoteInputConnection(mTarget, remoteIc, cancellationGroup) - : null; - info.makeCompatible(mTargetSdkVersion); inputMethod.dispatchStartInputWithToken(ic, info, restarting, startInputToken, navButtonFlags, imeDispatcher); args.recycle(); @@ -358,14 +353,19 @@ class IInputMethodWrapper extends IInputMethod.Stub Log.e(TAG, "startInput must be called after bindInput."); mCancellationGroup = new CancellationGroup(); } + + editorInfo.makeCompatible(mTargetSdkVersion); + + final InputConnection ic = inputConnection == null ? null + : new RemoteInputConnection(mTarget, inputConnection, mCancellationGroup); + final SomeArgs args = SomeArgs.obtain(); args.arg1 = startInputToken; - args.arg2 = inputConnection; + args.arg2 = ic; args.arg3 = editorInfo; args.argi1 = restarting ? 1 : 0; args.argi2 = navButtonFlags; args.arg4 = imeDispatcher; - args.arg5 = mCancellationGroup; mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_START_INPUT, args)); } diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 13ca2c34b27e..5123a9f45176 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -183,6 +183,9 @@ public final class PowerManager { /** * Wake lock flag: Turn the screen on when the wake lock is acquired. * <p> + * This flag requires {@link android.Manifest.permission#TURN_SCREEN_ON} for apps targeting + * Android version {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and higher. + * </p><p> * Normally wake locks don't actually wake the device, they just cause the screen to remain on * once it's already on. This flag will cause the device to wake up when the wake lock is * acquired. @@ -195,10 +198,10 @@ public final class PowerManager { * * @deprecated Most applications should use {@link android.R.attr#turnScreenOn} or * {@link android.app.Activity#setTurnScreenOn(boolean)} instead, as this prevents the previous - * foreground app from being resumed first when the screen turns on. Note that this flag may - * require a permission in the future. + * foreground app from being resumed first when the screen turns on. */ @Deprecated + @RequiresPermission(value = android.Manifest.permission.TURN_SCREEN_ON, conditional = true) public static final int ACQUIRE_CAUSES_WAKEUP = 0x10000000; /** diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING index e02c45ddf5a8..3a5666283ae2 100644 --- a/core/java/android/os/TEST_MAPPING +++ b/core/java/android/os/TEST_MAPPING @@ -58,6 +58,18 @@ }, { "file_patterns": [ + "Parcel\\.java", + "[^/]*Bundle[^/]*\\.java" + ], + "name": "FrameworksMockingCoreTests", + "options": [ + { "include-filter": "android.os.BundleRecyclingTest"}, + { "exclude-annotation": "androidx.test.filters.FlakyTest" }, + { "exclude-annotation": "org.junit.Ignore" } + ] + }, + { + "file_patterns": [ "BatteryUsageStats[^/]*\\.java", "PowerComponents\\.java", "[^/]*BatteryConsumer[^/]*\\.java" diff --git a/core/java/android/os/VibrationAttributes.java b/core/java/android/os/VibrationAttributes.java index 5bed32cb0438..06930bb32090 100644 --- a/core/java/android/os/VibrationAttributes.java +++ b/core/java/android/os/VibrationAttributes.java @@ -152,6 +152,9 @@ public final class VibrationAttributes implements Parcelable { /** * Flag requesting vibration effect to be played even under limited interruptions. + * + * <p>Only privileged apps can ignore user settings that limit interruptions, and this + * flag will be ignored otherwise. */ public static final int FLAG_BYPASS_INTERRUPTION_POLICY = 0x1; diff --git a/core/java/android/service/voice/VoiceInteractionSession.java b/core/java/android/service/voice/VoiceInteractionSession.java index b783f6b8fd51..7d17093b2707 100644 --- a/core/java/android/service/voice/VoiceInteractionSession.java +++ b/core/java/android/service/voice/VoiceInteractionSession.java @@ -16,6 +16,7 @@ package android.service.voice; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import android.annotation.CallbackExecutor; @@ -38,6 +39,7 @@ import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.Region; +import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Bundle; import android.os.CancellationSignal; @@ -1043,15 +1045,33 @@ public class VoiceInteractionSession implements KeyEvent.Callback, ComponentCall } public VoiceInteractionSession(Context context, Handler handler) { - mContext = context; mHandlerCaller = new HandlerCaller(context, handler.getLooper(), mCallbacks, true); + mContext = createWindowContextIfNeeded(context); } public Context getContext() { return mContext; } + private Context createWindowContextIfNeeded(Context context) { + try { + if (!context.isUiContext()) { + DisplayManager displayManager = context.getSystemService(DisplayManager.class); + if (displayManager != null) { + return context.createWindowContext( + displayManager.getDisplay(DEFAULT_DISPLAY), + WindowManager.LayoutParams.TYPE_VOICE_INTERACTION, + /* options= */ null); + } + } + return context; + } catch (RuntimeException e) { + Log.w(TAG, "Fail to createWindowContext, Exception = " + e); + return context; + } + } + void addRequest(Request req) { synchronized (this) { mActiveRequests.put(req.mInterface.asBinder(), req); diff --git a/core/java/android/view/IRemoteAnimationRunner.aidl b/core/java/android/view/IRemoteAnimationRunner.aidl index 1f64fb8ca2ec..1981c9d66c8b 100644 --- a/core/java/android/view/IRemoteAnimationRunner.aidl +++ b/core/java/android/view/IRemoteAnimationRunner.aidl @@ -46,5 +46,5 @@ oneway interface IRemoteAnimationRunner { * won't have any effect anymore. */ @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) - void onAnimationCancelled(); + void onAnimationCancelled(boolean isKeyguardOccluded); } diff --git a/core/java/android/view/TEST_MAPPING b/core/java/android/view/TEST_MAPPING index 50d69f78688a..ecb98f9ce801 100644 --- a/core/java/android/view/TEST_MAPPING +++ b/core/java/android/view/TEST_MAPPING @@ -10,6 +10,9 @@ "include-annotation": "android.platform.test.annotations.Presubmit" }, { + "exclude-annotation": "android.platform.test.annotations.LargeTest" + }, + { "exclude-annotation": "androidx.test.filters.FlakyTest" }, { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index bc7da13b66db..657c0b7801b5 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -805,6 +805,7 @@ public final class ViewRootImpl implements ViewParent, private final ViewRootRectTracker mUnrestrictedKeepClearRectsTracker = new ViewRootRectTracker(v -> v.collectUnrestrictedPreferKeepClearRects()); private boolean mHasPendingKeepClearAreaChange; + private Rect mKeepClearAccessibilityFocusRect; private IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection; @@ -4865,13 +4866,27 @@ public final class ViewRootImpl implements ViewParent, mHandler.sendEmptyMessage(MSG_KEEP_CLEAR_RECTS_CHANGED); } - void keepClearRectsChanged() { + private void updateKeepClearForAccessibilityFocusRect() { + if (mViewConfiguration.isPreferKeepClearForFocusEnabled()) { + if (mKeepClearAccessibilityFocusRect == null) { + mKeepClearAccessibilityFocusRect = new Rect(); + } + boolean hasAccessibilityFocus = + getAccessibilityFocusedRect(mKeepClearAccessibilityFocusRect); + if (!hasAccessibilityFocus) { + mKeepClearAccessibilityFocusRect.setEmpty(); + } + mHandler.obtainMessage(MSG_KEEP_CLEAR_RECTS_CHANGED, 1, 0).sendToTarget(); + } + } + + void keepClearRectsChanged(boolean accessibilityFocusRectChanged) { boolean restrictedKeepClearRectsChanged = mKeepClearRectsTracker.computeChanges(); boolean unrestrictedKeepClearRectsChanged = mUnrestrictedKeepClearRectsTracker.computeChanges(); - if ((restrictedKeepClearRectsChanged || unrestrictedKeepClearRectsChanged) - && mView != null) { + if ((restrictedKeepClearRectsChanged || unrestrictedKeepClearRectsChanged + || accessibilityFocusRectChanged) && mView != null) { mHasPendingKeepClearAreaChange = true; // Only report keep clear areas immediately if they have not been reported recently if (!mHandler.hasMessages(MSG_REPORT_KEEP_CLEAR_RECTS)) { @@ -4888,10 +4903,16 @@ public final class ViewRootImpl implements ViewParent, } mHasPendingKeepClearAreaChange = false; - final List<Rect> restrictedKeepClearRects = mKeepClearRectsTracker.getLastComputedRects(); + List<Rect> restrictedKeepClearRects = mKeepClearRectsTracker.getLastComputedRects(); final List<Rect> unrestrictedKeepClearRects = mUnrestrictedKeepClearRectsTracker.getLastComputedRects(); + if (mKeepClearAccessibilityFocusRect != null + && !mKeepClearAccessibilityFocusRect.isEmpty()) { + restrictedKeepClearRects = new ArrayList<>(restrictedKeepClearRects); + restrictedKeepClearRects.add(mKeepClearAccessibilityFocusRect); + } + try { mWindowSession.reportKeepClearAreasChanged(mWindow, restrictedKeepClearRects, unrestrictedKeepClearRects); @@ -5091,6 +5112,7 @@ public final class ViewRootImpl implements ViewParent, // Set the new focus host and node. mAccessibilityFocusedHost = view; mAccessibilityFocusedVirtualView = node; + updateKeepClearForAccessibilityFocusRect(); if (mAttachInfo.mThreadedRenderer != null) { mAttachInfo.mThreadedRenderer.invalidateRoot(); @@ -5679,7 +5701,7 @@ public final class ViewRootImpl implements ViewParent, systemGestureExclusionChanged(); } break; case MSG_KEEP_CLEAR_RECTS_CHANGED: { - keepClearRectsChanged(); + keepClearRectsChanged(/* accessibilityFocusRectChanged= */ msg.arg1 == 1); } break; case MSG_REPORT_KEEP_CLEAR_RECTS: { reportKeepClearAreasChanged(); diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 50f3e0cfbcf3..a9fe34a9c0ff 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1421,14 +1421,38 @@ public final class InputMethodManager { } /** - * Returns {@code true} if currently selected IME supports Stylus handwriting. + * Returns {@code true} if currently selected IME supports Stylus handwriting & is enabled. * If the method returns {@code false}, {@link #startStylusHandwriting(View)} shouldn't be * called and Stylus touch should continue as normal touch input. * @see #startStylusHandwriting(View) */ public boolean isStylusHandwritingAvailable() { + return isStylusHandwritingAvailableAsUser(UserHandle.myUserId()); + } + + /** + * Returns {@code true} if currently selected IME supports Stylus handwriting & is enabled for + * the given userId. + * If the method returns {@code false}, {@link #startStylusHandwriting(View)} shouldn't be + * called and Stylus touch should continue as normal touch input. + * @see #startStylusHandwriting(View) + * @param userId user ID to query. + * @hide + */ + public boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) { + final Context fallbackContext = ActivityThread.currentApplication(); + if (fallbackContext == null) { + return false; + } + if (Settings.Global.getInt(fallbackContext.getContentResolver(), + Settings.Global.STYLUS_HANDWRITING_ENABLED, 0) == 0) { + if (DEBUG) { + Log.d(TAG, "Stylus handwriting is not enabled in settings."); + } + return false; + } try { - return mService.isStylusHandwritingAvailable(); + return mService.isStylusHandwritingAvailableAsUser(userId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1645,6 +1669,7 @@ public final class InputMethodManager { mCurId = null; mCurMethod = null; // for @UnsupportedAppUsage mCurrentInputMethodSession = null; + mIsInputMethodSuppressingSpellChecker = false; } /** @@ -1707,7 +1732,6 @@ public final class InputMethodManager { @GuardedBy("mH") void finishInputLocked() { mVirtualDisplayToScreenMatrix = null; - mIsInputMethodSuppressingSpellChecker = false; setNextServedViewLocked(null); if (getServedViewLocked() != null) { if (DEBUG) { diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 184e7bca963b..1cb96b15b8ef 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -242,6 +242,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public static final int CHOICE_MODE_MULTIPLE_MODAL = 3; /** + * When flinging the stretch towards scrolling content, it should destretch quicker than the + * fling would normally do. The visual effect of flinging the stretch looks strange as little + * appears to happen at first and then when the stretch disappears, the content starts + * scrolling quickly. + */ + private static final float FLING_DESTRETCH_FACTOR = 4f; + + /** * The thread that created this view. */ private final Thread mOwnerThread; @@ -4216,9 +4224,23 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // fling further. boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity; if (flingVelocity && !mEdgeGlowTop.isFinished()) { - mEdgeGlowTop.onAbsorb(initialVelocity); + if (shouldAbsorb(mEdgeGlowTop, initialVelocity)) { + mEdgeGlowTop.onAbsorb(initialVelocity); + } else { + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + mFlingRunnable.start(-initialVelocity); + } } else if (flingVelocity && !mEdgeGlowBottom.isFinished()) { - mEdgeGlowBottom.onAbsorb(-initialVelocity); + if (shouldAbsorb(mEdgeGlowBottom, -initialVelocity)) { + mEdgeGlowBottom.onAbsorb(-initialVelocity); + } else { + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + mFlingRunnable.start(-initialVelocity); + } } else if (flingVelocity && !((mFirstPosition == 0 && firstChildTop == contentTop - mOverscrollDistance) @@ -4301,6 +4323,60 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } + /** + * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should + * animate with a fling. It will animate with a fling if the velocity will remove the + * EdgeEffect through its normal operation. + * + * @param edgeEffect The EdgeEffect that might absorb the velocity. + * @param velocity The velocity of the fling motion + * @return true if the velocity should be absorbed or false if it should be flung. + */ + private boolean shouldAbsorb(EdgeEffect edgeEffect, int velocity) { + if (velocity > 0) { + return true; + } + float distance = edgeEffect.getDistance() * getHeight(); + + // This is flinging without the spring, so let's see if it will fling past the overscroll + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + float flingDistance = mFlingRunnable.getSplineFlingDistance(-velocity); + + return flingDistance < distance; + } + + /** + * Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for + * consuming deltas from EdgeEffects + * @param unconsumed The unconsumed delta that the EdgeEffets may consume + * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume. + */ + private int consumeFlingInStretch(int unconsumed) { + if (unconsumed < 0 && mEdgeGlowTop != null && mEdgeGlowTop.getDistance() != 0f) { + int size = getHeight(); + float deltaDistance = unconsumed * FLING_DESTRETCH_FACTOR / size; + int consumed = Math.round(size / FLING_DESTRETCH_FACTOR + * mEdgeGlowTop.onPullDistance(deltaDistance, 0.5f)); + if (consumed != unconsumed) { + mEdgeGlowTop.finish(); + } + return unconsumed - consumed; + } + if (unconsumed > 0 && mEdgeGlowBottom != null && mEdgeGlowBottom.getDistance() != 0f) { + int size = getHeight(); + float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size; + int consumed = Math.round(-size / FLING_DESTRETCH_FACTOR + * mEdgeGlowBottom.onPullDistance(deltaDistance, 0.5f)); + if (consumed != unconsumed) { + mEdgeGlowBottom.finish(); + } + return unconsumed - consumed; + } + return unconsumed; + } + private boolean shouldDisplayEdgeEffects() { return getOverScrollMode() != OVER_SCROLL_NEVER; } @@ -4783,6 +4859,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mScroller = new OverScroller(getContext()); } + float getSplineFlingDistance(int velocity) { + return (float) mScroller.getSplineFlingDistance(velocity); + } + // Use AbsListView#fling(int) instead @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) void start(int initialVelocity) { @@ -4905,6 +4985,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } if (mItemCount == 0 || getChildCount() == 0) { + mEdgeGlowBottom.onRelease(); + mEdgeGlowTop.onRelease(); endFling(); return; } @@ -4915,7 +4997,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // Flip sign to convert finger direction to list items direction // (e.g. finger moving down means list is moving towards the top) - int delta = mLastFlingY - y; + int delta = consumeFlingInStretch(mLastFlingY - y); // Pretend that each frame of a fling scroll is a touch scroll if (delta > 0) { diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 2dbfd7e5b2e2..1d6778b8a4a9 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -77,6 +77,14 @@ public class HorizontalScrollView extends FrameLayout { private static final String TAG = "HorizontalScrollView"; + /** + * When flinging the stretch towards scrolling content, it should destretch quicker than the + * fling would normally do. The visual effect of flinging the stretch looks strange as little + * appears to happen at first and then when the stretch disappears, the content starts + * scrolling quickly. + */ + private static final float FLING_DESTRETCH_FACTOR = 4f; + private long mLastScroll; private final Rect mTempRect = new Rect(); @@ -1456,18 +1464,19 @@ public class HorizontalScrollView extends FrameLayout { int oldY = mScrollY; int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); + int deltaX = consumeFlingInStretch(x - oldX); - if (oldX != x || oldY != y) { + if (deltaX != 0 || oldY != y) { final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); - overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0, + overScrollBy(deltaX, y - oldY, oldX, oldY, range, 0, mOverflingDistance, 0, false); onScrollChanged(mScrollX, mScrollY, oldX, oldY); - if (canOverscroll) { + if (canOverscroll && deltaX != 0) { if (x < 0 && oldX >= 0) { mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); } else if (x > range && oldX <= range) { @@ -1483,6 +1492,36 @@ public class HorizontalScrollView extends FrameLayout { } /** + * Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for + * consuming deltas from EdgeEffects + * @param unconsumed The unconsumed delta that the EdgeEffets may consume + * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume. + */ + private int consumeFlingInStretch(int unconsumed) { + if (unconsumed > 0 && mEdgeGlowLeft != null && mEdgeGlowLeft.getDistance() != 0f) { + int size = getWidth(); + float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size; + int consumed = Math.round(-size / FLING_DESTRETCH_FACTOR + * mEdgeGlowLeft.onPullDistance(deltaDistance, 0.5f)); + if (consumed != unconsumed) { + mEdgeGlowLeft.finish(); + } + return unconsumed - consumed; + } + if (unconsumed < 0 && mEdgeGlowRight != null && mEdgeGlowRight.getDistance() != 0f) { + int size = getWidth(); + float deltaDistance = unconsumed * FLING_DESTRETCH_FACTOR / size; + int consumed = Math.round(size / FLING_DESTRETCH_FACTOR + * mEdgeGlowRight.onPullDistance(deltaDistance, 0.5f)); + if (consumed != unconsumed) { + mEdgeGlowRight.finish(); + } + return unconsumed - consumed; + } + return unconsumed; + } + + /** * Scrolls the view to the given child. * * @param child the View to scroll to @@ -1746,11 +1785,23 @@ public class HorizontalScrollView extends FrameLayout { int maxScroll = Math.max(0, right - width); + boolean shouldFling = false; if (mScrollX == 0 && !mEdgeGlowLeft.isFinished()) { - mEdgeGlowLeft.onAbsorb(-velocityX); + if (shouldAbsorb(mEdgeGlowLeft, -velocityX)) { + mEdgeGlowLeft.onAbsorb(-velocityX); + } else { + shouldFling = true; + } } else if (mScrollX == maxScroll && !mEdgeGlowRight.isFinished()) { - mEdgeGlowRight.onAbsorb(velocityX); + if (shouldAbsorb(mEdgeGlowRight, velocityX)) { + mEdgeGlowRight.onAbsorb(velocityX); + } else { + shouldFling = true; + } } else { + shouldFling = true; + } + if (shouldFling) { mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0, maxScroll, 0, 0, width / 2, 0); @@ -1774,6 +1825,27 @@ public class HorizontalScrollView extends FrameLayout { } /** + * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should + * animate with a fling. It will animate with a fling if the velocity will remove the + * EdgeEffect through its normal operation. + * + * @param edgeEffect The EdgeEffect that might absorb the velocity. + * @param velocity The velocity of the fling motion + * @return true if the velocity should be absorbed or false if it should be flung. + */ + private boolean shouldAbsorb(EdgeEffect edgeEffect, int velocity) { + if (velocity > 0) { + return true; + } + float distance = edgeEffect.getDistance() * getWidth(); + + // This is flinging without the spring, so let's see if it will fling past the overscroll + float flingDistance = (float) mScroller.getSplineFlingDistance(-velocity); + + return flingDistance < distance; + } + + /** * {@inheritDoc} * * <p>This version also clamps the scrolling to the bounds of our child. diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java index 1683878cd8b2..d00fc1c89d8f 100644 --- a/core/java/android/widget/OverScroller.java +++ b/core/java/android/widget/OverScroller.java @@ -527,6 +527,10 @@ public class OverScroller { Math.signum(yvel) == Math.signum(dy); } + double getSplineFlingDistance(int velocity) { + return mScrollerY.getSplineFlingDistance(velocity); + } + static class SplineOverScroller { // Initial position private int mStart; diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 2acd50c9e169..84b6f65ffea6 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -85,6 +85,14 @@ public class ScrollView extends FrameLayout { private static final String TAG = "ScrollView"; + /** + * When flinging the stretch towards scrolling content, it should destretch quicker than the + * fling would normally do. The visual effect of flinging the stretch looks strange as little + * appears to happen at first and then when the stretch disappears, the content starts + * scrolling quickly. + */ + private static final float FLING_DESTRETCH_FACTOR = 4f; + @UnsupportedAppUsage private long mLastScroll; @@ -1488,18 +1496,19 @@ public class ScrollView extends FrameLayout { int oldY = mScrollY; int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); + int deltaY = consumeFlingInStretch(y - oldY); - if (oldX != x || oldY != y) { + if (oldX != x || deltaY != 0) { final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); - overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, + overScrollBy(x - oldX, deltaY, oldX, oldY, 0, range, 0, mOverflingDistance, false); onScrollChanged(mScrollX, mScrollY, oldX, oldY); - if (canOverscroll) { + if (canOverscroll && deltaY != 0) { if (y < 0 && oldY >= 0) { mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); } else if (y > range && oldY <= range) { @@ -1521,6 +1530,36 @@ public class ScrollView extends FrameLayout { } /** + * Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for + * consuming deltas from EdgeEffects + * @param unconsumed The unconsumed delta that the EdgeEffets may consume + * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume. + */ + private int consumeFlingInStretch(int unconsumed) { + if (unconsumed > 0 && mEdgeGlowTop != null && mEdgeGlowTop.getDistance() != 0f) { + int size = getHeight(); + float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size; + int consumed = Math.round(-size / FLING_DESTRETCH_FACTOR + * mEdgeGlowTop.onPullDistance(deltaDistance, 0.5f)); + if (consumed != unconsumed) { + mEdgeGlowTop.finish(); + } + return unconsumed - consumed; + } + if (unconsumed < 0 && mEdgeGlowBottom != null && mEdgeGlowBottom.getDistance() != 0f) { + int size = getHeight(); + float deltaDistance = unconsumed * FLING_DESTRETCH_FACTOR / size; + int consumed = Math.round(size / FLING_DESTRETCH_FACTOR + * mEdgeGlowBottom.onPullDistance(deltaDistance, 0.5f)); + if (consumed != unconsumed) { + mEdgeGlowBottom.finish(); + } + return unconsumed - consumed; + } + return unconsumed; + } + + /** * Scrolls the view to the given child. * * @param child the View to scroll to @@ -1803,14 +1842,43 @@ public class ScrollView extends FrameLayout { fling(velocityY); } else if (!consumed) { if (!mEdgeGlowTop.isFinished()) { - mEdgeGlowTop.onAbsorb(-velocityY); + if (shouldAbsorb(mEdgeGlowTop, -velocityY)) { + mEdgeGlowTop.onAbsorb(-velocityY); + } else { + fling(velocityY); + } } else if (!mEdgeGlowBottom.isFinished()) { - mEdgeGlowBottom.onAbsorb(velocityY); + if (shouldAbsorb(mEdgeGlowBottom, velocityY)) { + mEdgeGlowBottom.onAbsorb(velocityY); + } else { + fling(velocityY); + } } } } } + /** + * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should + * animate with a fling. It will animate with a fling if the velocity will remove the + * EdgeEffect through its normal operation. + * + * @param edgeEffect The EdgeEffect that might absorb the velocity. + * @param velocity The velocity of the fling motion + * @return true if the velocity should be absorbed or false if it should be flung. + */ + private boolean shouldAbsorb(EdgeEffect edgeEffect, int velocity) { + if (velocity > 0) { + return true; + } + float distance = edgeEffect.getDistance() * getHeight(); + + // This is flinging without the spring, so let's see if it will fling past the overscroll + float flingDistance = (float) mScroller.getSplineFlingDistance(-velocity); + + return flingDistance < distance; + } + @UnsupportedAppUsage private void endDrag() { mIsBeingDragged = false; diff --git a/core/java/android/window/SizeConfigurationBuckets.java b/core/java/android/window/SizeConfigurationBuckets.java index f474f0a76cc6..998bec0892ae 100644 --- a/core/java/android/window/SizeConfigurationBuckets.java +++ b/core/java/android/window/SizeConfigurationBuckets.java @@ -104,24 +104,15 @@ public final class SizeConfigurationBuckets implements Parcelable { /** * Get the changes between two configurations but don't count changes in sizes if they don't * cross boundaries that are important to the app. - * - * This is a static helper to deal with null `buckets`. When no buckets have been specified, - * this actually filters out all 3 size-configs. This is legacy behavior. */ public static int filterDiff(int diff, @NonNull Configuration oldConfig, @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) { - final boolean nonSizeLayoutFieldsUnchanged = - areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout); if (buckets == null) { - // Only unflip CONFIG_SCREEN_LAYOUT if non-size-related attributes of screen layout do - // not change. - if (nonSizeLayoutFieldsUnchanged) { - return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE - | CONFIG_SCREEN_LAYOUT); - } else { - return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE); - } + return diff; } + + final boolean nonSizeLayoutFieldsUnchanged = + areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout); if ((diff & CONFIG_SCREEN_SIZE) != 0) { final boolean crosses = buckets.crossesHorizontalSizeThreshold(oldConfig.screenWidthDp, newConfig.screenWidthDp) diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index c2b69717bc37..3732ea5abaa5 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -68,13 +68,6 @@ interface IBatteryStats { @EnforcePermission("BATTERY_STATS") List<BatteryUsageStats> getBatteryUsageStats(in List<BatteryUsageStatsQuery> queries); - @UnsupportedAppUsage - @EnforcePermission("BATTERY_STATS") - byte[] getStatistics(); - - @EnforcePermission("BATTERY_STATS") - ParcelFileDescriptor getStatisticsStream(boolean updateAll); - // Return true if we see the battery as currently charging. @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) @RequiresNoPermission diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index cbaac5f33eb4..44997b4a9c30 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -548,6 +548,12 @@ public final class SystemUiDeviceConfigFlags { */ public static final String TASK_MANAGER_SHOW_FOOTER_DOT = "task_manager_show_footer_dot"; + /** + * (boolean) Whether the task manager should show a stop button if the app is allowlisted + * by the user. + */ + public static final String TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = + "show_stop_button_for_user_allowlisted_apps"; /** * (boolean) Whether the clipboard overlay is enabled. diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 52fd7fec932e..22340c6b0c55 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -75,6 +75,8 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_LOADING_TO_SHOW_INFO_WITH_ACTIONS; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_SHOW_FUNCTION_SCREEN_WITH_ACTIONS; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TAKE_SCREENSHOT; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_COLLAPSE; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_EXPAND; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__UNFOLD_ANIM; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_DIALOG_OPEN; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH; @@ -206,6 +208,8 @@ public class InteractionJankMonitor { public static final int CUJ_SETTINGS_TOGGLE = 57; public static final int CUJ_SHADE_DIALOG_OPEN = 58; public static final int CUJ_USER_DIALOG_OPEN = 59; + public static final int CUJ_TASKBAR_EXPAND = 60; + public static final int CUJ_TASKBAR_COLLAPSE = 61; private static final int NO_STATSD_LOGGING = -1; @@ -274,6 +278,8 @@ public class InteractionJankMonitor { UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_TOGGLE, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_DIALOG_OPEN, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_DIALOG_OPEN, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_EXPAND, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_COLLAPSE, }; private static volatile InteractionJankMonitor sInstance; @@ -354,6 +360,8 @@ public class InteractionJankMonitor { CUJ_SETTINGS_TOGGLE, CUJ_SHADE_DIALOG_OPEN, CUJ_USER_DIALOG_OPEN, + CUJ_TASKBAR_EXPAND, + CUJ_TASKBAR_COLLAPSE }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -792,6 +800,10 @@ public class InteractionJankMonitor { return "SHADE_DIALOG_OPEN"; case CUJ_USER_DIALOG_OPEN: return "USER_DIALOG_OPEN"; + case CUJ_TASKBAR_EXPAND: + return "TASKBAR_EXPAND"; + case CUJ_TASKBAR_COLLAPSE: + return "TASKBAR_COLLAPSE"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 508e4450d0f9..d550fef968a8 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -91,5 +91,5 @@ interface IInputMethodManager { /** Start Stylus handwriting session **/ void startStylusHandwriting(in IInputMethodClient client); /** Returns {@code true} if currently selected IME supports Stylus handwriting. */ - boolean isStylusHandwritingAvailable(); + boolean isStylusHandwritingAvailableAsUser(int userId); } diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp index 4af28ea24361..1520ea5c6831 100644 --- a/core/jni/android_graphics_BLASTBufferQueue.cpp +++ b/core/jni/android_graphics_BLASTBufferQueue.cpp @@ -41,7 +41,12 @@ struct { static JNIEnv* getenv(JavaVM* vm) { JNIEnv* env; - if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + auto result = vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6); + if (result == JNI_EDETACHED) { + if (vm->AttachCurrentThreadAsDaemon(&env, nullptr) != JNI_OK) { + LOG_ALWAYS_FATAL("Failed to AttachCurrentThread!"); + } + } else if (result != JNI_OK) { LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", vm); } return env; @@ -60,28 +65,22 @@ public: } ~TransactionHangCallbackWrapper() { - if (mTransactionHangObject) { - getenv()->DeleteGlobalRef(mTransactionHangObject); + if (mTransactionHangObject != nullptr) { + getenv(mVm)->DeleteGlobalRef(mTransactionHangObject); mTransactionHangObject = nullptr; } } void onTransactionHang(bool isGpuHang) { if (mTransactionHangObject) { - getenv()->CallVoidMethod(mTransactionHangObject, - gTransactionHangCallback.onTransactionHang, isGpuHang); + getenv(mVm)->CallVoidMethod(mTransactionHangObject, + gTransactionHangCallback.onTransactionHang, isGpuHang); } } private: JavaVM* mVm; jobject mTransactionHangObject; - - JNIEnv* getenv() { - JNIEnv* env; - mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6); - return env; - } }; static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring jName, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d7aa174ddfef..c2fcd1d0612a 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1702,7 +1702,7 @@ android:description="@string/permdesc_useBiometric" android:protectionLevel="normal" /> - <!-- ======================================================================= --> + <!-- ====================================================================== --> <!-- Permissions for posting notifications --> <!-- ====================================================================== --> <eat-comment /> @@ -2468,6 +2468,15 @@ android:description="@string/permdesc_transmitIr" android:protectionLevel="normal" /> + <!-- Allows an app to turn on the screen on, e.g. with + {@link android.os.PowerManager#ACQUIRE_CAUSES_WAKEUP}. + <p>Intended to only be used by home automation apps. + --> + <permission android:name="android.permission.TURN_SCREEN_ON" + android:label="@string/permlab_turnScreenOn" + android:description="@string/permdesc_turnScreenOn" + android:protectionLevel="normal|appop" /> + <!-- ==================================================== --> <!-- Permissions related to changing audio settings --> <!-- ==================================================== --> @@ -4858,11 +4867,10 @@ <permission android:name="android.permission.DISABLE_INPUT_DEVICE" android:protectionLevel="signature" /> - <!-- Allows an application to configure and connect to Wifi displays - @hide - @SystemApi --> + <!-- Allows an application to configure and connect to Wifi displays --> <permission android:name="android.permission.CONFIGURE_WIFI_DISPLAY" - android:protectionLevel="signature" /> + android:protectionLevel="signature|knownSigner" + android:knownCerts="@array/wifi_known_signers" /> <!-- Allows an application to control low-level features of Wifi displays such as opening an RTSP socket. This permission should only be used diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 5ddfce0846ed..aef71e435bc6 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1619,6 +1619,11 @@ <string name="permdesc_postNotification">Allows the app to show notifications</string> <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR_LIMIT=NONE] --> + <string name="permlab_turnScreenOn">turn on the screen</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR_LIMIT=NONE] --> + <string name="permdesc_turnScreenOn">Allows the app to turn on the screen.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR_LIMIT=NONE] --> <string name="permlab_useBiometric">use biometric hardware</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this.[CHAR_LIMIT=NONE] --> <string name="permdesc_useBiometric">Allows the app to use biometric hardware for authentication</string> diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 08135e82e7bd..df3ae0e37547 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -61,7 +61,7 @@ <shortcode country="bg" pattern="\\d{4,5}" premium="18(?:16|423)|19(?:1[56]|35)" free="116\\d{3}|1988|1490" /> <!-- Bahrain: 1-5 digits (standard system default, not country specific) --> - <shortcode country="bh" pattern="\\d{1,5}" free="81181" /> + <shortcode country="bh" pattern="\\d{1,5}" free="81181|85999" /> <!-- Brazil: 1-5 digits (standard system default, not country specific) --> <shortcode country="br" pattern="\\d{1,5}" free="6000[012]\\d|876|5500|9963|4141|8000" /> @@ -83,7 +83,7 @@ <shortcode country="cn" premium="1066.*" free="1065.*" /> <!-- Colombia: 1-6 digits (not confirmed) --> - <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739" /> + <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517" /> <!-- Cyprus: 4-6 digits (not confirmed), known premium codes listed, plus EU --> <shortcode country="cy" pattern="\\d{4,6}" premium="7510" free="116\\d{3}" /> @@ -190,7 +190,7 @@ <shortcode country="mk" pattern="\\d{1,6}" free="129005|122" /> <!-- Mexico: 4-5 digits (not confirmed), known premium codes listed --> - <shortcode country="mx" pattern="\\d{4,5}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963" /> + <shortcode country="mx" pattern="\\d{4,5}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963|91101" /> <!-- Malaysia: 5 digits: http://www.skmm.gov.my/attachment/Consumer_Regulation/Mobile_Content_Services_FAQs.pdf --> <shortcode country="my" pattern="\\d{5}" premium="32298|33776" free="22099|28288|66668" /> diff --git a/core/tests/BroadcastRadioTests/OWNERS b/core/tests/BroadcastRadioTests/OWNERS index 3e360e7e992c..d2bdd643b0a2 100644 --- a/core/tests/BroadcastRadioTests/OWNERS +++ b/core/tests/BroadcastRadioTests/OWNERS @@ -1,3 +1,3 @@ -keunyoung@google.com +xuweilin@google.com oscarazu@google.com -twasilczyk@google.com +keunyoung@google.com diff --git a/core/tests/companiontests/src/android/companion/CompanionTestRunner.java b/core/tests/companiontests/src/android/companion/CompanionTestRunner.java index caa2c685accc..3c59e7d716b0 100644 --- a/core/tests/companiontests/src/android/companion/CompanionTestRunner.java +++ b/core/tests/companiontests/src/android/companion/CompanionTestRunner.java @@ -33,6 +33,7 @@ public class CompanionTestRunner extends InstrumentationTestRunner { public TestSuite getAllTests() { TestSuite suite = new InstrumentationTestSuite(this); suite.addTestSuite(BluetoothDeviceFilterUtilsTest.class); + suite.addTestSuite(SystemDataTransportTest.class); return suite; } diff --git a/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java b/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java new file mode 100644 index 000000000000..be04b6c91a8a --- /dev/null +++ b/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.companion; + +import android.os.SystemClock; +import android.test.InstrumentationTestCase; +import android.util.Log; + +import com.android.internal.util.HexDump; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Random; + +public class SystemDataTransportTest extends InstrumentationTestCase { + private static final String TAG = "SystemDataTransportTest"; + + private static final int COMMAND_INVALID = 0xF00DCAFE; + private static final int COMMAND_PING_V0 = 0x50490000; + private static final int COMMAND_PONG_V0 = 0x504F0000; + + private CompanionDeviceManager mCdm; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mCdm = getInstrumentation().getTargetContext() + .getSystemService(CompanionDeviceManager.class); + } + + public void testPingHandRolled() { + // NOTE: These packets are explicitly hand-rolled to verify wire format; + // the remainder of the tests are fine using generated packets + + // PING v0 with payload "HELLO WORLD!" + final byte[] input = new byte[] { + 0x50, 0x49, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, + 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x21, + }; + // PONG v0 with payload "HELLO WORLD!" + final byte[] expected = new byte[] { + 0x50, 0x4F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, + 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x21, + }; + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, in, out); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testPingTrickle() { + final byte[] input = generatePacket(COMMAND_PING_V0, TAG); + final byte[] expected = generatePacket(COMMAND_PONG_V0, TAG); + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, new TrickleInputStream(in), out); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testPingDelay() { + final byte[] input = generatePacket(COMMAND_PING_V0, TAG); + final byte[] expected = generatePacket(COMMAND_PONG_V0, TAG); + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, new DelayingInputStream(in, 1000), + new DelayingOutputStream(out, 1000)); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testPingGiant() { + final byte[] blob = new byte[500_000]; + new Random().nextBytes(blob); + + final byte[] input = generatePacket(COMMAND_PING_V0, blob); + final byte[] expected = generatePacket(COMMAND_PONG_V0, blob); + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, in, out); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testMutiplePingPing() { + final byte[] input = concat(generatePacket(COMMAND_PING_V0, "red"), + generatePacket(COMMAND_PING_V0, "green")); + final byte[] expected = concat(generatePacket(COMMAND_PONG_V0, "red"), + generatePacket(COMMAND_PONG_V0, "green")); + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, in, out); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testMultipleInvalidPing() { + final byte[] input = concat(generatePacket(COMMAND_INVALID, "red"), + generatePacket(COMMAND_PING_V0, "green")); + final byte[] expected = generatePacket(COMMAND_PONG_V0, "green"); + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, in, out); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testDoubleAttach() { + // Connect an empty connection that is stalled out + final InputStream in = new EmptyInputStream(); + final OutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, in, out); + SystemClock.sleep(1000); + + // Attach a second transport that has some packets; it should disconnect + // the first transport and start replying on the second one + testPingHandRolled(); + } + + public static byte[] concat(byte[] a, byte[] b) { + return ByteBuffer.allocate(a.length + b.length).put(a).put(b).array(); + } + + public static byte[] generatePacket(int command, String data) { + return generatePacket(command, data.getBytes(StandardCharsets.UTF_8)); + } + + public static byte[] generatePacket(int command, byte[] data) { + return ByteBuffer.allocate(data.length + 8) + .putInt(command).putInt(data.length).put(data).array(); + } + + private static byte[] waitForByteArray(ByteArrayOutputStream out, int size) { + int i = 0; + while (out.size() < size) { + SystemClock.sleep(100); + if (i++ % 10 == 0) { + Log.w(TAG, "Waiting for data..."); + } + if (i > 100) { + fail(); + } + } + return out.toByteArray(); + } + + private static class EmptyInputStream extends InputStream { + @Override + public int read() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + // Instead of hanging indefinitely, wait a bit and claim that + // nothing was read, without hitting EOF + SystemClock.sleep(100); + return 0; + } + } + + private static class DelayingInputStream extends FilterInputStream { + private final long mDelay; + + public DelayingInputStream(InputStream in, long delay) { + super(in); + mDelay = delay; + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + SystemClock.sleep(mDelay); + return super.read(b, off, len); + } + } + + private static class DelayingOutputStream extends FilterOutputStream { + private final long mDelay; + + public DelayingOutputStream(OutputStream out, long delay) { + super(out); + mDelay = delay; + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + SystemClock.sleep(mDelay); + super.write(b, off, len); + } + } + + private static class TrickleInputStream extends FilterInputStream { + public TrickleInputStream(InputStream in) { + super(in); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + return super.read(b, off, 1); + } + } +} diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteConnectionPoolTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteConnectionPoolTest.java index f1d27d4a13ab..2b663bdb7861 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteConnectionPoolTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteConnectionPoolTest.java @@ -33,6 +33,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; +import java.util.concurrent.CountDownLatch; /** * Tests for {@link SQLiteConnectionPool} @@ -74,7 +75,7 @@ public class SQLiteConnectionPoolTest { Log.i(TAG, "Starting " + thread.getName()); thread.start(); SQLiteConnectionPool pool = SQLiteConnectionPool.open(mTestConf); - pool.setupIdleConnectionHandler(thread.getLooper(), 100); + pool.setupIdleConnectionHandler(thread.getLooper(), 100, null); SQLiteConnection c1 = pool.acquireConnection("pragma user_version", 0, null); assertEquals("First connection should be returned", 0, c1.getConnectionId()); pool.releaseConnection(c1); @@ -89,4 +90,31 @@ public class SQLiteConnectionPoolTest { pool.close(); thread.quit(); } + + @Test + public void testNonprimaryConnectionPoolRecycling() throws InterruptedException { + HandlerThread thread = new HandlerThread("test-close-idle-connections-thread"); + thread.start(); + SQLiteConnectionPool pool = SQLiteConnectionPool.open(mTestConf); + CountDownLatch latch = new CountDownLatch(1); + Runnable onIdleConnectionTimeout = () -> latch.countDown(); + pool.setupIdleConnectionHandler(thread.getLooper(), 1, onIdleConnectionTimeout); + + assertTrue("When the pool was just opened there should only be a primary connection.", + !pool.hasAnyAvailableNonPrimaryConnection()); + SQLiteConnection connection = pool.acquireConnection("pragma user_version", 0, null); + pool.releaseConnection(connection); + assertTrue("First time acquire should will return the primary connection.", + !pool.hasAnyAvailableNonPrimaryConnection()); + + // Wait for primary connection to time out + latch.await(); + + // Now that the primary is closed, acquiring again should open a non primary connection + connection = pool.acquireConnection("pragma user_version", 0, null); + pool.releaseConnection(connection); + assertTrue("There should be an available non primary connection in the pool.", + pool.hasAnyAvailableNonPrimaryConnection()); + pool.close(); + } } diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index 8d3ee2a15dce..18da1a4ab13e 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -16,6 +16,7 @@ package android.view.stylus; +import static android.provider.Settings.Global.STYLUS_HANDWRITING_ENABLED; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; @@ -32,6 +33,7 @@ import android.app.Instrumentation; import android.content.Context; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.provider.Settings; import android.view.HandwritingInitiator; import android.view.InputDevice; import android.view.MotionEvent; @@ -67,6 +69,8 @@ public class HandwritingInitiatorTest { private static final int HW_BOUNDS_OFFSETS_TOP_PX = 20; private static final int HW_BOUNDS_OFFSETS_RIGHT_PX = 30; private static final int HW_BOUNDS_OFFSETS_BOTTOM_PX = 40; + private static final int SETTING_VALUE_ON = 1; + private static final int SETTING_VALUE_OFF = 0; private int mHandwritingSlop = 4; private static final Rect sHwArea = new Rect(100, 200, 500, 500); @@ -74,12 +78,21 @@ public class HandwritingInitiatorTest { private HandwritingInitiator mHandwritingInitiator; private View mTestView; private Context mContext; + private int mHwInitialState; + private boolean mShouldRestoreInitialHwState; @Before public void setup() throws Exception { final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); mContext = instrumentation.getTargetContext(); + mHwInitialState = Settings.Global.getInt(mContext.getContentResolver(), + STYLUS_HANDWRITING_ENABLED, SETTING_VALUE_OFF); + if (mHwInitialState != SETTING_VALUE_ON) { + Settings.Global.putInt(mContext.getContentResolver(), + STYLUS_HANDWRITING_ENABLED, SETTING_VALUE_ON); + mShouldRestoreInitialHwState = true; + } String imeId = HandwritingImeService.getImeId(); instrumentation.getUiAutomation().executeShellCommand("ime enable " + imeId); instrumentation.getUiAutomation().executeShellCommand("ime set " + imeId); @@ -105,6 +118,11 @@ public class HandwritingInitiatorTest { @After public void tearDown() throws Exception { + if (mShouldRestoreInitialHwState) { + mShouldRestoreInitialHwState = false; + Settings.Global.putInt(mContext.getContentResolver(), + STYLUS_HANDWRITING_ENABLED, mHwInitialState); + } InstrumentationRegistry.getInstrumentation().getUiAutomation() .executeShellCommand("ime reset"); } diff --git a/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java b/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java new file mode 100644 index 000000000000..7c7649813824 --- /dev/null +++ b/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Test for verifying {@link android.os.Bundle} recycles the underlying parcel where needed. + * + * <p>Build/Install/Run: + * atest FrameworksMockingCoreTests:android.os.BundleRecyclingTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class BundleRecyclingTest { + private Parcel mParcelSpy; + private Bundle mBundle; + + @Before + public void setUp() throws Exception { + setUpBundle(/* hasLazy */ true); + } + + @Test + public void bundleClear_whenUnparcelledWithoutLazy_recyclesParcelOnce() { + setUpBundle(/* hasLazy */ false); + // Will unparcel and immediately recycle parcel + assertNotNull(mBundle.getString("key")); + verify(mParcelSpy, times(1)).recycle(); + assertFalse(mBundle.isDefinitelyEmpty()); + + // Should not recycle again + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + assertTrue(mBundle.isDefinitelyEmpty()); + } + + @Test + public void bundleClear_whenParcelled_recyclesParcel() { + assertTrue(mBundle.isParcelled()); + verify(mParcelSpy, times(0)).recycle(); + + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + assertTrue(mBundle.isDefinitelyEmpty()); + + // Should not recycle again + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + } + + @Test + public void bundleClear_whenUnparcelledWithLazyValueUnwrapped_recyclesParcel() { + // Will unparcel with a lazy value, and immediately unwrap the lazy value, + // with no lazy values left at the end of getParcelable + assertNotNull(mBundle.getParcelable("key", CustomParcelable.class)); + verify(mParcelSpy, times(0)).recycle(); + + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + assertTrue(mBundle.isDefinitelyEmpty()); + + // Should not recycle again + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + } + + @Test + public void bundleClear_whenUnparcelledWithLazy_recyclesParcel() { + // Will unparcel but keep the CustomParcelable lazy + assertFalse(mBundle.isEmpty()); + verify(mParcelSpy, times(0)).recycle(); + + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + assertTrue(mBundle.isDefinitelyEmpty()); + + // Should not recycle again + mBundle.clear(); + verify(mParcelSpy, times(1)).recycle(); + } + + @Test + public void bundleClear_whenClearedWithSharedParcel_doesNotRecycleParcel() { + Bundle copy = new Bundle(); + copy.putAll(mBundle); + + mBundle.clear(); + assertTrue(mBundle.isDefinitelyEmpty()); + + copy.clear(); + assertTrue(copy.isDefinitelyEmpty()); + + verify(mParcelSpy, never()).recycle(); + } + + @Test + public void bundleClear_whenClearedWithCopiedParcel_doesNotRecycleParcel() { + // Will unparcel but keep the CustomParcelable lazy + assertFalse(mBundle.isEmpty()); + + Bundle copy = mBundle.deepCopy(); + copy.putAll(mBundle); + + mBundle.clear(); + assertTrue(mBundle.isDefinitelyEmpty()); + + copy.clear(); + assertTrue(copy.isDefinitelyEmpty()); + + verify(mParcelSpy, never()).recycle(); + } + + private void setUpBundle(boolean hasLazy) { + AtomicReference<Parcel> parcel = new AtomicReference<>(); + StaticMockitoSession session = mockitoSession() + .strictness(Strictness.STRICT_STUBS) + .spyStatic(Parcel.class) + .startMocking(); + doAnswer((Answer<Parcel>) invocationOnSpy -> { + Parcel spy = (Parcel) invocationOnSpy.callRealMethod(); + spyOn(spy); + parcel.set(spy); + return spy; + }).when(() -> Parcel.obtain()); + + Bundle bundle = new Bundle(); + bundle.setClassLoader(getClass().getClassLoader()); + Parcel p = createBundle(hasLazy); + bundle.readFromParcel(p); + p.recycle(); + + session.finishMocking(); + + mParcelSpy = parcel.get(); + mBundle = bundle; + } + + /** + * Create a test bundle, parcel it and return the parcel. + */ + private Parcel createBundle(boolean hasLazy) { + final Bundle source = new Bundle(); + if (hasLazy) { + source.putParcelable("key", new CustomParcelable(13, "Tiramisu")); + } else { + source.putString("key", "tiramisu"); + } + return getParcelledBundle(source); + } + + /** + * Take a bundle, write it to a parcel and return the parcel. + */ + private Parcel getParcelledBundle(Bundle bundle) { + final Parcel p = Parcel.obtain(); + // Don't use p.writeParcelabe(), which would write the creator, which we don't need. + bundle.writeToParcel(p, 0); + p.setDataPosition(0); + return p; + } + + private static class CustomParcelable implements Parcelable { + public final int integer; + public final String string; + + CustomParcelable(int integer, String string) { + this.integer = integer; + this.string = string; + } + + protected CustomParcelable(Parcel in) { + integer = in.readInt(); + string = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(integer); + out.writeString(string); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof CustomParcelable)) { + return false; + } + CustomParcelable that = (CustomParcelable) other; + return integer == that.integer && string.equals(that.string); + } + + @Override + public int hashCode() { + return Objects.hash(integer, string); + } + + public static final Creator<CustomParcelable> CREATOR = new Creator<CustomParcelable>() { + @Override + public CustomParcelable createFromParcel(Parcel in) { + return new CustomParcelable(in); + } + @Override + public CustomParcelable[] newArray(int size) { + return new CustomParcelable[size]; + } + }; + } +} diff --git a/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java index fa4aa803c75e..ed857e8fc960 100644 --- a/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java +++ b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java @@ -88,26 +88,15 @@ public class SizeConfigurationBucketsTest { } /** - * Tests that null size configuration buckets unflips the correct configuration flags. + * Tests that {@code null} size configuration buckets do not unflip the configuration flags. */ @Test public void testNullSizeConfigurationBuckets() { - // Check that all 3 size configurations are filtered out of the diff if the buckets are null - // and non-size attributes of screen layout are unchanged. Add a non-size related config - // change (i.e. CONFIG_LOCALE) to test that the diff is not set to zero. final int diff = CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE | CONFIG_SCREEN_LAYOUT | CONFIG_LOCALE; final int filteredDiffNonSizeLayoutUnchanged = SizeConfigurationBuckets.filterDiff(diff, Configuration.EMPTY, Configuration.EMPTY, null); - assertEquals(CONFIG_LOCALE, filteredDiffNonSizeLayoutUnchanged); - - // Check that only screen size and smallest screen size are filtered out of the diff if the - // buckets are null and non-size attributes of screen layout are changed. - final Configuration newConfig = new Configuration(); - newConfig.screenLayout |= SCREENLAYOUT_ROUND_YES; - final int filteredDiffNonSizeLayoutChanged = SizeConfigurationBuckets.filterDiff(diff, - Configuration.EMPTY, newConfig, null); - assertEquals(CONFIG_SCREEN_LAYOUT | CONFIG_LOCALE, filteredDiffNonSizeLayoutChanged); + assertEquals(diff, filteredDiffNonSizeLayoutUnchanged); } /** diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 547144f6ce1a..2ce8fac6d520 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -475,12 +475,6 @@ "group": "WM_DEBUG_ADD_REMOVE", "at": "com\/android\/server\/wm\/ResetTargetTaskHelper.java" }, - "-1635750891": { - "message": "Received remote change for Display[%d], applied: [%dx%d, rot = %d]", - "level": "VERBOSE", - "group": "WM_DEBUG_CONFIGURATION", - "at": "com\/android\/server\/wm\/RemoteDisplayChangeController.java" - }, "-1633115609": { "message": "Key dispatch not paused for screen off", "level": "VERBOSE", @@ -1711,6 +1705,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, + "-417730399": { + "message": "Preparing to sync a window that was already in the sync, so try dropping buffer. win=%s", + "level": "DEBUG", + "group": "WM_DEBUG_SYNC_ENGINE", + "at": "com\/android\/server\/wm\/WindowState.java" + }, "-415865166": { "message": "findFocusedWindow: Found new focus @ %s", "level": "VERBOSE", @@ -2137,6 +2137,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimation.java" }, + "-4263657": { + "message": "Got a buffer for request id=%d but latest request is id=%d. Since the buffer is out-of-date, drop it. win=%s", + "level": "DEBUG", + "group": "WM_DEBUG_SYNC_ENGINE", + "at": "com\/android\/server\/wm\/WindowState.java" + }, "3593205": { "message": "commitVisibility: %s: visible=%b mVisibleRequested=%b", "level": "VERBOSE", @@ -2599,6 +2605,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/TaskFragment.java" }, + "385237117": { + "message": "moveFocusableActivityToTop: already on top and focused, activity=%s", + "level": "DEBUG", + "group": "WM_DEBUG_FOCUS", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "385595355": { "message": "Starting animation on %s: type=%d, anim=%s", "level": "VERBOSE", @@ -3403,6 +3415,12 @@ "group": "WM_DEBUG_BOOT", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1239439010": { + "message": "moveFocusableActivityToTop: set focused, activity=%s", + "level": "DEBUG", + "group": "WM_DEBUG_FOCUS", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "1252594551": { "message": "Window types in WindowContext and LayoutParams.type should match! Type from LayoutParams is %d, but type from WindowContext is %d", "level": "WARN", @@ -3877,12 +3895,6 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/WindowStateAnimator.java" }, - "1764619787": { - "message": "Remote change for Display[%d]: timeout reached", - "level": "VERBOSE", - "group": "WM_DEBUG_CONFIGURATION", - "at": "com\/android\/server\/wm\/RemoteDisplayChangeController.java" - }, "1774661765": { "message": "Devices still not ready after waiting %d milliseconds before attempting to detect safe mode.", "level": "WARN", @@ -3991,12 +4003,6 @@ "group": "WM_DEBUG_STARTING_WINDOW", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "1856211951": { - "message": "moveFocusableActivityToTop: already on top, activity=%s", - "level": "DEBUG", - "group": "WM_DEBUG_FOCUS", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "1856783490": { "message": "resumeTopActivity: Restarting %s", "level": "DEBUG", diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 1629b6ace35d..239621eeed1e 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -40,6 +40,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.net.Uri; import android.os.Build; +import android.os.Trace; import android.system.ErrnoException; import android.system.Os; import android.util.DisplayMetrics; @@ -223,13 +224,21 @@ public final class ImageDecoder implements AutoCloseable { public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { return nCreate(mData, mOffset, mLength, preferAnimation, this); } + + @Override + public String toString() { + return "ByteArraySource{len=" + mLength + "}"; + } } private static class ByteBufferSource extends Source { ByteBufferSource(@NonNull ByteBuffer buffer) { mBuffer = buffer; + mLength = mBuffer.limit() - mBuffer.position(); } + private final ByteBuffer mBuffer; + private final int mLength; @Override public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { @@ -241,6 +250,11 @@ public final class ImageDecoder implements AutoCloseable { ByteBuffer buffer = mBuffer.slice(); return nCreate(buffer, buffer.position(), buffer.limit(), preferAnimation, this); } + + @Override + public String toString() { + return "ByteBufferSource{len=" + mLength + "}"; + } } private static class ContentResolverSource extends Source { @@ -285,6 +299,16 @@ public final class ImageDecoder implements AutoCloseable { return createFromAssetFileDescriptor(assetFd, preferAnimation, this); } + + @Override + public String toString() { + String uri = mUri.toString(); + if (uri.length() > 90) { + // We want to keep the Uri usable - usually the authority and the end is important. + uri = uri.substring(0, 80) + ".." + uri.substring(uri.length() - 10); + } + return "ContentResolverSource{uri=" + uri + "}"; + } } @NonNull @@ -399,6 +423,11 @@ public final class ImageDecoder implements AutoCloseable { return createFromStream(is, false, preferAnimation, this); } } + + @Override + public String toString() { + return "InputStream{s=" + mInputStream + "}"; + } } /** @@ -444,6 +473,11 @@ public final class ImageDecoder implements AutoCloseable { return createFromAsset(ais, preferAnimation, this); } } + + @Override + public String toString() { + return "AssetInputStream{s=" + mAssetInputStream + "}"; + } } private static class ResourceSource extends Source { @@ -485,6 +519,17 @@ public final class ImageDecoder implements AutoCloseable { return createFromAsset((AssetInputStream) is, preferAnimation, this); } + + @Override + public String toString() { + // Try to return a human-readable name for debugging purposes. + try { + return "Resource{name=" + mResources.getResourceName(mResId) + "}"; + } catch (Resources.NotFoundException e) { + // It's ok if we don't find it, fall back to ID. + } + return "Resource{id=" + mResId + "}"; + } } /** @@ -521,6 +566,11 @@ public final class ImageDecoder implements AutoCloseable { InputStream is = mAssets.open(mFileName); return createFromAsset((AssetInputStream) is, preferAnimation, this); } + + @Override + public String toString() { + return "AssetSource{file=" + mFileName + "}"; + } } private static class FileSource extends Source { @@ -534,6 +584,11 @@ public final class ImageDecoder implements AutoCloseable { public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { return createFromFile(mFile, preferAnimation, this); } + + @Override + public String toString() { + return "FileSource{file=" + mFile + "}"; + } } private static class CallableSource extends Source { @@ -557,6 +612,11 @@ public final class ImageDecoder implements AutoCloseable { } return createFromAssetFileDescriptor(assetFd, preferAnimation, this); } + + @Override + public String toString() { + return "CallableSource{obj=" + mCallable.toString() + "}"; + } } /** @@ -1763,61 +1823,65 @@ public final class ImageDecoder implements AutoCloseable { @NonNull private static Drawable decodeDrawableImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeDrawable"); try (ImageDecoder decoder = src.createImageDecoder(true /*preferAnimation*/)) { decoder.mSource = src; decoder.callHeaderDecoded(listener, src); - if (decoder.mUnpremultipliedRequired) { - // Though this could be supported (ignored) for opaque images, - // it seems better to always report this error. - throw new IllegalStateException("Cannot decode a Drawable " + - "with unpremultiplied pixels!"); - } + try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) { + if (decoder.mUnpremultipliedRequired) { + // Though this could be supported (ignored) for opaque images, + // it seems better to always report this error. + throw new IllegalStateException( + "Cannot decode a Drawable with unpremultiplied pixels!"); + } - if (decoder.mMutable) { - throw new IllegalStateException("Cannot decode a mutable " + - "Drawable!"); - } + if (decoder.mMutable) { + throw new IllegalStateException("Cannot decode a mutable Drawable!"); + } - // this call potentially manipulates the decoder so it must be performed prior to - // decoding the bitmap and after decode set the density on the resulting bitmap - final int srcDensity = decoder.computeDensity(src); - if (decoder.mAnimated) { - // AnimatedImageDrawable calls postProcessAndRelease only if - // mPostProcessor exists. - ImageDecoder postProcessPtr = decoder.mPostProcessor == null ? - null : decoder; - decoder.checkState(true); - Drawable d = new AnimatedImageDrawable(decoder.mNativePtr, - postProcessPtr, decoder.mDesiredWidth, - decoder.mDesiredHeight, decoder.getColorSpacePtr(), - decoder.checkForExtended(), srcDensity, - src.computeDstDensity(), decoder.mCropRect, - decoder.mInputStream, decoder.mAssetFd); - // d has taken ownership of these objects. - decoder.mInputStream = null; - decoder.mAssetFd = null; - return d; - } + // this call potentially manipulates the decoder so it must be performed prior to + // decoding the bitmap and after decode set the density on the resulting bitmap + final int srcDensity = decoder.computeDensity(src); + if (decoder.mAnimated) { + // AnimatedImageDrawable calls postProcessAndRelease only if + // mPostProcessor exists. + ImageDecoder postProcessPtr = decoder.mPostProcessor == null ? null : decoder; + decoder.checkState(true); + Drawable d = new AnimatedImageDrawable(decoder.mNativePtr, + postProcessPtr, decoder.mDesiredWidth, + decoder.mDesiredHeight, decoder.getColorSpacePtr(), + decoder.checkForExtended(), srcDensity, + src.computeDstDensity(), decoder.mCropRect, + decoder.mInputStream, decoder.mAssetFd); + // d has taken ownership of these objects. + decoder.mInputStream = null; + decoder.mAssetFd = null; + return d; + } - Bitmap bm = decoder.decodeBitmapInternal(); - bm.setDensity(srcDensity); + Bitmap bm = decoder.decodeBitmapInternal(); + bm.setDensity(srcDensity); - Resources res = src.getResources(); - byte[] np = bm.getNinePatchChunk(); - if (np != null && NinePatch.isNinePatchChunk(np)) { - Rect opticalInsets = new Rect(); - bm.getOpticalInsets(opticalInsets); - Rect padding = decoder.mOutPaddingRect; - if (padding == null) { - padding = new Rect(); + Resources res = src.getResources(); + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + Rect opticalInsets = new Rect(); + bm.getOpticalInsets(opticalInsets); + Rect padding = decoder.mOutPaddingRect; + if (padding == null) { + padding = new Rect(); + } + nGetPadding(decoder.mNativePtr, padding); + return new NinePatchDrawable(res, bm, np, padding, + opticalInsets, null); } - nGetPadding(decoder.mNativePtr, padding); - return new NinePatchDrawable(res, bm, np, padding, - opticalInsets, null); - } - return new BitmapDrawable(res, bm); + return new BitmapDrawable(res, bm); + } + } finally { + // Close the ImageDecoder#decode trace. + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } } @@ -1867,26 +1931,51 @@ public final class ImageDecoder implements AutoCloseable { @NonNull private static Bitmap decodeBitmapImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeBitmap"); try (ImageDecoder decoder = src.createImageDecoder(false /*preferAnimation*/)) { decoder.mSource = src; decoder.callHeaderDecoded(listener, src); + try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) { + // this call potentially manipulates the decoder so it must be performed prior to + // decoding the bitmap + final int srcDensity = decoder.computeDensity(src); + Bitmap bm = decoder.decodeBitmapInternal(); + bm.setDensity(srcDensity); - // this call potentially manipulates the decoder so it must be performed prior to - // decoding the bitmap - final int srcDensity = decoder.computeDensity(src); - Bitmap bm = decoder.decodeBitmapInternal(); - bm.setDensity(srcDensity); - - Rect padding = decoder.mOutPaddingRect; - if (padding != null) { - byte[] np = bm.getNinePatchChunk(); - if (np != null && NinePatch.isNinePatchChunk(np)) { - nGetPadding(decoder.mNativePtr, padding); + Rect padding = decoder.mOutPaddingRect; + if (padding != null) { + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + nGetPadding(decoder.mNativePtr, padding); + } } + return bm; } + } finally { + // Close the ImageDecoder#decode trace. + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } + } - return bm; + /** + * This describes the decoder in traces to ease debugging. It has to be called after + * header has been decoded and width/height have been populated. It should be used + * inside a try-with-resources call to automatically complete the trace. + */ + private static AutoCloseable traceDecoderSource(ImageDecoder decoder) { + final boolean resourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES); + if (resourceTracingEnabled) { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder)); } + + return new AutoCloseable() { + @Override + public void close() throws Exception { + if (resourceTracingEnabled) { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } + } + }; } // This method may modify the decoder so it must be called prior to performing the decode @@ -1994,6 +2083,66 @@ public final class ImageDecoder implements AutoCloseable { } } + /** + * Returns a short string describing what passed ImageDecoder is loading - + * it reports image dimensions, desired dimensions (if any) and source resource. + * + * The string appears in perf traces to simplify search for slow or memory intensive + * image loads. + * + * Example: ID#w=300;h=250;dw=150;dh=150;src=Resource{name=@resource} + * + * @hide + */ + private static String describeDecoderForTrace(@NonNull ImageDecoder decoder) { + StringBuilder builder = new StringBuilder(); + // Source dimensions + builder.append("ID#w="); + builder.append(decoder.mWidth); + builder.append(";h="); + builder.append(decoder.mHeight); + // Desired dimensions (if present) + if (decoder.mDesiredWidth != decoder.mWidth + || decoder.mDesiredHeight != decoder.mHeight) { + builder.append(";dw="); + builder.append(decoder.mDesiredWidth); + builder.append(";dh="); + builder.append(decoder.mDesiredHeight); + } + // Source description + builder.append(";src="); + builder.append(decoder.mSource); + return builder.toString(); + } + + /** + * Records a trace with information about the source being decoded - dimensions, + * desired dimensions and source information. + * + * It significantly eases debugging of slow resource loads on main thread and + * possible large memory consumers. + * + * @hide + */ + private static final class ImageDecoderSourceTrace implements AutoCloseable { + + private final boolean mResourceTracingEnabled; + + ImageDecoderSourceTrace(ImageDecoder decoder) { + mResourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES); + if (mResourceTracingEnabled) { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder)); + } + } + + @Override + public void close() { + if (mResourceTracingEnabled) { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } + } + } + private static native ImageDecoder nCreate(long asset, boolean preferAnimation, Source src) throws IOException; private static native ImageDecoder nCreate(ByteBuffer buffer, int position, int limit, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index b15dce7f3f17..41791afa45a3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -24,9 +24,9 @@ import static androidx.window.extensions.embedding.SplitContainer.getFinishSecon import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; -import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair; -import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; +import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; import android.app.Activity; @@ -381,6 +381,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * in a state that the caller shouldn't handle. */ @VisibleForTesting + @GuardedBy("mLock") boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) { if (isInPictureInPicture(activity) || activity.isFinishing()) { // We don't embed activity when it is in PIP, or finishing. Return true since we don't @@ -581,8 +582,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** Finds the activity below the given activity. */ + @VisibleForTesting @Nullable - private Activity findActivityBelow(@NonNull Activity activity) { + Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; final TaskFragmentContainer container = getContainerWithActivity(activity); if (container != null) { @@ -606,6 +608,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Checks if there is a rule to split the two activities. If there is one, puts them into split * and returns {@code true}. Otherwise, returns {@code false}. */ + @GuardedBy("mLock") private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); @@ -616,25 +619,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() - && canReuseContainer(splitRule, splitContainer.getSplitRule()) - && !boundsSmallerThanMinDimensions(primaryContainer.getLastRequestedBounds(), - getMinDimensions(primaryActivity))) { + && canReuseContainer(splitRule, splitContainer.getSplitRule())) { // Can launch in the existing secondary container if the rules share the same // presentation. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); - if (secondaryContainer == getContainerWithActivity(secondaryActivity) - && !boundsSmallerThanMinDimensions(secondaryContainer.getLastRequestedBounds(), - getMinDimensions(secondaryActivity))) { + if (secondaryContainer == getContainerWithActivity(secondaryActivity)) { // The activity is already in the target TaskFragment. return true; } secondaryContainer.addPendingAppearedActivity(secondaryActivity); final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.reparentActivityToTaskFragment( - secondaryContainer.getTaskFragmentToken(), - secondaryActivity.getActivityToken()); - mPresenter.applyTransaction(wct); - return true; + if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, + secondaryActivity, null /* secondaryIntent */) + != RESULT_EXPAND_FAILED_NO_TF_INFO) { + wct.reparentActivityToTaskFragment( + secondaryContainer.getTaskFragmentToken(), + secondaryActivity.getActivityToken()); + mPresenter.applyTransaction(wct); + return true; + } } // Create new split pair. mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule); @@ -792,6 +795,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a container for the new activity intent to launch into as splitting with the primary * activity. */ + @GuardedBy("mLock") @Nullable private TaskFragmentContainer getSecondaryContainerForSplitIfAny( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @@ -805,16 +809,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() && (canReuseContainer(splitRule, splitContainer.getSplitRule()) // TODO(b/231845476) we should always respect clearTop. - || !respectClearTop)) { - final Rect secondaryBounds = splitContainer.getSecondaryContainer() - .getLastRequestedBounds(); - if (secondaryBounds.isEmpty() - || !boundsSmallerThanMinDimensions(secondaryBounds, - getMinDimensions(intent))) { - // Can launch in the existing secondary container if the rules share the same - // presentation. - return splitContainer.getSecondaryContainer(); - } + || !respectClearTop) + && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, + null /* secondaryActivity */, intent) != RESULT_EXPAND_FAILED_NO_TF_INFO) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + return splitContainer.getSecondaryContainer(); } // Create a new TaskFragment to split with the primary activity for the new activity. return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, @@ -868,6 +868,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * if needed. * @param taskId parent Task of the new TaskFragment. */ + @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { if (activityInTask == null) { @@ -881,7 +882,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen pendingAppearedIntent, taskContainer, this); if (!taskContainer.isTaskBoundsInitialized()) { // Get the initial bounds before the TaskFragment has appeared. - final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask); + final Rect taskBounds = getNonEmbeddedActivityBounds(activityInTask); if (!taskContainer.setTaskBounds(taskBounds)) { Log.w(TAG, "Can't find bounds from activity=" + activityInTask); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 1b79ad999435..a89847a30d20 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -65,6 +65,41 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * No need to expand the splitContainer because screen is big enough to + * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied. + */ + static final int RESULT_NOT_EXPANDED = 0; + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * The splitContainer should be expanded. It is usually because minimum dimensions is not + * satisfied. + * @see #shouldShowSideBySide(Rect, SplitRule, Pair) + */ + static final int RESULT_EXPANDED = 1; + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * The splitContainer should be expanded, but the client side hasn't received + * {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer + * instead. + */ + static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2; + + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)} + */ + @IntDef(value = { + RESULT_NOT_EXPANDED, + RESULT_EXPANDED, + RESULT_EXPAND_FAILED_NO_TF_INFO, + }) + private @interface ResultCode {} + private final SplitController mController; SplitPresenter(@NonNull Executor executor, SplitController controller) { @@ -396,6 +431,44 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.updateWindowingMode(wct, fragmentToken, windowingMode); } + /** + * Expands the split container if the current split bounds are smaller than the Activity or + * Intent that is added to the container. + * + * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} + * and if {@link android.window.TaskFragmentInfo} has reported to the client side. + */ + @ResultCode + int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, + @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity, + @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) { + if (secondaryActivity == null && secondaryIntent == null) { + throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be" + + " non-null."); + } + final Rect taskBounds = getParentContainerBounds(primaryActivity); + final Pair<Size, Size> minDimensionsPair; + if (secondaryActivity != null) { + minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); + } else { + minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity, + secondaryIntent); + } + // Expand the splitContainer if minimum dimensions are not satisfied. + if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) { + // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment + // bounds. Return failure to create a new SplitContainer which fills task bounds. + if (splitContainer.getPrimaryContainer().getInfo() == null + || splitContainer.getSecondaryContainer().getInfo() == null) { + return RESULT_EXPAND_FAILED_NO_TF_INFO; + } + expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken()); + expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken()); + return RESULT_EXPANDED; + } + return RESULT_NOT_EXPANDED; + } + static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); } @@ -565,11 +638,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { if (container != null) { return getParentContainerBounds(container); } - return getTaskBoundsFromActivity(activity); + // Obtain bounds from Activity instead because the Activity hasn't been embedded yet. + return getNonEmbeddedActivityBounds(activity); } + /** + * Obtains the bounds from a non-embedded Activity. + * <p> + * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most + * cases unless we want to obtain task bounds before + * {@link TaskContainer#isTaskBoundsInitialized()}. + */ @NonNull - static Rect getTaskBoundsFromActivity(@NonNull Activity activity) { + static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) { final WindowConfiguration windowConfiguration = activity.getResources().getConfiguration().windowConfiguration; if (!activity.isInMultiWindowMode()) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java index 1ac33173668b..c4f37091a491 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java @@ -83,9 +83,9 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { if (TaskFragmentAnimationController.DEBUG) { - Log.v(TAG, "onAnimationCancelled"); + Log.v(TAG, "onAnimationCancelled: isKeyguardOccluded=" + isKeyguardOccluded); } mHandler.post(this::cancelAnimation); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java index 835c40365cda..effc1a3ef3ea 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock; import android.annotation.NonNull; import android.app.Activity; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; @@ -57,13 +58,21 @@ public class EmbeddingTestUtils { /** Creates a rule to always split the given activity and the given intent. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { + return createSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); + } + + /** Creates a rule to always split the given activity and the given intent. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent, boolean clearTop) { final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); return new SplitPairRule.Builder( activityPair -> false, targetPair::equals, w -> true) .setSplitRatio(SPLIT_RATIO) - .setShouldClearTop(true) + .setShouldClearTop(clearTop) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) .build(); } @@ -75,6 +84,14 @@ public class EmbeddingTestUtils { true /* clearTop */); } + /** Creates a rule to always split the given activities. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { + return createSplitRule(primaryActivity, secondaryActivity, + DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, + clearTop); + } + /** Creates a rule to always split the given activities with the given finish behaviors. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, @@ -105,4 +122,12 @@ public class EmbeddingTestUtils { false /* isTaskFragmentClearedForPip */, new Point()); } + + static ActivityInfo createActivityInfoWithMinDimensions() { + ActivityInfo aInfo = new ActivityInfo(); + final Rect primaryBounds = getSplitBounds(true /* isPrimary */); + aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, + primaryBounds.width() + 1, primaryBounds.height() + 1); + return aInfo; + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index ef7728cec387..042547fd30f2 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; @@ -34,6 +35,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; @@ -436,6 +438,50 @@ public class SplitControllerTest { } @Test + public void testResolveStartActivityIntent_shouldExpandSplitContainer() { + final Intent intent = new Intent().setComponent( + new ComponentName(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class)); + setupSplitRule(mActivity, intent, false /* clearTop */); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity)); + } + + @Test + public void testResolveStartActivityIntent_noInfo_shouldCreateSplitContainer() { + final Intent intent = new Intent().setComponent( + new ComponentName(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class)); + setupSplitRule(mActivity, intent, false /* clearTop */); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); + + final TaskFragmentContainer secondaryContainer = mSplitController + .getContainerWithActivity(secondaryActivity); + secondaryContainer.mInfo = null; + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertNotEquals(container, secondaryContainer); + } + + @Test public void testPlaceActivityInTopContainer() { mSplitController.placeActivityInTopContainer(mActivity); @@ -787,11 +833,7 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(mActivity, activityBelow); - ActivityInfo aInfo = new ActivityInfo(); - final Rect primaryBounds = getSplitBounds(true /* isPrimary */); - aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, - primaryBounds.width() + 1, primaryBounds.height() + 1); - doReturn(aInfo).when(mActivity).getActivityInfo(); + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); @@ -810,17 +852,12 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(activityBelow, mActivity); - ActivityInfo aInfo = new ActivityInfo(); - final Rect secondaryBounds = getSplitBounds(false /* isPrimary */); - aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, - secondaryBounds.width() + 1, secondaryBounds.height() + 1); - doReturn(aInfo).when(mActivity).getActivityInfo(); + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); container.addPendingAppearedActivity(mActivity); - // Allow to split as primary. boolean result = mSplitController.resolveActivityToContainer(mActivity, false /* isOnReparent */); @@ -828,6 +865,29 @@ public class SplitControllerTest { assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */); } + // Suppress GuardedBy warning on unit tests + @SuppressWarnings("GuardedBy") + @Test + public void testResolveActivityToContainer_minDimensions_shouldExpandSplitContainer() { + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, secondaryActivity, false /* clearTop */); + + setupSplitRule(primaryActivity, mActivity, false /* clearTop */); + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); + doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity)); + + clearInvocations(mSplitPresenter); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */); + assertEquals(mSplitController.getContainerWithActivity(secondaryActivity), + mSplitController.getContainerWithActivity(mActivity)); + verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any()); + } + @Test public void testResolveActivityToContainer_inUnknownTaskFragment() { doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity); @@ -944,23 +1004,41 @@ public class SplitControllerTest { /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { - final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent); + setupSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent, boolean clearTop) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent, clearTop); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { - final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity); + setupSplitRule(primaryActivity, secondaryActivity, true /* clearTop */); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity, clearTop); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } /** Adds a pair of TaskFragments as split for the given activities. */ private void addSplitTaskFragments(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { + addSplitTaskFragments(primaryActivity, secondaryActivity, true /* clearTop */); + } + + /** Adds a pair of TaskFragments as split for the given activities. */ + private void addSplitTaskFragments(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { registerSplitPair(createMockTaskFragmentContainer(primaryActivity), createMockTaskFragmentContainer(secondaryActivity), - createSplitRule(primaryActivity, secondaryActivity)); + createSplitRule(primaryActivity, secondaryActivity, clearTop)); } /** Registers the two given TaskFragments as split pair. */ @@ -1011,16 +1089,18 @@ public class SplitControllerTest { if (primaryContainer.mInfo != null) { final Rect primaryBounds = matchParentBounds ? new Rect() : getSplitBounds(true /* isPrimary */); + final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED + : WINDOWING_MODE_MULTI_WINDOW; assertTrue(primaryContainer.areLastRequestedBoundsEqual(primaryBounds)); - assertTrue(primaryContainer.isLastRequestedWindowingModeEqual( - WINDOWING_MODE_MULTI_WINDOW)); + assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); } if (secondaryContainer.mInfo != null) { final Rect secondaryBounds = matchParentBounds ? new Rect() : getSplitBounds(false /* isPrimary */); + final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED + : WINDOWING_MODE_MULTI_WINDOW; assertTrue(secondaryContainer.areLastRequestedBoundsEqual(secondaryBounds)); - assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual( - WINDOWING_MODE_MULTI_WINDOW)); + assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); } } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index acc398a27baf..d79319666c01 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -20,11 +20,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_END; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_FILL; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPANDED; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_NOT_EXPANDED; import static androidx.window.extensions.embedding.SplitPresenter.getBoundsForPosition; import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; @@ -34,6 +39,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -49,6 +55,7 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.util.Pair; import android.util.Size; @@ -195,6 +202,52 @@ public class SplitPresenterTest { splitRule, mActivity, minDimensionsPair)); } + @Test + public void testExpandSplitContainerIfNeeded() { + SplitContainer splitContainer = mock(SplitContainer.class); + Activity secondaryActivity = createMockActivity(); + SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); + TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); + TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID); + doReturn(splitRule).when(splitContainer).getSplitRule(); + doReturn(primaryTf).when(splitContainer).getPrimaryContainer(); + doReturn(secondaryTf).when(splitContainer).getSecondaryContainer(); + + assertThrows(IllegalArgumentException.class, () -> + mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, + null /* secondaryActivity */, null /* secondaryIntent */)); + + assertEquals(RESULT_NOT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, + splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); + verify(mPresenter, never()).expandTaskFragment(any(), any()); + + doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo(); + assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded( + mTransaction, splitContainer, mActivity, secondaryActivity, + null /* secondaryIntent */)); + + primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity)); + secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity)); + + assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, + splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(primaryTf.getTaskFragmentToken())); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(secondaryTf.getTaskFragmentToken())); + + clearInvocations(mPresenter); + + assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, + splitContainer, mActivity, null /* secondaryActivity */, + new Intent(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class))); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(primaryTf.getTaskFragmentToken())); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(secondaryTf.getTaskFragmentToken())); + } + private Activity createMockActivity() { final Activity activity = mock(Activity.class); final Configuration activityConfig = new Configuration(); @@ -203,6 +256,7 @@ public class SplitPresenterTest { doReturn(mActivityResources).when(activity).getResources(); doReturn(activityConfig).when(mActivityResources).getConfiguration(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); + doReturn(mock(IBinder.class)).when(activity).getActivityToken(); return activity; } } diff --git a/libs/WindowManager/Shell/res/values-television/config.xml b/libs/WindowManager/Shell/res/values-television/config.xml index 86ca65526336..cc0333efd82b 100644 --- a/libs/WindowManager/Shell/res/values-television/config.xml +++ b/libs/WindowManager/Shell/res/values-television/config.xml @@ -43,4 +43,13 @@ <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself if a custom action is present before closing it. --> <integer name="config_pipForceCloseDelay">5000</integer> + + <!-- Animation duration when exit starting window: fade out icon --> + <integer name="starting_window_app_reveal_icon_fade_out_duration">0</integer> + + <!-- Animation duration when exit starting window: reveal app --> + <integer name="starting_window_app_reveal_anim_delay">0</integer> + + <!-- Animation duration when exit starting window: reveal app --> + <integer name="starting_window_app_reveal_anim_duration">0</integer> </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 4eba1697b595..cf2734c375f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -591,7 +591,7 @@ public class PipAnimationController { final Rect insets = computeInsets(fraction); getSurfaceTransactionHelper().scaleAndCrop(tx, leash, sourceHintRect, initialSourceValue, bounds, insets, - isInPipDirection); + isInPipDirection, fraction); if (shouldApplyCornerRadius()) { final Rect sourceBounds = new Rect(initialContainerRect); sourceBounds.inset(insets); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index a017a2674359..c0bc108baada 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -104,7 +104,7 @@ public class PipSurfaceTransactionHelper { public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, SurfaceControl leash, Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets, - boolean isInPipDirection) { + boolean isInPipDirection, float fraction) { mTmpDestinationRect.set(sourceBounds); // Similar to {@link #scale}, we want to position the surface relative to the screen // coordinates so offset the bounds to 0,0 @@ -116,9 +116,13 @@ public class PipSurfaceTransactionHelper { if (isInPipDirection && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) { // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only. - scale = sourceBounds.width() <= sourceBounds.height() + final float endScale = sourceBounds.width() <= sourceBounds.height() ? (float) destinationBounds.width() / sourceRectHint.width() : (float) destinationBounds.height() / sourceRectHint.height(); + final float startScale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + scale = (1 - fraction) * startScale + fraction * endScale; } else { scale = sourceBounds.width() <= sourceBounds.height() ? (float) destinationBounds.width() / sourceBounds.width() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index bd386b5681d8..22b0ccbc8488 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -942,7 +942,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // Re-set the PIP bounds to none. mPipBoundsState.setBounds(new Rect()); mPipUiEventLoggerLogger.setTaskInfo(null); - mPipMenuController.detach(); + mMainExecutor.executeDelayed(() -> mPipMenuController.detach(), 0); if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) { mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); @@ -1472,6 +1472,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, "%s: Abort animation, invalid leash", TAG); return null; } + if (isInPipDirection(direction) + && !isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) { + // The given source rect hint is too small for enter PiP animation, reset it to null. + sourceHintRect = null; + } final int rotationDelta = mWaitForFixedRotation ? deltaRotation(mCurrentRotation, mNextRotation) : Surface.ROTATION_0; @@ -1546,6 +1551,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** + * This is a situation in which the source rect hint on at least one axis is smaller + * than the destination bounds, which represents a problem because we would have to scale + * up that axis to fit the bounds. So instead, just fallback to the non-source hint + * animation in this case. + * + * @return {@code false} if the given source is too small to use for the entering animation. + */ + private boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) { + return sourceRectHint != null + && sourceRectHint.width() > destinationBounds.width() + && sourceRectHint.height() > destinationBounds.height(); + } + + /** * Sync with {@link SplitScreenController} on destination bounds if PiP is going to * split screen. * diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index c80c14f353d0..05a890fc65ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -715,7 +715,7 @@ public class PipTransition extends PipTransitionController { mSurfaceTransactionHelper .crop(finishTransaction, leash, destinationBounds) .round(finishTransaction, leash, true /* applyCornerRadius */); - mPipMenuController.attach(leash); + mTransitions.getMainExecutor().executeDelayed(() -> mPipMenuController.attach(leash), 0); if (taskInfo.pictureInPictureParams != null && taskInfo.pictureInPictureParams.isAutoEnterEnabled() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index a43b6043908b..90a2695bdf90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -28,9 +28,7 @@ import android.app.TaskInfo; import android.content.ComponentName; import android.content.pm.ActivityInfo; import android.graphics.Rect; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -56,7 +54,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected final ShellTaskOrganizer mShellTaskOrganizer; protected final PipMenuController mPipMenuController; protected final Transitions mTransitions; - private final Handler mMainHandler; private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); protected PipTaskOrganizer mPipOrganizer; @@ -144,7 +141,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipAnimationController = pipAnimationController; mTransitions = transitions; - mMainHandler = new Handler(Looper.getMainLooper()); if (Transitions.ENABLE_SHELL_TRANSITIONS) { transitions.addHandler(this); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 4e7b20e3ae84..59b0afe22acb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -457,10 +457,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { onRemoteAnimationFinishedOrCancelled(evictWct); try { - adapter.getRunner().onAnimationCancelled(); + adapter.getRunner().onAnimationCancelled(isKeyguardOccluded); } catch (RemoteException e) { Slog.e(TAG, "Error starting remote animation", e); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS new file mode 100644 index 000000000000..d325d161ac53 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module TV splitscreen owner +galinap@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 05e5b8e66a00..dcd6277966dd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -866,13 +866,19 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { }); }; va.addListener(new AnimatorListenerAdapter() { + private boolean mFinished = false; + @Override public void onAnimationEnd(Animator animation) { + if (mFinished) return; + mFinished = true; finisher.run(); } @Override public void onAnimationCancel(Animator animation) { + if (mFinished) return; + mFinished = true; finisher.run(); } }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java index 61e11e877b90..61e92f355dc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java @@ -107,7 +107,7 @@ public class LegacyTransitions { } @Override - public void onAnimationCancelled() throws RemoteException { + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { mCancelled = true; mApps = mWallpapers = mNonApps = null; checkApply(); diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt index 278ba9b0f4db..5b073038059c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt @@ -48,7 +48,7 @@ abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { ServiceManager.getService(Context.NOTIFICATION_SERVICE)) protected val uid = context.packageManager.getApplicationInfo( - testApp.component.packageName, 0).uid + testApp.`package`, 0).uid protected abstract val transition: FlickerBuilder.() -> Unit @@ -59,7 +59,7 @@ abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { return { setup { test { - notifyManager.setBubblesAllowed(testApp.component.packageName, + notifyManager.setBubblesAllowed(testApp.`package`, uid, NotificationManager.BUBBLE_PREFERENCE_ALL) testApp.launchViaIntent(wmHelper) waitAndGetAddBubbleBtn() @@ -69,7 +69,7 @@ abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { teardown { test { - notifyManager.setBubblesAllowed(testApp.component.packageName, + notifyManager.setBubblesAllowed(testApp.`package`, uid, NotificationManager.BUBBLE_PREFERENCE_NONE) testApp.exit() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt index 27d65648dbb7..e2b19ea22b3b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt @@ -16,10 +16,10 @@ package com.android.wm.shell.flicker.bubble +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.WindowInsets import android.view.WindowManager -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until @@ -29,8 +29,8 @@ import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import org.junit.Assume -import org.junit.runner.RunWith import org.junit.Test +import org.junit.runner.RunWith import org.junit.runners.Parameterized /** @@ -54,20 +54,21 @@ class LaunchBubbleFromLockScreen(testSpec: FlickerTestParameter) : BaseBubbleScr val addBubbleBtn = waitAndGetAddBubbleBtn() addBubbleBtn?.click() ?: error("Bubble widget not found") device.sleep() - wmHelper.waitFor("noAppWindowsOnTop") { - it.wmState.topVisibleAppWindow.isEmpty() - } + wmHelper.StateSyncBuilder() + .withoutTopVisibleAppWindows() + .waitForAndVerify() device.wakeUp() } } transitions { // Swipe & wait for the notification shade to expand so all can be seen val wm = context.getSystemService(WindowManager::class.java) - val metricInsets = wm.getCurrentWindowMetrics().windowInsets + ?: error("Unable to obtain WM service") + val metricInsets = wm.currentWindowMetrics.windowInsets val insets = metricInsets.getInsetsIgnoringVisibility( WindowInsets.Type.statusBars() or WindowInsets.Type.displayCutout()) - device.swipe(100, insets.top + 100, 100, device.getDisplayHeight() / 2, 4) + device.swipe(100, insets.top + 100, 100, device.displayHeight / 2, 4) device.waitForIdle(2000) instrumentation.uiAutomation.syncInputTransactions() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt index 41cd31aabf05..f4305ed19824 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt @@ -17,44 +17,10 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import com.android.server.wm.flicker.Flicker -import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.traces.common.FlickerComponentName -import com.android.server.wm.traces.common.region.Region class AppPairsHelper( instrumentation: Instrumentation, activityLabel: String, component: FlickerComponentName -) : BaseAppHelper(instrumentation, activityLabel, component) { - fun getPrimaryBounds(dividerBounds: Region): Region { - val primaryAppBounds = Region.from(0, 0, dividerBounds.bounds.right, - dividerBounds.bounds.bottom + WindowUtils.dockedStackDividerInset) - return primaryAppBounds - } - - fun getSecondaryBounds(dividerBounds: Region): Region { - val displayBounds = WindowUtils.displayBounds - val secondaryAppBounds = Region.from(0, - dividerBounds.bounds.bottom - WindowUtils.dockedStackDividerInset, - displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarFrameHeight) - return secondaryAppBounds - } - - companion object { - const val TEST_REPETITIONS = 1 - const val TIMEOUT_MS = 3_000L - - fun Flicker.waitAppsShown(app1: SplitScreenHelper?, app2: SplitScreenHelper?) { - wmHelper.waitFor("primaryAndSecondaryAppsVisible") { dump -> - val primaryAppVisible = app1?.let { - dump.wmState.isWindowSurfaceShown(app1.defaultWindowName) - } ?: false - val secondaryAppVisible = app2?.let { - dump.wmState.isWindowSurfaceShown(app2.defaultWindowName) - } ?: false - primaryAppVisible && secondaryAppVisible - } - } - } -} +) : BaseAppHelper(instrumentation, activityLabel, component) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt index 3dd9e0572947..912ba67285f7 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt @@ -40,7 +40,7 @@ abstract class BaseAppHelper( component, LauncherStrategyFactory.getInstance(instrumentation).launcherStrategy ) { - private val appSelector = By.pkg(component.packageName).depth(0) + private val appSelector = By.pkg(`package`).depth(0) protected val isTelevision: Boolean get() = context.packageManager.run { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt index cc5b9f9eb26d..2e690de666f4 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation import androidx.test.uiautomator.By -import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.FIND_TIMEOUT import com.android.server.wm.traces.parser.toFlickerComponent @@ -35,8 +34,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( * * @param wmHelper Helper used to wait for WindowManager states */ - @JvmOverloads - open fun openIME(wmHelper: WindowManagerStateHelper? = null) { + open fun openIME(wmHelper: WindowManagerStateHelper) { if (!isTelevision) { val editText = uiDevice.wait( Until.findObject(By.res(getPackage(), "plain_text_input")), @@ -47,7 +45,9 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( "was left in an unknown state (e.g. in split screen)" } editText.click() - waitAndAssertIMEShown(uiDevice, wmHelper) + wmHelper.StateSyncBuilder() + .withImeShown() + .waitForAndVerify() } else { // If we do the same thing as above - editText.click() - on TV, that's going to force TV // into the touch mode. We really don't want that. @@ -55,36 +55,22 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } } - protected fun waitAndAssertIMEShown( - device: UiDevice, - wmHelper: WindowManagerStateHelper? = null - ) { - if (wmHelper == null) { - device.waitForIdle() - } else { - wmHelper.waitImeShown() - } - } - /** * Opens the IME and wait for it to be gone * * @param wmHelper Helper used to wait for WindowManager states */ - @JvmOverloads - open fun closeIME(wmHelper: WindowManagerStateHelper? = null) { + open fun closeIME(wmHelper: WindowManagerStateHelper) { if (!isTelevision) { uiDevice.pressBack() // Using only the AccessibilityInfo it is not possible to identify if the IME is active - if (wmHelper == null) { - uiDevice.waitForIdle() - } else { - wmHelper.waitImeGone() - } + wmHelper.StateSyncBuilder() + .withImeGone() + .waitForAndVerify() } else { // While pressing the back button should close the IME on TV as well, it may also lead // to the app closing. So let's instead just ask the app to close the IME. launchViaIntent(action = Components.ImeActivity.ACTION_CLOSE_IME) } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt index f73d191b1917..216445fe9356 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt @@ -19,13 +19,13 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation import android.media.session.MediaController import android.media.session.MediaSessionManager -import android.os.SystemClock import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.FIND_TIMEOUT import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE import com.android.server.wm.traces.common.Rect +import com.android.server.wm.traces.common.WindowManagerConditionsFactory import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow @@ -43,11 +43,11 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( private val mediaController: MediaController? get() = mediaSessionManager.getActiveSessions(null).firstOrNull { - it.packageName == component.packageName + it.packageName == `package` } fun clickObject(resId: String) { - val selector = By.res(component.packageName, resId) + val selector = By.res(`package`, resId) val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") if (!isTelevision) { @@ -71,8 +71,12 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( ) { launchViaIntentAndWaitShown( wmHelper, expectedWindowName, action, stringExtras, - waitConditions = arrayOf(WindowManagerStateHelper.pipShownCondition) + waitConditions = arrayOf(WindowManagerConditionsFactory.hasPipWindow()) ) + + wmHelper.StateSyncBuilder() + .withPipShown() + .waitForAndVerify() } /** @@ -95,12 +99,13 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( return false } - @JvmOverloads - fun clickEnterPipButton(wmHelper: WindowManagerStateHelper? = null) { + fun clickEnterPipButton(wmHelper: WindowManagerStateHelper) { clickObject(ENTER_PIP_BUTTON_ID) // Wait on WMHelper or simply wait for 3 seconds - wmHelper?.waitPipShown() ?: SystemClock.sleep(3_000) + wmHelper.StateSyncBuilder() + .withPipShown() + .waitForAndVerify() // when entering pip, the dismiss button is visible at the start. to ensure the pip // animation is complete, wait until the pip dismiss button is no longer visible. // b/176822698: dismiss-only state will be removed in the future @@ -116,7 +121,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } fun checkWithCustomActionsCheckbox() = uiDevice - .findObject(By.res(component.packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID)) + .findObject(By.res(`package`, WITH_CUSTOM_ACTIONS_BUTTON_ID)) ?.takeIf { it.isCheckable } ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) } ?: error("'With custom actions' checkbox not found") @@ -166,8 +171,10 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } // Wait for animation to complete. - wmHelper.waitPipGone() - wmHelper.waitForHomeActivityVisible() + wmHelper.StateSyncBuilder() + .withPipGone() + .withHomeActivityVisible() + .waitForAndVerify() } /** @@ -183,8 +190,10 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( ?: error("PIP window expand button not found") val expandButtonBounds = expandPipObject.visibleBounds uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY()) - wmHelper.waitPipGone() - wmHelper.waitForAppTransitionIdle() + wmHelper.StateSyncBuilder() + .withPipGone() + .withFullScreenApp(component) + .waitForAndVerify() } /** @@ -194,7 +203,9 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( val windowRect = getWindowRect(wmHelper) uiDevice.click(windowRect.centerX(), windowRect.centerY()) uiDevice.click(windowRect.centerX(), windowRect.centerY()) - wmHelper.waitForAppTransitionIdle() + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .waitForAndVerify() } companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt index e7d641e9c66e..cb84fc441f75 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.pip import androidx.test.filters.RequiresDevice -import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.annotation.Group3 @@ -51,7 +50,6 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 class EnterPipOnUserLeaveHintTest(testSpec: FlickerTestParameter) : EnterPipTest(testSpec) { - protected val taplInstrumentation = LauncherInstrumentation() /** * Defines the transition used to run the test */ @@ -70,7 +68,7 @@ class EnterPipOnUserLeaveHintTest(testSpec: FlickerTestParameter) : EnterPipTest } } transitions { - taplInstrumentation.goHome() + tapl.goHome() } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt index eb318fb256c9..c18c8ce345fc 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -16,16 +16,16 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.traces.common.FlickerComponentName.Companion.LAUNCHER import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -157,15 +157,15 @@ open class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec } /** - * Checks [LAUNCHER_COMPONENT] layer remains visible throughout the animation + * Checks [LAUNCHER] layer remains visible throughout the animation */ @Presubmit @Test fun launcherLayerBecomesVisible() { testSpec.assertLayers { - isInvisible(LAUNCHER_COMPONENT) + isInvisible(LAUNCHER) .then() - .isVisible(LAUNCHER_COMPONENT) + .isVisible(LAUNCHER) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt index 75481f2199c2..0333577b0d2a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt @@ -16,9 +16,9 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -33,8 +33,8 @@ import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.helpers.FixedAppHelper import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT -import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION +import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -98,10 +98,12 @@ class EnterPipToOtherOrientationTest( // Enter PiP, and assert that the PiP is within bounds now that the device is back // in portrait broadcastActionTrigger.doAction(ACTION_ENTER_PIP) - wmHelper.waitPipShown() - wmHelper.waitForAppTransitionIdle() // during rotation the status bar becomes invisible and reappears at the end - wmHelper.waitForNavBarStatusBarVisible() + wmHelper.StateSyncBuilder() + .withPipShown() + .withAppTransitionIdle() + .withNavBarStatusBarVisible() + .waitForAndVerify() } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt index 0b4bc761838d..47215b9fb883 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt @@ -19,10 +19,10 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit import android.view.Surface import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.traces.common.FlickerComponentName.Companion.LAUNCHER import org.junit.Test /** @@ -78,7 +78,7 @@ abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition } /** - * Checks that [pipApp] and [LAUNCHER_COMPONENT] layers are visible at the start + * Checks that [pipApp] and [LAUNCHER] layers are visible at the start * of the transition. Then [pipApp] layer becomes invisible, and remains invisible * until the end of the transition */ @@ -87,10 +87,10 @@ abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition open fun pipLayerBecomesInvisible() { testSpec.assertLayers { this.isVisible(pipApp.component) - .isVisible(LAUNCHER_COMPONENT) + .isVisible(LAUNCHER) .then() .isInvisible(pipApp.component) - .isVisible(LAUNCHER_COMPONENT) + .isVisible(LAUNCHER) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt index 46424a768408..32022ad5f018 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt @@ -16,8 +16,8 @@ package com.android.wm.shell.flicker.pip -import android.view.Surface import android.platform.test.annotations.FlakyTest +import android.view.Surface import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -74,7 +74,9 @@ class ExitPipViaExpandButtonClickTest( // This will bring PipApp to fullscreen pipApp.expandPipWindowToApp(wmHelper) // Wait until the other app is no longer visible - wmHelper.waitForWindowSurfaceDisappeared(testApp.component) + wmHelper.StateSyncBuilder() + .withWindowSurfaceDisappeared(testApp.component) + .waitForAndVerify() } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt index 18711d8fd3c6..3fbea488d091 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt @@ -16,9 +16,9 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -73,7 +73,9 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit // This will bring PipApp to fullscreen pipApp.exitPipToFullScreenViaIntent(wmHelper) // Wait until the other app is no longer visible - wmHelper.waitForWindowSurfaceDisappeared(testApp.component) + wmHelper.StateSyncBuilder() + .withWindowSurfaceDisappeared(testApp.component) + .waitForAndVerify() } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt index 9aae7f324269..210b1968b2da 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt @@ -16,9 +16,9 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -64,9 +64,12 @@ class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransiti val pipCenterY = pipRegion.centerY() val displayCenterX = device.displayWidth / 2 device.swipe(pipCenterX, pipCenterY, displayCenterX, device.displayHeight, 10) - wmHelper.waitPipGone() - wmHelper.waitForWindowSurfaceDisappeared(pipApp.component) - wmHelper.waitForAppTransitionIdle() + // Wait until the other app is no longer visible + wmHelper.StateSyncBuilder() + .withPipGone() + .withWindowSurfaceDisappeared(pipApp.component) + .withAppTransitionIdle() + .waitForAndVerify() } } @@ -108,4 +111,4 @@ class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransiti repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt index ffbb89ee8f6b..dc1fdde18d1b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt @@ -23,9 +23,9 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.traces.common.FlickerComponentName.Companion.LAUNCHER import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -147,13 +147,13 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition } /** - * Checks [pipApp] layer remains visible throughout the animation + * Checks [LAUNCHER] layer remains visible throughout the animation */ @Presubmit @Test fun launcherIsAlwaysVisible() { testSpec.assertLayers { - isVisible(LAUNCHER_COMPONENT) + isVisible(LAUNCHER) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt index 8d542c8ec9e6..fe5dd8b83cfb 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder @@ -41,6 +42,7 @@ import org.junit.Test abstract class PipTransition(protected val testSpec: FlickerTestParameter) { protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + protected val tapl = LauncherInstrumentation() protected val pipApp = PipAppHelper(instrumentation) protected val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation) protected val broadcastActionTrigger = BroadcastActionTrigger(instrumentation) @@ -117,13 +119,11 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { test { if (!eachRun) { pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) - wmHelper.waitPipShown() } } eachRun { if (eachRun) { pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) - wmHelper.waitPipShown() } } } @@ -171,4 +171,4 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { @Presubmit @Test open fun entireScreenCovered() = testSpec.entireScreenCovered() -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt index e44cf3866a09..83d3fe260928 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt @@ -16,18 +16,18 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE import com.android.wm.shell.flicker.testapp.Components @@ -66,11 +66,12 @@ open class SetRequestedOrientationWhilePinnedTest( EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())) // Enter PiP. broadcastActionTrigger.doAction(Components.PipActivity.ACTION_ENTER_PIP) - wmHelper.waitPipShown() - wmHelper.waitForRotation(Surface.ROTATION_0) - wmHelper.waitForAppTransitionIdle() // System bar may fade out during fixed rotation. - wmHelper.waitForNavBarStatusBarVisible() + wmHelper.StateSyncBuilder() + .withPipShown() + .withRotation(Surface.ROTATION_0) + .withNavBarStatusBarVisible() + .waitForAndVerify() } } teardown { @@ -85,11 +86,13 @@ open class SetRequestedOrientationWhilePinnedTest( transitions { // Launch the activity back into fullscreen and ensure that it is now in landscape pipApp.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(pipApp.component) - wmHelper.waitForRotation(Surface.ROTATION_90) - wmHelper.waitForAppTransitionIdle() // System bar may fade out during fixed rotation. - wmHelper.waitForNavBarStatusBarVisible() + wmHelper.StateSyncBuilder() + .withFullScreenApp(pipApp.component) + .withRotation(Surface.ROTATION_90) + .withAppTransitionIdle() + .withNavBarStatusBarVisible() + .waitForAndVerify() } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt index 49094e609fbc..31fb16ffbd3e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt @@ -43,7 +43,7 @@ class TvPipBasicTest( // Set up ratio and enter Pip testApp.clickObject(radioButtonId) - testApp.clickEnterPipButton() + testApp.clickEnterPipButton(wmHelper) val actualRatio: Float = testApp.ui?.visibleBounds?.ratio ?: fail("Application UI not found") @@ -84,4 +84,4 @@ class TvPipBasicTest( ) } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt index 061218a015e4..4be19d61278b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt @@ -244,7 +244,7 @@ class TvPipMenuTests : TvPipTestBase() { } private fun enterPip_openMenu_assertShown(): UiObject2 { - testApp.clickEnterPipButton() + testApp.clickEnterPipButton(wmHelper) // Pressing the Window key should bring up Pip menu uiDevice.pressWindowKey() return uiDevice.waitForTvPipMenu() ?: fail("Pip menu should have been shown") @@ -256,4 +256,4 @@ class TvPipMenuTests : TvPipTestBase() { uiDevice.findTvPipMenuFullscreenButton() ?: fail("\"Full screen\" button should be shown in Pip menu") } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt index bcf38d340867..134e97bd46e7 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt @@ -56,7 +56,7 @@ class TvPipNotificationTests : TvPipTestBase() { @Test fun pipNotification_postedAndDismissed() { testApp.launchViaIntent() - testApp.clickEnterPipButton() + testApp.clickEnterPipButton(wmHelper) assertNotNull("Pip notification should have been posted", waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) }) @@ -70,7 +70,7 @@ class TvPipNotificationTests : TvPipTestBase() { @Test fun pipNotification_closeIntent() { testApp.launchViaIntent() - testApp.clickEnterPipButton() + testApp.clickEnterPipButton(wmHelper) val notification: StatusBarNotification = waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) @@ -87,8 +87,8 @@ class TvPipNotificationTests : TvPipTestBase() { @Test fun pipNotification_menuIntent() { - testApp.launchViaIntent() - testApp.clickEnterPipButton() + testApp.launchViaIntent(wmHelper) + testApp.clickEnterPipButton(wmHelper) val notification: StatusBarNotification = waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) @@ -106,10 +106,10 @@ class TvPipNotificationTests : TvPipTestBase() { @Test fun pipNotification_mediaSessionTitle_isDisplayed() { - testApp.launchViaIntent() + testApp.launchViaIntent(wmHelper) // Start media session and to PiP testApp.clickStartMediaSessionButton() - testApp.clickEnterPipButton() + testApp.clickEnterPipButton(wmHelper) // Wait for the correct notification to show up... waitForNotificationToAppear { @@ -170,4 +170,4 @@ private val StatusBarNotification.deleteIntent: PendingIntent? get() = tvExtensions?.getParcelable("delete_intent") private fun StatusBarNotification.isPipNotificationWithTitle(expectedTitle: String): Boolean = - tag == "TvPip" && title == expectedTitle
\ No newline at end of file + tag == "TvPip" && title == expectedTitle diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt index 9c3b0fa183b6..a97994e7d6c3 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt @@ -23,6 +23,7 @@ import android.os.SystemClock import android.view.Surface.ROTATION_0 import android.view.Surface.rotationToString import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME import com.android.wm.shell.flicker.pip.PipTestBase import org.junit.After @@ -33,6 +34,7 @@ import org.junit.Before abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATION_0) { private val systemUiProcessObserver = SystemUiProcessObserver() + protected val wmHelper = WindowManagerStateHelper() @Before final override fun televisionSetUp() { @@ -88,4 +90,4 @@ abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATIO companion object { private const val AFTER_TEXT_PROCESS_CHECK_DELAY = 1_000L // 1 sec } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt index 702710caded7..cfba3387825c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt @@ -56,7 +56,7 @@ class EnterSplitScreenByDragFromAllApps( ) : SplitScreenBase(testSpec) { @Before - open fun before() { + fun before() { Assume.assumeTrue(taplInstrumentation.isTablet) } @@ -73,8 +73,7 @@ class EnterSplitScreenByDragFromAllApps( taplInstrumentation.launchedAppState.taskbar .openAllApps() .getAppIcon(secondaryApp.appName) - .dragToSplitscreen(secondaryApp.component.packageName, - primaryApp.component.packageName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) } } diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp index cab3f8414d7b..130b204954b4 100644 --- a/libs/input/SpriteController.cpp +++ b/libs/input/SpriteController.cpp @@ -131,8 +131,9 @@ void SpriteController::doUpdateSprites() { update.state.surfaceHeight = update.state.icon.height(); update.state.surfaceDrawn = false; update.state.surfaceVisible = false; - update.state.surfaceControl = obtainSurface( - update.state.surfaceWidth, update.state.surfaceHeight); + update.state.surfaceControl = + obtainSurface(update.state.surfaceWidth, update.state.surfaceHeight, + update.state.displayId); if (update.state.surfaceControl != NULL) { update.surfaceChanged = surfaceChanged = true; } @@ -168,8 +169,8 @@ void SpriteController::doUpdateSprites() { } } - // If surface is a new one, we have to set right layer stack. - if (update.surfaceChanged || update.state.dirty & DIRTY_DISPLAY_ID) { + // If surface has changed to a new display, we have to reparent it. + if (update.state.dirty & DIRTY_DISPLAY_ID) { t.reparent(update.state.surfaceControl, mParentSurfaceProvider(update.state.displayId)); needApplyTransaction = true; } @@ -330,21 +331,28 @@ void SpriteController::ensureSurfaceComposerClient() { } } -sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height) { +sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height, + int32_t displayId) { ensureSurfaceComposerClient(); - sp<SurfaceControl> surfaceControl = mSurfaceComposerClient->createSurface( - String8("Sprite"), width, height, PIXEL_FORMAT_RGBA_8888, - ISurfaceComposerClient::eHidden | - ISurfaceComposerClient::eCursorWindow); - if (surfaceControl == NULL || !surfaceControl->isValid()) { + const sp<SurfaceControl> parent = mParentSurfaceProvider(displayId); + if (parent == nullptr) { + ALOGE("Failed to get the parent surface for pointers on display %d", displayId); + } + + const sp<SurfaceControl> surfaceControl = + mSurfaceComposerClient->createSurface(String8("Sprite"), width, height, + PIXEL_FORMAT_RGBA_8888, + ISurfaceComposerClient::eHidden | + ISurfaceComposerClient::eCursorWindow, + parent ? parent->getHandle() : nullptr); + if (surfaceControl == nullptr || !surfaceControl->isValid()) { ALOGE("Error creating sprite surface."); - return NULL; + return nullptr; } return surfaceControl; } - // --- SpriteController::SpriteImpl --- SpriteController::SpriteImpl::SpriteImpl(const sp<SpriteController> controller) : diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h index 2e9cb9685c46..1f113c045360 100644 --- a/libs/input/SpriteController.h +++ b/libs/input/SpriteController.h @@ -265,7 +265,7 @@ private: void doDisposeSurfaces(); void ensureSurfaceComposerClient(); - sp<SurfaceControl> obtainSurface(int32_t width, int32_t height); + sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, int32_t displayId); }; } // namespace android diff --git a/libs/protoutil/Android.bp b/libs/protoutil/Android.bp index 132d71eb6db8..128be3c33ac5 100644 --- a/libs/protoutil/Android.bp +++ b/libs/protoutil/Android.bp @@ -55,6 +55,7 @@ cc_library { export_include_dirs: ["include"], + min_sdk_version: "30", apex_available: [ "//apex_available:platform", "com.android.os.statsd", @@ -81,5 +82,5 @@ cc_test { proto: { type: "full", - } + }, } diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl index b136d5bc4db3..2bdd5c8bc977 100644 --- a/media/java/android/media/projection/IMediaProjection.aidl +++ b/media/java/android/media/projection/IMediaProjection.aidl @@ -17,7 +17,7 @@ package android.media.projection; import android.media.projection.IMediaProjectionCallback; -import android.window.WindowContainerToken; +import android.os.IBinder; /** {@hide} */ interface IMediaProjection { @@ -31,14 +31,14 @@ interface IMediaProjection { void unregisterCallback(IMediaProjectionCallback callback); /** - * Returns the {@link android.window.WindowContainerToken} identifying the task to record, or - * {@code null} if there is none. + * Returns the {@link android.os.IBinder} identifying the task to record, or {@code null} if + * there is none. */ - WindowContainerToken getTaskRecordingWindowContainerToken(); + IBinder getLaunchCookie(); /** - * Updates the {@link android.window.WindowContainerToken} identifying the task to record, or - * {@code null} if there is none. + * Updates the {@link android.os.IBinder} identifying the task to record, or {@code null} if + * there is none. */ - void setTaskRecordingWindowContainerToken(in WindowContainerToken token); + void setLaunchCookie(in IBinder launchCookie); } diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index ba7bf3f5f5d3..ae44fc575f7c 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -25,13 +25,13 @@ import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.util.ArrayMap; import android.util.Log; import android.view.ContentRecordingSession; import android.view.Surface; -import android.window.WindowContainerToken; import java.util.Map; @@ -172,18 +172,16 @@ public final class MediaProjection { @NonNull VirtualDisplayConfig.Builder virtualDisplayConfig, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { try { - final WindowContainerToken taskWindowContainerToken = - mImpl.getTaskRecordingWindowContainerToken(); + final IBinder launchCookie = mImpl.getLaunchCookie(); Context windowContext = null; ContentRecordingSession session; - if (taskWindowContainerToken == null) { + if (launchCookie == null) { windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(), TYPE_APPLICATION, null /* options */); session = ContentRecordingSession.createDisplaySession( windowContext.getWindowContextToken()); } else { - session = ContentRecordingSession.createTaskSession( - taskWindowContainerToken.asBinder()); + session = ContentRecordingSession.createTaskSession(launchCookie); } virtualDisplayConfig.setWindowManagerMirroring(true); final DisplayManager dm = mContext.getSystemService(DisplayManager.class); diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index 88ca30ceb8a6..c0261f203604 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -1613,7 +1613,9 @@ public final class TvInteractiveAppManager { mHandler.post(new Runnable() { @Override public void run() { - mSession.getInputSession().requestBroadcastInfo(request); + if (mSession.getInputSession() != null) { + mSession.getInputSession().requestBroadcastInfo(request); + } } }); } @@ -1622,7 +1624,9 @@ public final class TvInteractiveAppManager { mHandler.post(new Runnable() { @Override public void run() { - mSession.getInputSession().removeBroadcastInfo(requestId); + if (mSession.getInputSession() != null) { + mSession.getInputSession().removeBroadcastInfo(requestId); + } } }); } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index 6b9daa35f9ca..cf23a5446f91 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -48,7 +48,6 @@ import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.text.format.Formatter; -import android.util.IconDrawableFactory; import android.util.Log; import android.util.SparseArray; @@ -116,7 +115,6 @@ public class ApplicationsState { final Context mContext; final PackageManager mPm; - final IconDrawableFactory mDrawableFactory; final IPackageManager mIpm; final UserManager mUm; final StorageStatsManager mStats; @@ -194,7 +192,6 @@ public class ApplicationsState { private ApplicationsState(Application app, IPackageManager iPackageManager) { mContext = app; mPm = mContext.getPackageManager(); - mDrawableFactory = IconDrawableFactory.newInstance(mContext); mIpm = iPackageManager; mUm = mContext.getSystemService(UserManager.class); mStats = mContext.getSystemService(StorageStatsManager.class); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index 89e10c4b5e11..fc70ba40fb47 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -20,15 +20,19 @@ import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; +import android.os.Build; import android.os.ParcelUuid; import android.util.Log; +import androidx.annotation.ChecksSdkIntAtLeast; + import com.android.internal.annotations.VisibleForTesting; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * CsipDeviceManager manages the set of remote CSIP Bluetooth devices. @@ -126,32 +130,84 @@ public class CsipDeviceManager { } } + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) + private static boolean isAtLeastT() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; + } + // Group devices by groupId @VisibleForTesting void onGroupIdChanged(int groupId) { - int firstMatchedIndex = -1; - CachedBluetoothDevice mainDevice = null; + if (!isValidGroupId(groupId)) { + log("onGroupIdChanged: groupId is invalid"); + return; + } + log("onGroupIdChanged: mCachedDevices list =" + mCachedDevices.toString()); + final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); + final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager(); + final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile(); + final BluetoothDevice mainBluetoothDevice = (leAudioProfile != null && isAtLeastT()) ? + leAudioProfile.getConnectedGroupLeadDevice(groupId) : null; + CachedBluetoothDevice newMainDevice = + mainBluetoothDevice != null ? deviceManager.findDevice(mainBluetoothDevice) : null; + if (newMainDevice != null) { + final CachedBluetoothDevice finalNewMainDevice = newMainDevice; + final List<CachedBluetoothDevice> memberDevices = mCachedDevices.stream() + .filter(cachedDevice -> !cachedDevice.equals(finalNewMainDevice) + && cachedDevice.getGroupId() == groupId) + .collect(Collectors.toList()); + if (memberDevices == null || memberDevices.isEmpty()) { + log("onGroupIdChanged: There is no member device in list."); + return; + } + log("onGroupIdChanged: removed from UI device =" + memberDevices + + ", with groupId=" + groupId + " mainDevice= " + newMainDevice); + for (CachedBluetoothDevice memberDeviceItem : memberDevices) { + Set<CachedBluetoothDevice> memberSet = memberDeviceItem.getMemberDevice(); + if (!memberSet.isEmpty()) { + log("onGroupIdChanged: Transfer the member list into new main device."); + for (CachedBluetoothDevice memberListItem : memberSet) { + if (!memberListItem.equals(newMainDevice)) { + newMainDevice.addMemberDevice(memberListItem); + } + } + memberSet.clear(); + } - for (int i = mCachedDevices.size() - 1; i >= 0; i--) { - final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); - if (cachedDevice.getGroupId() != groupId) { - continue; + newMainDevice.addMemberDevice(memberDeviceItem); + mCachedDevices.remove(memberDeviceItem); + mBtManager.getEventManager().dispatchDeviceRemoved(memberDeviceItem); } - if (firstMatchedIndex == -1) { - // Found the first one - firstMatchedIndex = i; - mainDevice = cachedDevice; - continue; + if (!mCachedDevices.contains(newMainDevice)) { + mCachedDevices.add(newMainDevice); + mBtManager.getEventManager().dispatchDeviceAdded(newMainDevice); } + } else { + log("onGroupIdChanged: There is no main device from the LE profile."); + int firstMatchedIndex = -1; - log("onGroupIdChanged: removed from UI device =" + cachedDevice - + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex); + for (int i = mCachedDevices.size() - 1; i >= 0; i--) { + final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); + if (cachedDevice.getGroupId() != groupId) { + continue; + } + + if (firstMatchedIndex == -1) { + // Found the first one + firstMatchedIndex = i; + newMainDevice = cachedDevice; + continue; + } + + log("onGroupIdChanged: removed from UI device =" + cachedDevice + + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex); - mainDevice.addMemberDevice(cachedDevice); - mCachedDevices.remove(i); - mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); - break; + newMainDevice.addMemberDevice(cachedDevice); + mCachedDevices.remove(i); + mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); + break; + } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index 19df1e9c0730..0f57d8785de9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -21,6 +21,7 @@ import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; +import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCodecConfig; @@ -183,6 +184,37 @@ public class LeAudioProfile implements LocalBluetoothProfile { return mBluetoothAdapter.getActiveDevices(BluetoothProfile.LE_AUDIO); } + /** + * Get Lead device for the group. + * + * Lead device is the device that can be used as an active device in the system. + * Active devices points to the Audio Device for the Le Audio group. + * This method returns the Lead devices for the connected LE Audio + * group and this device should be used in the setActiveDevice() method by other parts + * of the system, which wants to set to active a particular Le Audio group. + * + * Note: getActiveDevice() returns the Lead device for the currently active LE Audio group. + * Note: When Lead device gets disconnected while Le Audio group is active and has more devices + * in the group, then Lead device will not change. If Lead device gets disconnected, for the + * Le Audio group which is not active, a new Lead device will be chosen + * + * @param groupId The group id. + * @return group lead device. + * + * @hide + */ + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) { + if (DEBUG) { + Log.d(TAG,"getConnectedGroupLeadDevice"); + } + if (mService == null) { + Log.e(TAG,"No service."); + return null; + } + return mService.getConnectedGroupLeadDevice(groupId); + } + @Override public boolean isEnabled(BluetoothDevice device) { if (mService == null || device == null) { diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java index 4939e04dad6a..132a631e25cc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java @@ -135,7 +135,7 @@ public class BatteryStatus { * @return true if the device is charged */ public boolean isCharged() { - return status == BATTERY_STATUS_FULL || level >= 100; + return isCharged(status, level); } /** @@ -177,4 +177,31 @@ public class BatteryStatus { return "BatteryStatus{status=" + status + ",level=" + level + ",plugged=" + plugged + ",health=" + health + ",maxChargingWattage=" + maxChargingWattage + "}"; } + + /** + * Whether or not the device is charged. Note that some devices never return 100% for + * battery level, so this allows either battery level or status to determine if the + * battery is charged. + * + * @param batteryChangedIntent ACTION_BATTERY_CHANGED intent + * @return true if the device is charged + */ + public static boolean isCharged(Intent batteryChangedIntent) { + int status = batteryChangedIntent.getIntExtra(EXTRA_STATUS, BATTERY_STATUS_UNKNOWN); + int level = batteryChangedIntent.getIntExtra(EXTRA_LEVEL, 0); + return isCharged(status, level); + } + + /** + * Whether or not the device is charged. Note that some devices never return 100% for + * battery level, so this allows either battery level or status to determine if the + * battery is charged. + * + * @param status values for "status" field in the ACTION_BATTERY_CHANGED Intent + * @param level values from 0 to 100 + * @return true if the device is charged + */ + public static boolean isCharged(int status, int level) { + return status == BATTERY_STATUS_FULL || level >= 100; + } } diff --git a/packages/StatementService/OWNERS b/packages/StatementService/OWNERS new file mode 100644 index 000000000000..f0b4ce7bf5b1 --- /dev/null +++ b/packages/StatementService/OWNERS @@ -0,0 +1,2 @@ +include /PACKAGE_MANAGER_OWNERS + diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt index 0ec8ed3416b8..acb54f6093de 100644 --- a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt +++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt @@ -67,6 +67,10 @@ class DomainVerificationReceiverV1 : BaseDomainVerificationReceiver() { } } + //clear sp before enqueue unique work since policy is REPLACE + val deContext = context.createDeviceProtectedStorageContext() + val editor = deContext?.getSharedPreferences(packageName, Context.MODE_PRIVATE)?.edit() + editor?.clear()?.apply() WorkManager.getInstance(context) .beginUniqueWork( "$PACKAGE_WORK_PREFIX_V1$packageName", diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt index 3a3aea9288cd..36c81722b5d9 100644 --- a/packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt +++ b/packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt @@ -41,9 +41,7 @@ class CollectV1Worker(appContext: Context, params: WorkerParameters) : Data.Builder() .putInt(VERIFICATION_ID_KEY, verificationId) .apply { - if (DEBUG) { - putString(PACKAGE_NAME_KEY, packageName) - } + putString(PACKAGE_NAME_KEY, packageName) } .build() ) @@ -52,6 +50,18 @@ class CollectV1Worker(appContext: Context, params: WorkerParameters) : override suspend fun doWork() = coroutineScope { if (!AndroidUtils.isReceiverV1Enabled(appContext)) { + //clear sp and commit here + val inputData = params.inputData + val packageName = inputData.getString(PACKAGE_NAME_KEY) + val deContext = appContext.createDeviceProtectedStorageContext() + val sp = deContext?.getSharedPreferences(packageName, Context.MODE_PRIVATE) + val editor = sp?.edit() + editor?.clear()?.commit() + //delete sp file + val retOfDel = deContext?.deleteSharedPreferences(packageName) + if (DEBUG) { + Log.d(TAG, "delete sp for $packageName return $retOfDel") + } return@coroutineScope Result.success() } @@ -59,7 +69,10 @@ class CollectV1Worker(appContext: Context, params: WorkerParameters) : val verificationId = inputData.getInt(VERIFICATION_ID_KEY, -1) val successfulHosts = mutableListOf<String>() val failedHosts = mutableListOf<String>() - inputData.keyValueMap.entries.forEach { (key, _) -> + val packageName = inputData.getString(PACKAGE_NAME_KEY) + val deContext = appContext.createDeviceProtectedStorageContext() + val sp = deContext?.getSharedPreferences(packageName, Context.MODE_PRIVATE) + sp?.all?.entries?.forEach { (key, _) -> when { key.startsWith(SingleV1RequestWorker.HOST_SUCCESS_PREFIX) -> successfulHosts += key.removePrefix(SingleV1RequestWorker.HOST_SUCCESS_PREFIX) @@ -69,7 +82,6 @@ class CollectV1Worker(appContext: Context, params: WorkerParameters) : } if (DEBUG) { - val packageName = inputData.getString(PACKAGE_NAME_KEY) Log.d( TAG, "Domain verification v1 request for $packageName: " + "success = $successfulHosts, failed = $failedHosts" @@ -84,6 +96,15 @@ class CollectV1Worker(appContext: Context, params: WorkerParameters) : appContext.packageManager.verifyIntentFilter(verificationId, resultCode, failedHosts) + //clear sp and commit here + val editor = sp?.edit() + editor?.clear()?.commit() + //delete sp file + val retOfDel = deContext?.deleteSharedPreferences(packageName) + if (DEBUG) { + Log.d(TAG, "delete sp for $packageName return $retOfDel") + } + Result.success() } } diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt index cd8a18218004..7a198cb59ca4 100644 --- a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt +++ b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt @@ -71,16 +71,18 @@ class SingleV1RequestWorker(appContext: Context, params: WorkerParameters) : // Coerce failure results into success so that final collection task gets a chance to run when (result) { - is Result.Success -> Result.success( - Data.Builder() - .putInt("$HOST_SUCCESS_PREFIX$host", status.value) - .build() - ) - is Result.Failure -> Result.success( - Data.Builder() - .putInt("$HOST_FAILURE_PREFIX$host", status.value) - .build() - ) + is Result.Success -> { + val deContext = appContext.createDeviceProtectedStorageContext() + val sp = deContext?.getSharedPreferences(packageName, Context.MODE_PRIVATE) + sp?.edit()?.putInt("$HOST_SUCCESS_PREFIX$host", status.value)?.apply() + Result.success() + } + is Result.Failure -> { + val deContext = appContext.createDeviceProtectedStorageContext() + val sp = deContext?.getSharedPreferences(packageName, Context.MODE_PRIVATE) + sp?.edit()?.putInt("$HOST_FAILURE_PREFIX$host", status.value)?.apply() + Result.success() + } else -> result } } diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index de9e1f4ccec5..bae7064f9b0f 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -93,6 +93,7 @@ android_library { "SystemUISharedLib", "SystemUI-statsd", "SettingsLib", + "androidx.core_core-ktx", "androidx.viewpager2_viewpager2", "androidx.legacy_legacy-support-v4", "androidx.recyclerview_recyclerview", diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING index 3bd6d51eecc6..f9a9ef65cab6 100644 --- a/packages/SystemUI/TEST_MAPPING +++ b/packages/SystemUI/TEST_MAPPING @@ -52,6 +52,17 @@ ] }, { + "name": "SystemUIGoogleScreenshotTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { // Permission indicators "name": "CtsPermission4TestCases", "options": [ diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 5b47ae525919..fbe33565f11d 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -656,7 +656,7 @@ class ActivityLaunchAnimator( controller.onLaunchAnimationCancelled() } - override fun onAnimationCancelled() { + override fun onAnimationCancelled(isKeyguardOccluded: Boolean) { if (timedOut) { return } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/ExampleFeature.kt b/packages/SystemUI/compose/features/src/com/android/systemui/ExampleFeature.kt index 1ad54846fb25..c58c16259abe 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/ExampleFeature.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/ExampleFeature.kt @@ -17,8 +17,12 @@ package com.android.systemui import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -29,14 +33,37 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt /** * This is an example Compose feature, which shows a text and a count that is incremented when - * clicked. + * clicked. We also show the max width available to this component, which is displayed either next + * to or below the text depending on that max width. */ @Composable fun ExampleFeature(text: String, modifier: Modifier = Modifier) { + BoxWithConstraints(modifier) { + val maxWidth = maxWidth + if (maxWidth < 600.dp) { + Column { + CounterTile(text) + Spacer(Modifier.size(16.dp)) + MaxWidthTile(maxWidth) + } + } else { + Row { + CounterTile(text) + Spacer(Modifier.size(16.dp)) + MaxWidthTile(maxWidth) + } + } + } +} + +@Composable +private fun CounterTile(text: String, modifier: Modifier = Modifier) { Surface( modifier, color = MaterialTheme.colorScheme.primaryContainer, @@ -51,3 +78,17 @@ fun ExampleFeature(text: String, modifier: Modifier = Modifier) { } } } + +@Composable +private fun MaxWidthTile(maxWidth: Dp, modifier: Modifier = Modifier) { + Surface( + modifier, + color = MaterialTheme.colorScheme.tertiaryContainer, + shape = RoundedCornerShape(28.dp), + ) { + Text( + "The max available width to me is: ${maxWidth.value.roundToInt()}dp", + Modifier.padding(16.dp) + ) + } +} diff --git a/packages/SystemUI/compose/gallery/Android.bp b/packages/SystemUI/compose/gallery/Android.bp index 030ad854992a..bddc1e63fee8 100644 --- a/packages/SystemUI/compose/gallery/Android.bp +++ b/packages/SystemUI/compose/gallery/Android.bp @@ -52,6 +52,7 @@ android_library { android_app { name: "SystemUIComposeGallery", defaults: ["platform_app_defaults"], + manifest: "app/AndroidManifest.xml", static_libs: [ "SystemUIComposeGalleryLib", diff --git a/packages/SystemUI/compose/gallery/AndroidManifest.xml b/packages/SystemUI/compose/gallery/AndroidManifest.xml index 81132632079a..562e1cc3339a 100644 --- a/packages/SystemUI/compose/gallery/AndroidManifest.xml +++ b/packages/SystemUI/compose/gallery/AndroidManifest.xml @@ -17,21 +17,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.systemui.compose.gallery"> - <application - android:allowBackup="true" - android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" - android:roundIcon="@mipmap/ic_launcher_round" - android:supportsRtl="true" - android:theme="@style/Theme.SystemUIGallery"> - <activity - android:name=".GalleryActivity" - android:exported="true" - android:label="@string/app_name"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> - </activity> - </application> + </manifest> diff --git a/packages/SystemUI/compose/gallery/app/AndroidManifest.xml b/packages/SystemUI/compose/gallery/app/AndroidManifest.xml new file mode 100644 index 000000000000..81132632079a --- /dev/null +++ b/packages/SystemUI/compose/gallery/app/AndroidManifest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.systemui.compose.gallery"> + <application + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/Theme.SystemUIGallery"> + <activity + android:name=".GalleryActivity" + android:exported="true" + android:label="@string/app_name"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt index 652c403470f1..6e1721490f98 100644 --- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt +++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt @@ -18,10 +18,11 @@ package com.android.systemui.compose.gallery import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.android.systemui.ExampleFeature /** The screen that shows ExampleFeature. */ @Composable -fun ExampleFeatureScreen() { - Column { ExampleFeature("This is an example feature!") } +fun ExampleFeatureScreen(modifier: Modifier = Modifier) { + Column(modifier) { ExampleFeature("This is an example feature!") } } diff --git a/packages/SystemUI/compose/testing/Android.bp b/packages/SystemUI/compose/testing/Android.bp new file mode 100644 index 000000000000..293e51f68b98 --- /dev/null +++ b/packages/SystemUI/compose/testing/Android.bp @@ -0,0 +1,43 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + name: "SystemUIComposeTesting", + manifest: "AndroidManifest.xml", + + srcs: [ + "src/**/*.kt", + ], + + static_libs: [ + "SystemUIComposeCore", + "SystemUIScreenshotLib", + + "androidx.compose.runtime_runtime", + "androidx.compose.material3_material3", + "androidx.compose.ui_ui-test-junit4", + "androidx.compose.ui_ui-test-manifest", + ], + + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SystemUI/compose/testing/AndroidManifest.xml b/packages/SystemUI/compose/testing/AndroidManifest.xml new file mode 100644 index 000000000000..b1f7c3be2796 --- /dev/null +++ b/packages/SystemUI/compose/testing/AndroidManifest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.systemui.testing.compose"> + <application + android:appComponentFactory="androidx.core.app.AppComponentFactory" + tools:replace="android:appComponentFactory"> + </application> +</manifest> diff --git a/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt b/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt new file mode 100644 index 000000000000..7a0b1f190f99 --- /dev/null +++ b/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.testing.compose + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ViewRootForTest +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import com.android.systemui.compose.theme.SystemUITheme +import com.android.systemui.testing.screenshot.ScreenshotActivity +import com.android.systemui.testing.screenshot.ScreenshotTestRule +import com.android.systemui.testing.screenshot.ScreenshotTestSpec +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** A rule for Compose screenshot diff tests. */ +class ComposeScreenshotTestRule(testSpec: ScreenshotTestSpec) : TestRule { + private val composeRule = createAndroidComposeRule<ScreenshotActivity>() + private val screenshotRule = ScreenshotTestRule(testSpec) + + private val delegate = RuleChain.outerRule(screenshotRule).around(composeRule) + + override fun apply(base: Statement, description: Description): Statement { + return delegate.apply(base, description) + } + + /** + * Compare [content] with the golden image identified by [goldenIdentifier] in the context of + * [testSpec]. + */ + fun screenshotTest( + goldenIdentifier: String, + content: @Composable () -> Unit, + ) { + // Make sure that the activity draws full screen and fits the whole display instead of the + // system bars. + val activity = composeRule.activity + activity.mainExecutor.execute { activity.window.setDecorFitsSystemWindows(false) } + + // Set the content using the AndroidComposeRule to make sure that the Activity is set up + // correctly. + composeRule.setContent { + SystemUITheme { + Surface( + color = MaterialTheme.colorScheme.background, + ) { + content() + } + } + } + composeRule.waitForIdle() + + val view = (composeRule.onRoot().fetchSemanticsNode().root as ViewRootForTest).view + screenshotRule.screenshotTest(goldenIdentifier, view) + } +} diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml index 5e8b892018eb..2b3d11b0e191 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml @@ -14,20 +14,19 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<!-- TODO(b/203800646): layout_marginTop doesn't seem to work on some large screens. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/media_ttt_receiver_chip" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@drawable/media_ttt_chip_background_receiver" > <com.android.internal.widget.CachingIconView android:id="@+id/app_icon" android:layout_width="@dimen/media_ttt_icon_size_receiver" android:layout_height="@dimen/media_ttt_icon_size_receiver" - android:layout_gravity="center" + android:layout_gravity="center|bottom" + android:alpha="0.0" /> </FrameLayout> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 1210b79d3ff5..009a123de40c 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1051,6 +1051,7 @@ <!-- Since the generic icon isn't circular, we need to scale it down so it still fits within the circular chip. --> <dimen name="media_ttt_generic_icon_size_receiver">70dp</dimen> + <dimen name="media_ttt_receiver_vert_translation">20dp</dimen> <!-- Window magnification --> <dimen name="magnification_border_drag_size">35dp</dimen> diff --git a/packages/SystemUI/screenshot/Android.bp b/packages/SystemUI/screenshot/Android.bp new file mode 100644 index 000000000000..a79fd9040db3 --- /dev/null +++ b/packages/SystemUI/screenshot/Android.bp @@ -0,0 +1,48 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + name: "SystemUIScreenshotLib", + manifest: "AndroidManifest.xml", + + srcs: [ + // All files in this library should be in Kotlin besides some exceptions. + "src/**/*.kt", + + // This file was forked from google3, so exceptionally it can be in Java. + "src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java", + ], + + resource_dirs: [ + "res", + ], + + static_libs: [ + "SystemUI-core", + "androidx.test.espresso.core", + "androidx.appcompat_appcompat", + "platform-screenshot-diff-core", + ], + + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SystemUI/screenshot/AndroidManifest.xml b/packages/SystemUI/screenshot/AndroidManifest.xml new file mode 100644 index 000000000000..3b703be34e5d --- /dev/null +++ b/packages/SystemUI/screenshot/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.systemui.testing.screenshot"> + <application> + <activity + android:name="com.android.systemui.testing.screenshot.ScreenshotActivity" + android:exported="true" + android:theme="@style/Theme.SystemUI.Screenshot" /> + </application> + + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> +</manifest> diff --git a/packages/SystemUI/screenshot/res/values/themes.xml b/packages/SystemUI/screenshot/res/values/themes.xml new file mode 100644 index 000000000000..40e50bbb6bbf --- /dev/null +++ b/packages/SystemUI/screenshot/res/values/themes.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <style name="Theme.SystemUI.Screenshot" parent="Theme.SystemUI"> + <item name="android:windowActionBar">false</item> + <item name="android:windowNoTitle">true</item> + + <!-- Make sure that device specific cutouts don't impact the outcome of screenshot tests --> + <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java new file mode 100644 index 000000000000..96ec4c543474 --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.testing.screenshot; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import android.app.UiAutomation; +import android.content.Context; +import android.provider.Settings; +import android.util.Log; + +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; +import androidx.test.espresso.Espresso; +import androidx.test.espresso.IdlingRegistry; +import androidx.test.espresso.IdlingResource; + +import org.json.JSONObject; +import org.junit.function.ThrowingRunnable; + +import java.util.HashMap; +import java.util.Map; + +/* + * Note: This file was forked from + * google3/third_party/java_src/android_libs/material_components/screenshot_tests/java/android/ + * support/design/scuba/color/DynamicColorsTestUtils.java. + */ + +/** Utility that helps change the dynamic system colors for testing. */ +@RequiresApi(32) +public class DynamicColorsTestUtils { + + private static final String TAG = DynamicColorsTestUtils.class.getSimpleName(); + + private static final String THEME_CUSTOMIZATION_KEY = "theme_customization_overlay_packages"; + private static final String THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY = + "android.theme.customization.system_palette"; + + private static final int ORANGE_SYSTEM_SEED_COLOR = 0xA66800; + private static final int ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR = -8235756; + + private DynamicColorsTestUtils() { + } + + /** + * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on an orange + * seed color, and then wait for the change to propagate to the app by comparing + * android.R.color.system_accent1_600 to the expected orange value. + */ + public static void updateSystemColorsToOrange() { + updateSystemColors(ORANGE_SYSTEM_SEED_COLOR, ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR); + } + + /** + * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided + * {@code seedColor}, and then wait for the change to propagate to the app by comparing + * android.R.color.system_accent1_600 to {@code expectedSystemAccent1600}. + */ + public static void updateSystemColors( + @ColorInt int seedColor, @ColorInt int expectedSystemAccent1600) { + Context context = getInstrumentation().getTargetContext(); + + int actualSystemAccent1600 = + ContextCompat.getColor(context, android.R.color.system_accent1_600); + + if (expectedSystemAccent1600 == actualSystemAccent1600) { + String expectedColorString = Integer.toHexString(expectedSystemAccent1600); + Log.d( + TAG, + "Skipped updating system colors since system_accent1_600 is already equal to " + + "expected: " + + expectedColorString); + return; + } + + updateSystemColors(seedColor); + } + + /** + * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided + * {@code seedColor}, and then wait for the change to propagate to the app by checking + * android.R.color.system_accent1_600 for any change. + */ + public static void updateSystemColors(@ColorInt int seedColor) { + Context context = getInstrumentation().getTargetContext(); + + // Initialize system color idling resource with original system_accent1_600 value. + ColorChangeIdlingResource systemColorIdlingResource = + new ColorChangeIdlingResource(context, android.R.color.system_accent1_600); + + // Update system theme color setting to trigger fabricated resource overlay. + runWithShellPermissionIdentity( + () -> + Settings.Secure.putString( + context.getContentResolver(), + THEME_CUSTOMIZATION_KEY, + buildThemeCustomizationString(seedColor))); + + // Wait for system color update to propagate to app. + IdlingRegistry idlingRegistry = IdlingRegistry.getInstance(); + idlingRegistry.register(systemColorIdlingResource); + Espresso.onIdle(); + idlingRegistry.unregister(systemColorIdlingResource); + + Log.d(TAG, + Settings.Secure.getString(context.getContentResolver(), THEME_CUSTOMIZATION_KEY)); + } + + private static String buildThemeCustomizationString(@ColorInt int seedColor) { + String seedColorHex = Integer.toHexString(seedColor); + Map<String, String> themeCustomizationMap = new HashMap<>(); + themeCustomizationMap.put(THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY, seedColorHex); + return new JSONObject(themeCustomizationMap).toString(); + } + + private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable runnable) { + UiAutomation uiAutomation = getInstrumentation().getUiAutomation(); + uiAutomation.adoptShellPermissionIdentity(); + try { + runnable.run(); + } catch (Throwable e) { + throw new RuntimeException(e); + } finally { + uiAutomation.dropShellPermissionIdentity(); + } + } + + private static class ColorChangeIdlingResource implements IdlingResource { + + private final Context mContext; + private final int mColorResId; + private final int mInitialColorInt; + + private ResourceCallback mResourceCallback; + private boolean mIdleNow; + + ColorChangeIdlingResource(Context context, @ColorRes int colorResId) { + this.mContext = context; + this.mColorResId = colorResId; + this.mInitialColorInt = ContextCompat.getColor(context, colorResId); + } + + @Override + public String getName() { + return ColorChangeIdlingResource.class.getName(); + } + + @Override + public boolean isIdleNow() { + if (mIdleNow) { + return true; + } + + int currentColorInt = ContextCompat.getColor(mContext, mColorResId); + + String initialColorString = Integer.toHexString(mInitialColorInt); + String currentColorString = Integer.toHexString(currentColorInt); + Log.d(TAG, String.format("Initial=%s, Current=%s", initialColorString, + currentColorString)); + + mIdleNow = currentColorInt != mInitialColorInt; + Log.d(TAG, String.format("idleNow=%b", mIdleNow)); + + if (mIdleNow) { + mResourceCallback.onTransitionToIdle(); + } + return mIdleNow; + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { + this.mResourceCallback = resourceCallback; + } + } +} diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt new file mode 100644 index 000000000000..2a55a80eb7f4 --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.testing.screenshot + +import androidx.activity.ComponentActivity + +/** The Activity that is launched and whose content is set for screenshot tests. */ +class ScreenshotActivity : ComponentActivity() diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt new file mode 100644 index 000000000000..363ce10fa36c --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.testing.screenshot + +import android.app.UiModeManager +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.os.UserHandle +import android.view.Display +import android.view.View +import android.view.WindowManagerGlobal +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import platform.test.screenshot.GoldenImagePathManager +import platform.test.screenshot.PathConfig +import platform.test.screenshot.PathElementNoContext +import platform.test.screenshot.ScreenshotTestRule +import platform.test.screenshot.matchers.PixelPerfectMatcher + +/** + * A base rule for screenshot diff tests. + * + * This rules takes care of setting up the activity according to [testSpec] by: + * - emulating the display size and density. + * - setting the dark/light mode. + * - setting the system (Material You) colors to a fixed value. + * + * @see ComposeScreenshotTestRule + * @see ViewScreenshotTestRule + */ +class ScreenshotTestRule(private val testSpec: ScreenshotTestSpec) : TestRule { + private var currentDisplay: DisplaySpec? = null + private var currentGoldenIdentifier: String? = null + + private val pathConfig = + PathConfig( + PathElementNoContext("model", isDir = true) { + currentDisplay?.name ?: error("currentDisplay is null") + }, + ) + private val defaultMatcher = PixelPerfectMatcher() + + private val screenshotRule = + ScreenshotTestRule( + SystemUIGoldenImagePathManager( + pathConfig, + currentGoldenIdentifier = { + currentGoldenIdentifier ?: error("currentGoldenIdentifier is null") + }, + ) + ) + + override fun apply(base: Statement, description: Description): Statement { + // The statement which call beforeTest() before running the test and afterTest() afterwards. + val statement = + object : Statement() { + override fun evaluate() { + try { + beforeTest() + base.evaluate() + } finally { + afterTest() + } + } + } + + return screenshotRule.apply(statement, description) + } + + private fun beforeTest() { + // Update the system colors to a fixed color, so that tests don't depend on the host device + // extracted colors. Note that we don't restore the default device colors at the end of the + // test because changing the colors (and waiting for them to be applied) is costly and makes + // the screenshot tests noticeably slower. + DynamicColorsTestUtils.updateSystemColorsToOrange() + + // Emulate the display size and density. + val display = testSpec.display + val density = display.densityDpi + val wm = WindowManagerGlobal.getWindowManagerService() + val (width, height) = getEmulatedDisplaySize() + wm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, density, UserHandle.myUserId()) + wm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, width, height) + + // Force the dark/light theme. + val uiModeManager = + InstrumentationRegistry.getInstrumentation() + .targetContext + .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiModeManager.setApplicationNightMode( + if (testSpec.isDarkTheme) { + UiModeManager.MODE_NIGHT_YES + } else { + UiModeManager.MODE_NIGHT_NO + } + ) + } + + private fun afterTest() { + // Reset the density and display size. + val wm = WindowManagerGlobal.getWindowManagerService() + wm.clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, UserHandle.myUserId()) + wm.clearForcedDisplaySize(Display.DEFAULT_DISPLAY) + + // Reset the dark/light theme. + val uiModeManager = + InstrumentationRegistry.getInstrumentation() + .targetContext + .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO) + } + + /** + * Compare the content of [view] with the golden image identified by [goldenIdentifier] in the + * context of [testSpec]. + */ + fun screenshotTest(goldenIdentifier: String, view: View) { + val bitmap = drawIntoBitmap(view) + + // Compare bitmap against golden asset. + val isDarkTheme = testSpec.isDarkTheme + val isLandscape = testSpec.isLandscape + val identifierWithSpec = buildString { + append(goldenIdentifier) + if (isDarkTheme) append("_dark") + if (isLandscape) append("_landscape") + } + + // TODO(b/230832101): Provide a way to pass a PathConfig and override the file name on + // device to assertBitmapAgainstGolden instead? + currentDisplay = testSpec.display + currentGoldenIdentifier = goldenIdentifier + screenshotRule.assertBitmapAgainstGolden(bitmap, identifierWithSpec, defaultMatcher) + currentDisplay = null + currentGoldenIdentifier = goldenIdentifier + } + + /** Draw [view] into a [Bitmap]. */ + private fun drawIntoBitmap(view: View): Bitmap { + val bitmap = + Bitmap.createBitmap( + view.measuredWidth, + view.measuredHeight, + Bitmap.Config.ARGB_8888, + ) + val canvas = Canvas(bitmap) + view.draw(canvas) + return bitmap + } + + /** Get the emulated display size for [testSpec]. */ + private fun getEmulatedDisplaySize(): Pair<Int, Int> { + val display = testSpec.display + val isPortraitNaturalPosition = display.width < display.height + return if (testSpec.isLandscape) { + if (isPortraitNaturalPosition) { + display.height to display.width + } else { + display.width to display.height + } + } else { + if (isPortraitNaturalPosition) { + display.width to display.height + } else { + display.height to display.width + } + } + } +} + +private class SystemUIGoldenImagePathManager( + pathConfig: PathConfig, + private val currentGoldenIdentifier: () -> String, +) : + GoldenImagePathManager( + appContext = InstrumentationRegistry.getInstrumentation().context, + deviceLocalPath = + InstrumentationRegistry.getInstrumentation() + .targetContext + .filesDir + .absolutePath + .toString() + "/sysui_screenshots", + pathConfig = pathConfig, + ) { + // This string is appended to all actual/expected screenshots on the device. We append the + // golden identifier so that our pull_golden.py scripts can map a screenshot on device to its + // asset (and automatically update it, if necessary). + override fun toString() = currentGoldenIdentifier() +} diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt new file mode 100644 index 000000000000..7fc624554738 --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.testing.screenshot + +/** The specification of a device display to be used in a screenshot test. */ +data class DisplaySpec( + val name: String, + val width: Int, + val height: Int, + val densityDpi: Int, +) + +/** The specification of a screenshot diff test. */ +class ScreenshotTestSpec( + val display: DisplaySpec, + val isDarkTheme: Boolean = false, + val isLandscape: Boolean = false, +) { + companion object { + /** + * Return a list of [ScreenshotTestSpec] for each of the [displays]. + * + * If [isDarkTheme] is null, this will create a spec for both light and dark themes, for + * each of the orientation. + * + * If [isLandscape] is null, this will create a spec for both portrait and landscape, for + * each of the light/dark themes. + */ + fun forDisplays( + vararg displays: DisplaySpec, + isDarkTheme: Boolean? = null, + isLandscape: Boolean? = null, + ): List<ScreenshotTestSpec> { + return displays.flatMap { display -> + buildList { + fun addDisplay(isLandscape: Boolean) { + if (isDarkTheme != true) { + add(ScreenshotTestSpec(display, isDarkTheme = false, isLandscape)) + } + + if (isDarkTheme != false) { + add(ScreenshotTestSpec(display, isDarkTheme = true, isLandscape)) + } + } + + if (isLandscape != true) { + addDisplay(isLandscape = false) + } + + if (isLandscape != false) { + addDisplay(isLandscape = true) + } + } + } + } + } + + override fun toString(): String = buildString { + // This string is appended to PNGs stored in the device, so let's keep it simple. + append(display.name) + if (isDarkTheme) append("_dark") + if (isLandscape) append("_landscape") + } +} diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt new file mode 100644 index 000000000000..35812e39677d --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt @@ -0,0 +1,80 @@ +package com.android.systemui.testing.screenshot + +import android.app.Activity +import android.app.Dialog +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams +import androidx.test.ext.junit.rules.ActivityScenarioRule +import org.junit.Assert.assertEquals +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** A rule for View screenshot diff tests. */ +class ViewScreenshotTestRule(testSpec: ScreenshotTestSpec) : TestRule { + private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java) + private val screenshotRule = ScreenshotTestRule(testSpec) + + private val delegate = RuleChain.outerRule(screenshotRule).around(activityRule) + + override fun apply(base: Statement, description: Description): Statement { + return delegate.apply(base, description) + } + + /** + * Compare the content of the view provided by [viewProvider] with the golden image identified + * by [goldenIdentifier] in the context of [testSpec]. + */ + fun screenshotTest( + goldenIdentifier: String, + layoutParams: LayoutParams = + LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT), + viewProvider: (Activity) -> View, + ) { + activityRule.scenario.onActivity { activity -> + // Make sure that the activity draws full screen and fits the whole display instead of + // the system bars. + activity.window.setDecorFitsSystemWindows(false) + activity.setContentView(viewProvider(activity), layoutParams) + } + + // We call onActivity again because it will make sure that our Activity is done measuring, + // laying out and drawing its content (that we set in the previous onActivity lambda). + activityRule.scenario.onActivity { activity -> + // Check that the content is what we expected. + val content = activity.requireViewById<ViewGroup>(android.R.id.content) + assertEquals(1, content.childCount) + screenshotRule.screenshotTest(goldenIdentifier, content.getChildAt(0)) + } + } + + /** + * Compare the content of the dialog provided by [dialogProvider] with the golden image + * identified by [goldenIdentifier] in the context of [testSpec]. + */ + fun dialogScreenshotTest( + goldenIdentifier: String, + dialogProvider: (Activity) -> Dialog, + ) { + var dialog: Dialog? = null + activityRule.scenario.onActivity { activity -> + // Make sure that the dialog draws full screen and fits the whole display instead of the + // system bars. + dialog = + dialogProvider(activity).apply { + window.setDecorFitsSystemWindows(false) + show() + } + } + + // We call onActivity again because it will make sure that our Dialog is done measuring, + // laying out and drawing its content (that we set in the previous onActivity lambda). + activityRule.scenario.onActivity { + // Check that the content is what we expected. + val dialog = dialog ?: error("dialog is null") + screenshotRule.screenshotTest(goldenIdentifier, dialog.window.decorView) + } + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt index 06247c6c9523..835d6e92a63d 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -74,18 +74,27 @@ open class ClockRegistry( open var currentClockId: ClockId get() { - val json = Settings.Secure.getString( - context.contentResolver, - Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE - ) - return gson.fromJson(json, ClockSetting::class.java)?.clockId ?: DEFAULT_CLOCK_ID + return try { + val json = Settings.Secure.getString( + context.contentResolver, + Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE + ) + gson.fromJson(json, ClockSetting::class.java)?.clockId ?: DEFAULT_CLOCK_ID + } catch (ex: Exception) { + Log.e(TAG, "Failed to parse clock setting", ex) + DEFAULT_CLOCK_ID + } } set(value) { - val json = gson.toJson(ClockSetting(value, System.currentTimeMillis())) - Settings.Secure.putString( - context.contentResolver, - Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json - ) + try { + val json = gson.toJson(ClockSetting(value, System.currentTimeMillis())) + Settings.Secure.putString( + context.contentResolver, + Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json + ) + } catch (ex: Exception) { + Log.e(TAG, "Failed to set clock setting", ex) + } } init { @@ -183,6 +192,6 @@ open class ClockRegistry( private data class ClockSetting( val clockId: ClockId, - val _applied_timestamp: Long + val _applied_timestamp: Long? ) } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java index 88fe03465405..203b236fcdd6 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java @@ -80,7 +80,8 @@ public class PipSurfaceTransactionHelper { public PictureInPictureSurfaceTransaction scaleAndCrop( SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets) { + Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets, + float progress) { mTmpSourceRectF.set(sourceBounds); mTmpDestinationRect.set(sourceBounds); mTmpDestinationRect.inset(insets); @@ -93,9 +94,13 @@ public class PipSurfaceTransactionHelper { : (float) destinationBounds.height() / sourceBounds.height(); } else { // scale by sourceRectHint if it's not edge-to-edge - scale = sourceRectHint.width() <= sourceRectHint.height() + final float endScale = sourceRectHint.width() <= sourceRectHint.height() ? (float) destinationBounds.width() / sourceRectHint.width() : (float) destinationBounds.height() / sourceRectHint.height(); + final float startScale = sourceRectHint.width() <= sourceRectHint.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + scale = (1 - progress) * startScale + progress * endScale; } final float left = destinationBounds.left - (insets.left + sourceBounds.left) * scale; final float top = destinationBounds.top - (insets.top + sourceBounds.top) * scale; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java index 302ba8444f05..9265f07ad284 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java @@ -105,7 +105,7 @@ public class RemoteAnimationAdapterCompat { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { remoteAnimationAdapter.onAnimationCancelled(); } }; @@ -220,9 +220,9 @@ public class RemoteAnimationAdapterCompat { for (int i = info.getChanges().size() - 1; i >= 0; --i) { info.getChanges().get(i).getLeash().release(); } - for (int i = leashMap.size() - 1; i >= 0; --i) { - leashMap.valueAt(i).release(); - } + // Don't release here since launcher might still be using them. Instead + // let launcher release them (eg. via RemoteAnimationTargets) + leashMap.clear(); try { finishCallback.onTransitionFinished(null /* wct */, finishTransaction); } catch (RemoteException e) { diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java index 4ce110bf7c47..249133a9a63b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java @@ -141,8 +141,10 @@ public class RemoteAnimationTargetCompat { final int mode = change.getMode(); t.reparent(leash, info.getRootLeash()); - t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x, - change.getStartAbsBounds().top - info.getRootOffset().y); + final Rect absBounds = + (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds(); + t.setPosition(leash, absBounds.left - info.getRootOffset().x, + absBounds.top - info.getRootOffset().y); // Put all the OPEN/SHOW on top if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java index e0b11d83bf75..15b16ff971ea 100644 --- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java +++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java @@ -22,7 +22,9 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Rect; import android.icu.text.NumberFormat; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -30,16 +32,24 @@ import androidx.annotation.VisibleForTesting; import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.AnimatableClockView; +import com.android.systemui.shared.navigationbar.RegionSamplingHelper; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.ViewController; import java.io.PrintWriter; import java.util.Locale; import java.util.Objects; +import java.util.Optional; import java.util.TimeZone; +import java.util.concurrent.Executor; + +import javax.inject.Inject; /** * Controller for an AnimatableClockView on the keyguard. Instantiated by @@ -54,7 +64,10 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final BatteryController mBatteryController; private final int mDozingColor = Color.WHITE; + private Optional<RegionSamplingHelper> mRegionSamplingHelper = Optional.empty(); + private Rect mSamplingBounds = new Rect(); private int mLockScreenColor; + private final boolean mRegionSamplingEnabled; private boolean mIsDozing; private boolean mIsCharging; @@ -67,13 +80,17 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie private final float mBurmeseLineSpacing; private final float mDefaultLineSpacing; + @Inject public AnimatableClockController( AnimatableClockView view, StatusBarStateController statusBarStateController, BroadcastDispatcher broadcastDispatcher, BatteryController batteryController, KeyguardUpdateMonitor keyguardUpdateMonitor, - @Main Resources resources + @Main Resources resources, + @Main Executor mainExecutor, + @Background Executor bgExecutor, + FeatureFlags featureFlags ) { super(view); mStatusBarStateController = statusBarStateController; @@ -86,6 +103,40 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie R.dimen.keyguard_clock_line_spacing_scale_burmese); mDefaultLineSpacing = resources.getFloat( R.dimen.keyguard_clock_line_spacing_scale); + + mRegionSamplingEnabled = featureFlags.isEnabled(Flags.REGION_SAMPLING); + if (!mRegionSamplingEnabled) { + return; + } + + mRegionSamplingHelper = Optional.of(new RegionSamplingHelper(mView, + new RegionSamplingHelper.SamplingCallback() { + @Override + public void onRegionDarknessChanged(boolean isRegionDark) { + if (isRegionDark) { + mLockScreenColor = Color.WHITE; + } else { + mLockScreenColor = Color.BLACK; + } + initColors(); + } + + @Override + public Rect getSampledRegion(View sampledView) { + mSamplingBounds = new Rect(sampledView.getLeft(), sampledView.getTop(), + sampledView.getRight(), sampledView.getBottom()); + return mSamplingBounds; + } + + @Override + public boolean isSamplingEnabled() { + return mRegionSamplingEnabled; + } + }, mainExecutor, bgExecutor) + ); + mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { + regionSamplingHelper.setWindowVisible(true); + }); } private void reset() { @@ -169,6 +220,10 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie mStatusBarStateController.addCallback(mStatusBarStateListener); + mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { + regionSamplingHelper.start(mSamplingBounds); + }); + refreshTime(); initColors(); mView.animateDoze(mIsDozing, false); @@ -180,6 +235,9 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback); mBatteryController.removeCallback(mBatteryCallback); mStatusBarStateController.removeCallback(mStatusBarStateListener); + mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { + regionSamplingHelper.stop(); + }); } /** Animate the clock appearance */ @@ -223,8 +281,10 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie } private void initColors() { - mLockScreenColor = Utils.getColorAttrDefaultColor(getContext(), - com.android.systemui.R.attr.wallpaperTextColorAccent); + if (!mRegionSamplingEnabled) { + mLockScreenColor = Utils.getColorAttrDefaultColor(getContext(), + com.android.systemui.R.attr.wallpaperTextColorAccent); + } mView.setColors(mDozingColor, mLockScreenColor); mView.animateDoze(mIsDozing, false); } @@ -235,5 +295,8 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie public void dump(@NonNull PrintWriter pw) { pw.println(this); mView.dump(pw); + mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { + regionSamplingHelper.dump(pw); + }); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index eeab538932c0..dd78f1cef78c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -263,9 +263,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mCurrentClockSize = clockSize; + Clock clock = getClock(); boolean appeared = mView.switchToClock(clockSize, animate); - if (animate && appeared && clockSize == LARGE) { - getClock().getAnimations().enter(); + if (clock != null && animate && appeared && clockSize == LARGE) { + clock.getAnimations().enter(); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java index ec4cf2fd8bd4..24b893340ae0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java @@ -510,6 +510,7 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud mKeyguardViewManager.isBouncerInTransit() ? BouncerPanelExpansionCalculator .aboutToShowBouncerProgress(fraction) : fraction; updateAlpha(); + updatePauseAuth(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 8de8e020a359..4096ed4283e5 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -46,6 +46,7 @@ import android.content.res.AssetManager; import android.content.res.Resources; import android.hardware.SensorManager; import android.hardware.SensorPrivacyManager; +import android.hardware.camera2.CameraManager; import android.hardware.devicestate.DeviceStateManager; import android.hardware.display.AmbientDisplayConfiguration; import android.hardware.display.ColorDisplayManager; @@ -561,4 +562,10 @@ public class FrameworkServicesModule { static SafetyCenterManager provideSafetyCenterManager(Context context) { return context.getSystemService(SafetyCenterManager.class); } + + @Provides + @Singleton + static CameraManager provideCameraManager(Context context) { + return context.getSystemService(CameraManager.class); + } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 56bb53b567be..2fa104a7ce18 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -162,8 +162,13 @@ public class Flags { new ResourceBooleanFlag(800, R.bool.flag_monet); /***************************************/ + // 801 - region sampling + public static final BooleanFlag REGION_SAMPLING = + new BooleanFlag(801, false); + + /***************************************/ // 900 - media - public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, false); + public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, true); public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, false); public static final BooleanFlag MEDIA_NEARBY_DEVICES = new BooleanFlag(903, true); public static final BooleanFlag MEDIA_MUTE_AWAIT = new BooleanFlag(904, true); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index d71956d054be..95b3b3f6452f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -263,7 +263,7 @@ public class KeyguardService extends Service { // already finished (or not started yet), so do nothing. return; } - runner.onAnimationCancelled(); + runner.onAnimationCancelled(false /* isKeyguardOccluded */); origFinishCB.onTransitionFinished(null /* wct */, null /* t */); } catch (RemoteException e) { // nothing, we'll just let it finish on its own I guess. @@ -396,7 +396,7 @@ public class KeyguardService extends Service { } @Override // Binder interface - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { mKeyguardViewMediator.cancelKeyguardExitAnimation(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index de8b0cfa495a..f1d3653050fe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -843,7 +843,6 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, @Override public void onLaunchAnimationCancelled() { - setOccluded(true /* occluded */, false /* animate */); Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: " + mOccluded); } @@ -911,12 +910,12 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, private final Matrix mUnoccludeMatrix = new Matrix(); @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { if (mUnoccludeAnimator != null) { mUnoccludeAnimator.cancel(); } - setOccluded(false /* isOccluded */, false /* animate */); + setOccluded(isKeyguardOccluded /* isOccluded */, false /* animate */); Log.d(TAG, "Unocclude animation cancelled. Occluded state is now: " + mOccluded); } @@ -3169,9 +3168,9 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } @Override - public void onAnimationCancelled() throws RemoteException { + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { if (mRunner != null) { - mRunner.onAnimationCancelled(); + mRunner.onAnimationCancelled(isKeyguardOccluded); } } @@ -3212,9 +3211,12 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } @Override - public void onAnimationCancelled() throws RemoteException { - super.onAnimationCancelled(); - Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: " + mOccluded); + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { + super.onAnimationCancelled(isKeyguardOccluded); + setOccluded(isKeyguardOccluded /* occluded */, false /* animate */); + + Log.d(TAG, "Occlude animation cancelled by WM. " + + "Setting occluded state to: " + mOccluded); } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt index fe1ac80e24df..0f1cdcc3fa5b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt @@ -25,17 +25,14 @@ import android.graphics.drawable.Drawable import android.os.PowerManager import android.os.SystemClock import android.util.Log -import android.view.Gravity import android.view.LayoutInflater import android.view.MotionEvent -import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT -import android.widget.LinearLayout import com.android.internal.widget.CachingIconView import com.android.settingslib.Utils import com.android.systemui.R @@ -65,12 +62,15 @@ abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>( private val powerManager: PowerManager, @LayoutRes private val chipLayoutRes: Int ) { - /** The window layout parameters we'll use when attaching the view to a window. */ + + /** + * Window layout params that will be used as a starting point for the [windowLayoutParams] of + * all subclasses. + */ @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY - private val windowLayoutParams = WindowManager.LayoutParams().apply { + internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply { width = WindowManager.LayoutParams.WRAP_CONTENT height = WindowManager.LayoutParams.WRAP_CONTENT - gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL title = WINDOW_TITLE @@ -78,6 +78,14 @@ abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>( setTrustedOverlay() } + /** + * The window layout parameters we'll use when attaching the view to a window. + * + * Subclasses must override this to provide their specific layout params, and they should use + * [commonWindowLayoutParams] as part of their layout params. + */ + internal abstract val windowLayoutParams: WindowManager.LayoutParams + /** The chip view currently being displayed. Null if the chip is not being displayed. */ private var chipView: ViewGroup? = null diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index a5d763c5327b..f9818f0ad8be 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -24,6 +24,8 @@ import android.media.MediaRoute2Info import android.os.Handler import android.os.PowerManager import android.util.Log +import android.view.Gravity +import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager @@ -36,6 +38,7 @@ import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCom import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.gesture.TapGestureDetector +import com.android.systemui.util.animation.AnimationUtil.Companion.frames import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.view.ViewUtil import javax.inject.Inject @@ -69,6 +72,11 @@ class MediaTttChipControllerReceiver @Inject constructor( powerManager, R.layout.media_ttt_chip_receiver ) { + override val windowLayoutParams = commonWindowLayoutParams.apply { + height = getWindowHeight() + gravity = Gravity.BOTTOM.or(Gravity.CENTER_HORIZONTAL) + } + private val commandQueueCallbacks = object : CommandQueue.Callbacks { override fun updateMediaTapToTransferReceiverDisplay( @StatusBarManager.MediaTransferReceiverState displayState: Int, @@ -131,6 +139,19 @@ class MediaTttChipControllerReceiver @Inject constructor( ) } + override fun animateChipIn(chipView: ViewGroup) { + val appIconView = chipView.requireViewById<View>(R.id.app_icon) + appIconView.animate() + .translationYBy(-1 * getTranslationAmount().toFloat()) + .setDuration(30.frames) + .start() + appIconView.animate() + .alpha(1f) + .setDuration(5.frames) + .start() + + } + override fun getIconSize(isAppIcon: Boolean): Int? = context.resources.getDimensionPixelSize( if (isAppIcon) { @@ -139,6 +160,17 @@ class MediaTttChipControllerReceiver @Inject constructor( R.dimen.media_ttt_generic_icon_size_receiver } ) + + private fun getWindowHeight(): Int { + return context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver) + + // Make the window large enough to accommodate the animation amount + getTranslationAmount() + } + + /** Returns the amount that the chip will be translated by in its intro animation. */ + private fun getTranslationAmount(): Int { + return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation) + } } data class ChipReceiverInfo( diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt index 943604cff887..797a7701413b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt @@ -21,6 +21,7 @@ import android.content.Context import android.media.MediaRoute2Info import android.os.PowerManager import android.util.Log +import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.WindowManager @@ -69,6 +70,10 @@ class MediaTttChipControllerSender @Inject constructor( powerManager, R.layout.media_ttt_chip ) { + override val windowLayoutParams = commonWindowLayoutParams.apply { + gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) + } + private var currentlyDisplayedChipState: ChipStateSender? = null private val commandQueueCallbacks = object : CommandQueue.Callbacks { diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 0288c9fce64a..8f6c3737d6da 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -40,11 +40,13 @@ import android.widget.Button import android.widget.ImageView import android.widget.TextView import androidx.annotation.GuardedBy +import androidx.annotation.VisibleForTesting import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_FOOTER_DOT +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.Dumpable import com.android.systemui.R @@ -87,6 +89,7 @@ class FgsManagerController @Inject constructor( private val LOG_TAG = FgsManagerController::class.java.simpleName private const val DEFAULT_TASK_MANAGER_ENABLED = true private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false + private const val DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = true } var changesSinceDialog = false @@ -96,6 +99,8 @@ class FgsManagerController @Inject constructor( private set var showFooterDot = false private set + var showStopBtnForUserAllowlistedApps = false + private set private val lock = Any() @@ -163,6 +168,9 @@ class FgsManagerController @Inject constructor( isAvailable = it.getBoolean(TASK_MANAGER_ENABLED, isAvailable) showFooterDot = it.getBoolean(TASK_MANAGER_SHOW_FOOTER_DOT, showFooterDot) + showStopBtnForUserAllowlistedApps = it.getBoolean( + TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS, + showStopBtnForUserAllowlistedApps) } isAvailable = deviceConfigProxy.getBoolean( @@ -173,6 +181,10 @@ class FgsManagerController @Inject constructor( NAMESPACE_SYSTEMUI, TASK_MANAGER_SHOW_FOOTER_DOT, DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT ) + showStopBtnForUserAllowlistedApps = deviceConfigProxy.getBoolean( + NAMESPACE_SYSTEMUI, + TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS, + DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS) dumpManager.registerDumpable(this) @@ -275,6 +287,20 @@ class FgsManagerController @Inject constructor( } } + @VisibleForTesting + @JvmName("getNumVisibleButtons") + internal fun getNumVisibleButtons(): Int { + synchronized(lock) { + return getNumVisibleButtonsLocked() + } + } + + private fun getNumVisibleButtonsLocked(): Int { + return runningServiceTokens.keys.count { + it.uiControl != UIControl.HIDE_BUTTON && currentProfileIds.contains(it.userId) + } + } + fun shouldUpdateFooterVisibility() = dialog == null fun showDialog(viewLaunchedFrom: View?) { @@ -505,6 +531,13 @@ class FgsManagerController @Inject constructor( PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI, PowerExemptionManager.REASON_ROLE_DIALER, PowerExemptionManager.REASON_SYSTEM_MODULE -> UIControl.HIDE_BUTTON + + PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE -> + if (showStopBtnForUserAllowlistedApps) { + UIControl.NORMAL + } else { + UIControl.HIDE_BUTTON + } else -> UIControl.NORMAL } uiControlInitialized = true diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index fbdabc74aba4..a1c66b35bacc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -396,7 +396,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D } void saveTilesToSettings(List<String> tileSpecs) { - if (tileSpecs.contains("work")) Log.wtfStack(TAG, "Saving work tile"); mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs), null /* tag */, false /* default */, mCurrentUser, true /* overrideable by restore */); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 19eb168fddc0..89a15f65e98f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -142,7 +142,7 @@ public class ScreenshotController { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt index 5df593b64c24..558bcac681d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt @@ -61,7 +61,7 @@ class WiredChargingRippleController @Inject constructor( private val systemClock: SystemClock, private val uiEventLogger: UiEventLogger ) { - private var pluggedIn: Boolean? = null + private var pluggedIn: Boolean = false private val rippleEnabled: Boolean = featureFlags.isEnabled(Flags.CHARGING_RIPPLE) && !SystemProperties.getBoolean("persist.debug.suppress-charging-ripple", false) private var normalizedPortPosX: Float = context.resources.getFloat( @@ -99,15 +99,17 @@ class WiredChargingRippleController @Inject constructor( nowPluggedIn: Boolean, charging: Boolean ) { - // Suppresses the ripple when the state change comes from wireless charging. - if (batteryController.isPluggedInWireless) { + // Suppresses the ripple when the state change comes from wireless charging or + // its dock. + if (batteryController.isPluggedInWireless || + batteryController.isChargingSourceDock) { return } - val wasPluggedIn = pluggedIn - pluggedIn = nowPluggedIn - if ((wasPluggedIn == null || !wasPluggedIn) && nowPluggedIn) { + + if (!pluggedIn && nowPluggedIn) { startRippleWithDebounce() } + pluggedIn = nowPluggedIn } } batteryController.addCallback(batteryStateChangeCallback) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt index 2397005a1a61..52dcf02e3a19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt @@ -17,18 +17,32 @@ package com.android.systemui.statusbar.notification import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel import com.android.systemui.log.LogLevel.DEBUG import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.LogLevel.WARNING +import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.util.Compile import javax.inject.Inject /** Logger for [NotificationEntryManager]. */ class NotificationEntryManagerLogger @Inject constructor( + notifPipelineFlags: NotifPipelineFlags, @NotificationLog private val buffer: LogBuffer ) { + private val devLoggingEnabled by lazy { notifPipelineFlags.isDevLoggingEnabled() } + + private inline fun devLog( + level: LogLevel, + initializer: LogMessage.() -> Unit, + noinline printer: LogMessage.() -> String + ) { + if (Compile.IS_DEBUG && devLoggingEnabled) buffer.log(TAG, level, initializer, printer) + } + fun logNotifAdded(key: String) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = key }, { "NOTIF ADDED $str1" @@ -36,7 +50,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logNotifUpdated(key: String) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = key }, { "NOTIF UPDATED $str1" @@ -44,7 +58,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logInflationAborted(key: String, status: String, reason: String) { - buffer.log(TAG, DEBUG, { + devLog(DEBUG, { str1 = key str2 = status str3 = reason @@ -54,7 +68,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logNotifInflated(key: String, isNew: Boolean) { - buffer.log(TAG, DEBUG, { + devLog(DEBUG, { str1 = key bool1 = isNew }, { @@ -63,7 +77,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logRemovalIntercepted(key: String) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = key }, { "NOTIF REMOVE INTERCEPTED for $str1" @@ -71,7 +85,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logLifetimeExtended(key: String, extenderName: String, status: String) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = key str2 = extenderName str3 = status @@ -81,7 +95,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logNotifRemoved(key: String, removedByUser: Boolean) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = key bool1 = removedByUser }, { @@ -90,7 +104,7 @@ class NotificationEntryManagerLogger @Inject constructor( } fun logFilterAndSort(reason: String) { - buffer.log(TAG, INFO, { + devLog(INFO, { str1 = reason }, { "FILTER AND SORT reason=$str1" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS index 63c37e9584d6..ed80f33be701 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS @@ -9,4 +9,6 @@ juliacr@google.com juliatuttle@google.com lynhan@google.com steell@google.com -yurilin@google.com
\ No newline at end of file +yurilin@google.com + +per-file MediaNotificationProcessor.java = ethibodeau@google.com, asc@google.com diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index aedbd1b56622..0a16fb65b1ac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -175,6 +175,10 @@ public final class NotificationEntry extends ListEntry { public boolean mRemoteEditImeAnimatingAway; public boolean mRemoteEditImeVisible; private boolean mExpandAnimationRunning; + /** + * Flag to determine if the entry is blockable by DnD filters + */ + private boolean mBlockable; /** * @param sbn the StatusBarNotification from system server @@ -253,6 +257,7 @@ public final class NotificationEntry extends ListEntry { } mRanking = ranking.withAudiblyAlertedInfo(mRanking); + updateIsBlockable(); } /* @@ -781,15 +786,20 @@ public final class NotificationEntry extends ListEntry { * or is not in an allowList). */ public boolean isBlockable() { + return mBlockable; + } + + private void updateIsBlockable() { if (getChannel() == null) { - return false; + mBlockable = false; + return; } if (getChannel().isImportanceLockedByCriticalDeviceFunction() && !getChannel().isBlockable()) { - return false; + mBlockable = false; + return; } - - return true; + mBlockable = true; } private boolean shouldSuppressVisualEffect(int effect) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt index 3501b44a2c2e..38e3d496a60c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt @@ -19,20 +19,24 @@ package com.android.systemui.statusbar.notification.collection.render import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.util.Compile import javax.inject.Inject class NodeSpecBuilderLogger @Inject constructor( + notifPipelineFlags: NotifPipelineFlags, @NotificationLog private val buffer: LogBuffer ) { + private val devLoggingEnabled by lazy { notifPipelineFlags.isDevLoggingEnabled() } + fun logBuildNodeSpec( oldSections: Set<NotifSection?>, newHeaders: Map<NotifSection?, NodeController?>, newCounts: Map<NotifSection?, Int>, newSectionOrder: List<NotifSection?> ) { - if (!Compile.IS_DEBUG) + if (!(Compile.IS_DEBUG && devLoggingEnabled)) return buffer.log(TAG, LogLevel.DEBUG, { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 134f24e7e646..27aa4b38e0ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -266,9 +266,14 @@ public class NotificationConversationInfo extends LinearLayout implements snooze.setOnClickListener(mOnSnoozeClick); */ - if (mAppBubble == BUBBLE_PREFERENCE_ALL) { - ((TextView) findViewById(R.id.default_summary)).setText(getResources().getString( + TextView defaultSummaryTextView = findViewById(R.id.default_summary); + if (mAppBubble == BUBBLE_PREFERENCE_ALL + && BubblesManager.areBubblesEnabled(mContext, mSbn.getUser())) { + defaultSummaryTextView.setText(getResources().getString( R.string.notification_channel_summary_default_with_bubbles, mAppName)); + } else { + defaultSummaryTextView.setText(getResources().getString( + R.string.notification_channel_summary_default)); } findViewById(R.id.priority).setOnClickListener(mOnFavoriteClick); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS index 18f0fb38999c..f5828f914eab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS @@ -1,3 +1,16 @@ per-file *Notification* = set noparent per-file *Notification* = file:../notification/OWNERS -per-file NotificationIcon* = ccassidy@google.com, evanlaird@google.com, pixel@google.com
\ No newline at end of file + +per-file NotificationIcon* = ccassidy@google.com, evanlaird@google.com, pixel@google.com + +per-file NotificationsQuickSettingsContainer.java = kozynski@google.com, asc@google.com +per-file NotificationsQSContainerController.kt = kozynski@google.com, asc@google.com + +per-file NotificationShadeWindowControllerImpl.java = dupin@google.com, cinek@google.com, beverlyt@google.com, pixel@google.com, juliacr@google.com +per-file NotificationShadeWindowViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com +per-file NotificationShadeWindowView.java = pixel@google.com, cinek@google.com, juliacr@google.com + +per-file NotificationPanelUnfoldAnimationController.kt = alexflo@google.com, jeffdq@google.com, juliacr@google.com + +per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com +per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index 6e331bc13294..fa701e704c21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -89,6 +89,17 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh } public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock) { + // TODO(b/219008720): Remove those calls to Dependency.get by introducing a + // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set + // the content and attach listeners. + this(context, theme, dismissOnDeviceLock, Dependency.get(SystemUIDialogManager.class), + Dependency.get(SysUiState.class), Dependency.get(BroadcastDispatcher.class), + Dependency.get(DialogLaunchAnimator.class)); + } + + public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock, + SystemUIDialogManager dialogManager, SysUiState sysUiState, + BroadcastDispatcher broadcastDispatcher, DialogLaunchAnimator dialogLaunchAnimator) { super(context, theme); mContext = context; @@ -97,13 +108,10 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh attrs.setTitle(getClass().getSimpleName()); getWindow().setAttributes(attrs); - mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this) : null; - - // TODO(b/219008720): Remove those calls to Dependency.get by introducing a - // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set - // the content and attach listeners. - mDialogManager = Dependency.get(SystemUIDialogManager.class); - mSysUiState = Dependency.get(SysUiState.class); + mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this, broadcastDispatcher, + dialogLaunchAnimator) : null; + mDialogManager = dialogManager; + mSysUiState = sysUiState; } @Override @@ -322,7 +330,10 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh * @param dismissAction An action to run when the dialog is dismissed. */ public static void registerDismissListener(Dialog dialog, @Nullable Runnable dismissAction) { - DismissReceiver dismissReceiver = new DismissReceiver(dialog); + // TODO(b/219008720): Remove those calls to Dependency.get. + DismissReceiver dismissReceiver = new DismissReceiver(dialog, + Dependency.get(BroadcastDispatcher.class), + Dependency.get(DialogLaunchAnimator.class)); dialog.setOnDismissListener(d -> { dismissReceiver.unregister(); if (dismissAction != null) dismissAction.run(); @@ -404,11 +415,11 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh private final BroadcastDispatcher mBroadcastDispatcher; private final DialogLaunchAnimator mDialogLaunchAnimator; - DismissReceiver(Dialog dialog) { + DismissReceiver(Dialog dialog, BroadcastDispatcher broadcastDispatcher, + DialogLaunchAnimator dialogLaunchAnimator) { mDialog = dialog; - // TODO(b/219008720): Remove those calls to Dependency.get. - mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class); - mDialogLaunchAnimator = Dependency.get(DialogLaunchAnimator.class); + mBroadcastDispatcher = broadcastDispatcher; + mDialogLaunchAnimator = dialogLaunchAnimator; } void register() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java index a89c128dd584..753e94015751 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java @@ -119,6 +119,17 @@ public interface BatteryController extends DemoMode, Dumpable, } /** + * Returns {@code true} if the charging source is + * {@link android.os.BatteryManager#BATTERY_PLUGGED_DOCK}. + * + * <P>Note that charging from dock is not considered as wireless charging. In other words, + * {@link BatteryController#isWirelessCharging()} and this are mutually exclusive. + */ + default boolean isChargingSourceDock() { + return false; + } + + /** * A listener that will be notified whenever a change in battery level or power save mode has * occurred. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index 917a5e0b9374..33ddf7eed006 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -76,7 +76,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC protected int mLevel; protected boolean mPluggedIn; - private boolean mPluggedInWireless; + private int mPluggedChargingSource; protected boolean mCharging; private boolean mStateUnknown = false; private boolean mCharged; @@ -195,10 +195,8 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC mLevel = (int)(100f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); - mPluggedIn = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; - mPluggedInWireless = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) - == BatteryManager.BATTERY_PLUGGED_WIRELESS; - + mPluggedChargingSource = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + mPluggedIn = mPluggedChargingSource != 0; final int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN); mCharged = status == BatteryManager.BATTERY_STATUS_FULL; @@ -284,7 +282,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC @Override public boolean isPluggedInWireless() { - return mPluggedInWireless; + return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_WIRELESS; } @Override @@ -441,4 +439,9 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC registerReceiver(); updatePowerSave(); } + + @Override + public boolean isChargingSourceDock() { + return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_DOCK; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java index 4cf1d2b3f91d..9946b4b9ecaa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java @@ -17,15 +17,11 @@ package com.android.systemui.statusbar.policy; import android.annotation.WorkerThread; -import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Process; import android.provider.Settings; import android.provider.Settings.Secure; import android.text.TextUtils; @@ -33,12 +29,19 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.android.internal.annotations.GuardedBy; +import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; +import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; @@ -59,65 +62,88 @@ public class FlashlightControllerImpl implements FlashlightController { "com.android.settings.flashlight.action.FLASHLIGHT_CHANGED"; private final CameraManager mCameraManager; - private final Context mContext; - /** Call {@link #ensureHandler()} before using */ - private Handler mHandler; + private final Executor mExecutor; + private final SecureSettings mSecureSettings; + private final DumpManager mDumpManager; + private final BroadcastSender mBroadcastSender; - /** Lock on mListeners when accessing */ + private final boolean mHasFlashlight; + + @GuardedBy("mListeners") private final ArrayList<WeakReference<FlashlightListener>> mListeners = new ArrayList<>(1); - /** Lock on {@code this} when accessing */ + @GuardedBy("this") private boolean mFlashlightEnabled; - - private String mCameraId; + @GuardedBy("this") private boolean mTorchAvailable; + private final AtomicReference<String> mCameraId; + private final AtomicBoolean mInitted = new AtomicBoolean(false); + @Inject - public FlashlightControllerImpl(Context context, DumpManager dumpManager) { - mContext = context; - mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); + public FlashlightControllerImpl( + DumpManager dumpManager, + CameraManager cameraManager, + @Background Executor bgExecutor, + SecureSettings secureSettings, + BroadcastSender broadcastSender, + PackageManager packageManager + ) { + mCameraManager = cameraManager; + mExecutor = bgExecutor; + mCameraId = new AtomicReference<>(null); + mSecureSettings = secureSettings; + mDumpManager = dumpManager; + mBroadcastSender = broadcastSender; + + mHasFlashlight = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH); + init(); + } - dumpManager.registerDumpable(getClass().getSimpleName(), this); - tryInitCamera(); + private void init() { + if (!mInitted.getAndSet(true)) { + mDumpManager.registerDumpable(getClass().getSimpleName(), this); + mExecutor.execute(this::tryInitCamera); + } } + @WorkerThread private void tryInitCamera() { + if (!mHasFlashlight || mCameraId.get() != null) return; try { - mCameraId = getCameraId(); + mCameraId.set(getCameraId()); } catch (Throwable e) { Log.e(TAG, "Couldn't initialize.", e); return; } - if (mCameraId != null) { - ensureHandler(); - mCameraManager.registerTorchCallback(mTorchCallback, mHandler); + if (mCameraId.get() != null) { + mCameraManager.registerTorchCallback(mExecutor, mTorchCallback); } } public void setFlashlight(boolean enabled) { - boolean pendingError = false; - synchronized (this) { - if (mCameraId == null) return; - if (mFlashlightEnabled != enabled) { - mFlashlightEnabled = enabled; - try { - mCameraManager.setTorchMode(mCameraId, enabled); - } catch (CameraAccessException e) { - Log.e(TAG, "Couldn't set torch mode", e); - mFlashlightEnabled = false; - pendingError = true; + if (!mHasFlashlight) return; + if (mCameraId.get() == null) { + mExecutor.execute(this::tryInitCamera); + } + mExecutor.execute(() -> { + if (mCameraId.get() == null) return; + synchronized (this) { + if (mFlashlightEnabled != enabled) { + try { + mCameraManager.setTorchMode(mCameraId.get(), enabled); + } catch (CameraAccessException e) { + Log.e(TAG, "Couldn't set torch mode", e); + dispatchError(); + } } } - } - dispatchModeChanged(mFlashlightEnabled); - if (pendingError) { - dispatchError(); - } + }); } public boolean hasFlashlight() { - return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH); + return mHasFlashlight; } public synchronized boolean isEnabled() { @@ -131,13 +157,13 @@ public class FlashlightControllerImpl implements FlashlightController { @Override public void addCallback(@NonNull FlashlightListener l) { synchronized (mListeners) { - if (mCameraId == null) { - tryInitCamera(); + if (mCameraId.get() == null) { + mExecutor.execute(this::tryInitCamera); } cleanUpListenersLocked(l); mListeners.add(new WeakReference<>(l)); - l.onFlashlightAvailabilityChanged(mTorchAvailable); - l.onFlashlightChanged(mFlashlightEnabled); + l.onFlashlightAvailabilityChanged(isAvailable()); + l.onFlashlightChanged(isEnabled()); } } @@ -148,14 +174,7 @@ public class FlashlightControllerImpl implements FlashlightController { } } - private synchronized void ensureHandler() { - if (mHandler == null) { - HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); - thread.start(); - mHandler = new Handler(thread.getLooper()); - } - } - + @WorkerThread private String getCameraId() throws CameraAccessException { String[] ids = mCameraManager.getCameraIdList(); for (String id : ids) { @@ -221,10 +240,9 @@ public class FlashlightControllerImpl implements FlashlightController { @Override @WorkerThread public void onTorchModeUnavailable(String cameraId) { - if (TextUtils.equals(cameraId, mCameraId)) { + if (TextUtils.equals(cameraId, mCameraId.get())) { setCameraAvailable(false); - Settings.Secure.putInt( - mContext.getContentResolver(), Settings.Secure.FLASHLIGHT_AVAILABLE, 0); + mSecureSettings.putInt(Settings.Secure.FLASHLIGHT_AVAILABLE, 0); } } @@ -232,14 +250,12 @@ public class FlashlightControllerImpl implements FlashlightController { @Override @WorkerThread public void onTorchModeChanged(String cameraId, boolean enabled) { - if (TextUtils.equals(cameraId, mCameraId)) { + if (TextUtils.equals(cameraId, mCameraId.get())) { setCameraAvailable(true); setTorchMode(enabled); - Settings.Secure.putInt( - mContext.getContentResolver(), Settings.Secure.FLASHLIGHT_AVAILABLE, 1); - Settings.Secure.putInt( - mContext.getContentResolver(), Secure.FLASHLIGHT_ENABLED, enabled ? 1 : 0); - mContext.sendBroadcast(new Intent(ACTION_FLASHLIGHT_CHANGED)); + mSecureSettings.putInt(Settings.Secure.FLASHLIGHT_AVAILABLE, 1); + mSecureSettings.putInt(Secure.FLASHLIGHT_ENABLED, enabled ? 1 : 0); + mBroadcastSender.sendBroadcast(new Intent(ACTION_FLASHLIGHT_CHANGED)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt index d5df9fe0c2e8..c48cbb19b40a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt @@ -159,7 +159,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { @Test fun doesNotStartIfAnimationIsCancelled() { val runner = activityLaunchAnimator.createRunner(controller) - runner.onAnimationCancelled() + runner.onAnimationCancelled(false /* isKeyguardOccluded */) runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback) waitForIdleSync() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java index 0fdd9054e4bc..b61bda8edd10 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -468,6 +469,40 @@ public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { verify(mView).setUnpausedAlpha(255); } + @Test + public void testUpdatePanelExpansion_pauseAuth() { + // GIVEN view is attached + on the keyguard + mController.onViewAttached(); + captureStatusBarStateListeners(); + captureStatusBarExpansionListeners(); + sendStatusBarStateChanged(StatusBarState.KEYGUARD); + reset(mView); + + // WHEN panelViewExpansion changes to hide + when(mView.getUnpausedAlpha()).thenReturn(0); + updateStatusBarExpansion(0f, false); + + // THEN pause auth is updated to PAUSE + verify(mView, atLeastOnce()).setPauseAuth(true); + } + + @Test + public void testUpdatePanelExpansion_unpauseAuth() { + // GIVEN view is attached + on the keyguard + panel expansion is 0f + mController.onViewAttached(); + captureStatusBarStateListeners(); + captureStatusBarExpansionListeners(); + sendStatusBarStateChanged(StatusBarState.KEYGUARD); + reset(mView); + + // WHEN panelViewExpansion changes to expanded + when(mView.getUnpausedAlpha()).thenReturn(255); + updateStatusBarExpansion(1f, true); + + // THEN pause auth is updated to NOT pause + verify(mView, atLeastOnce()).setPauseAuth(false); + } + private void sendStatusBarStateChanged(int statusBarState) { mStatusBarStateListener.onStateChanged(statusBarState); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java index df506b479b88..b5e9e8decb4c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java @@ -38,6 +38,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.settingslib.Utils; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.AnimatableClockView; import com.android.systemui.statusbar.policy.BatteryController; @@ -53,6 +54,8 @@ import org.mockito.MockitoAnnotations; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +import java.util.concurrent.Executor; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @@ -69,6 +72,12 @@ public class AnimatableClockControllerTest extends SysuiTestCase { private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private Resources mResources; + @Mock + private Executor mMainExecutor; + @Mock + private Executor mBgExecutor; + @Mock + private FeatureFlags mFeatureFlags; private MockitoSession mStaticMockSession; private AnimatableClockController mAnimatableClockController; @@ -96,7 +105,10 @@ public class AnimatableClockControllerTest extends SysuiTestCase { mBroadcastDispatcher, mBatteryController, mKeyguardUpdateMonitor, - mResources + mResources, + mMainExecutor, + mBgExecutor, + mFeatureFlags ); mAnimatableClockController.init(); captureAttachListener(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt index 1527f0d0d71f..2eb478303cb2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt @@ -371,11 +371,9 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { powerManager, R.layout.media_ttt_chip ) { - override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) { - - } - - override fun getIconSize(isAppIcon: Boolean): Int? = ICON_SIZE + override val windowLayoutParams = commonWindowLayoutParams + override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) {} + override fun getIconSize(isAppIcon: Boolean): Int = ICON_SIZE } inner class ChipInfo : ChipInfoCommon { @@ -386,4 +384,4 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { private const val PACKAGE_NAME = "com.android.systemui" private const val APP_NAME = "Fake App Name" private const val TIMEOUT_MS = 10000L -private const val ICON_SIZE = 47
\ No newline at end of file +private const val ICON_SIZE = 47 diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java index 2927669020c8..3dc10d0ac6ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.qs; +import static android.os.PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -34,6 +36,7 @@ import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.os.Binder; import android.os.RemoteException; +import android.os.UserHandle; import android.provider.DeviceConfig; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -222,7 +225,41 @@ public class FgsManagerControllerTest extends SysuiTestCase { Assert.assertEquals(2, mFmc.getNumRunningPackages()); } + @Test + public void testButtonVisibilityOnShowAllowlistButtonFlagChange() throws Exception { + setUserProfiles(0); + setBackgroundRestrictionExemptionReason("pkg", 12345, REASON_ALLOWLISTED_PACKAGE); + + final Binder binder = new Binder(); + setShowStopButtonForUserAllowlistedApps(true); + mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, true); + Assert.assertEquals(1, mFmc.getNumVisibleButtons()); + + mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, false); + Assert.assertEquals(0, mFmc.getNumVisibleButtons()); + + setShowStopButtonForUserAllowlistedApps(false); + mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, true); + Assert.assertEquals(0, mFmc.getNumVisibleButtons()); + } + private void setShowStopButtonForUserAllowlistedApps(boolean enable) { + mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS, + enable ? "true" : "false", false); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + } + + private void setBackgroundRestrictionExemptionReason(String pkgName, int uid, int reason) + throws Exception { + Mockito.doReturn(uid) + .when(mPackageManager) + .getPackageUidAsUser(pkgName, UserHandle.getUserId(uid)); + Mockito.doReturn(reason) + .when(mIActivityManager) + .getBackgroundRestrictionExemptionReason(uid); + } FgsManagerController createFgsManagerController() throws RemoteException { ArgumentCaptor<IForegroundServiceObserver> iForegroundServiceObserverArgumentCaptor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt index b4cae38d8b6e..d0cf792b698d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt @@ -74,9 +74,9 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { // Verify ripple added to window manager. captor.value.onBatteryLevelChanged( - 0 /* unusedBatteryLevel */, - true /* plugged in */, - false /* charging */) + /* unusedBatteryLevel= */ 0, + /* plugged in= */ true, + /* charging= */ false) val attachListenerCaptor = ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java) verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture()) @@ -144,4 +144,22 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { // Verify that ripple is triggered. verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any()) } + + @Test + fun testRipple_whenDocked_doesNotPlayRipple() { + `when`(batteryController.isChargingSourceDock).thenReturn(true) + val captor = ArgumentCaptor + .forClass(BatteryController.BatteryStateChangeCallback::class.java) + verify(batteryController).addCallback(captor.capture()) + + captor.value.onBatteryLevelChanged( + /* unusedBatteryLevel= */ 0, + /* plugged in= */ true, + /* charging= */ false) + + val attachListenerCaptor = + ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java) + verify(rippleView, never()).addOnAttachStateChangeListener(attachListenerCaptor.capture()) + verify(windowManager, never()).addView(eq(rippleView), any<WindowManager.LayoutParams>()) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index 769143ddbc0d..d4add7547656 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -108,6 +108,7 @@ public class NotificationEntryTest extends SysuiTestCase { @Test public void testBlockableEntryWhenCritical() { doReturn(true).when(mChannel).isBlockable(); + mEntry.setRanking(mEntry.getRanking()); assertTrue(mEntry.isBlockable()); } @@ -117,6 +118,7 @@ public class NotificationEntryTest extends SysuiTestCase { public void testBlockableEntryWhenCriticalAndChannelNotBlockable() { doReturn(true).when(mChannel).isBlockable(); doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction(); + mEntry.setRanking(mEntry.getRanking()); assertTrue(mEntry.isBlockable()); } @@ -125,6 +127,7 @@ public class NotificationEntryTest extends SysuiTestCase { public void testNonBlockableEntryWhenCriticalAndChannelNotBlockable() { doReturn(false).when(mChannel).isBlockable(); doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction(); + mEntry.setRanking(mEntry.getRanking()); assertFalse(mEntry.isBlockable()); } @@ -164,6 +167,9 @@ public class NotificationEntryTest extends SysuiTestCase { doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction(); doReturn(false).when(mChannel).isBlockable(); + mEntry.setRanking(mEntry.getRanking()); + + assertFalse(mEntry.isBlockable()); assertTrue(mEntry.isExemptFromDndVisualSuppression()); assertFalse(mEntry.shouldSuppressAmbient()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java index fec2123b304a..fda80a2f9ed8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Intent; +import android.os.BatteryManager; import android.os.Handler; import android.os.PowerManager; import android.os.PowerSaveState; @@ -196,4 +197,26 @@ public class BatteryControllerTest extends SysuiTestCase { TestableLooper.get(this).processAllMessages(); // Should not throw an exception } + + @Test + public void batteryStateChanged_withChargingSourceDock_isChargingSourceDockTrue() { + Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); + intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING); + intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_DOCK); + + mBatteryController.onReceive(getContext(), intent); + + Assert.assertTrue(mBatteryController.isChargingSourceDock()); + } + + @Test + public void batteryStateChanged_withChargingSourceNotDock_isChargingSourceDockFalse() { + Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); + intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING); + intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_WIRELESS); + + mBatteryController.onReceive(getContext(), intent); + + Assert.assertFalse(mBatteryController.isChargingSourceDock()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt new file mode 100644 index 000000000000..db0029af4ee2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import android.content.pm.PackageManager +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.hardware.camera2.impl.CameraMetadataNative +import android.test.suitebuilder.annotation.SmallTest +import android.testing.AndroidTestingRunner +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.time.FakeSystemClock +import java.util.concurrent.Executor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.eq +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class FlashlightControllerImplTest : SysuiTestCase() { + + @Mock + private lateinit var dumpManager: DumpManager + + @Mock + private lateinit var cameraManager: CameraManager + + @Mock + private lateinit var broadcastSender: BroadcastSender + + @Mock + private lateinit var packageManager: PackageManager + + private lateinit var fakeSettings: FakeSettings + private lateinit var fakeSystemClock: FakeSystemClock + private lateinit var backgroundExecutor: FakeExecutor + private lateinit var controller: FlashlightControllerImpl + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + fakeSystemClock = FakeSystemClock() + backgroundExecutor = FakeExecutor(fakeSystemClock) + fakeSettings = FakeSettings() + + `when`(packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) + .thenReturn(true) + + controller = FlashlightControllerImpl( + dumpManager, + cameraManager, + backgroundExecutor, + fakeSettings, + broadcastSender, + packageManager + ) + } + + @Test + fun testNoCameraManagerInteractionDirectlyOnConstructor() { + verifyZeroInteractions(cameraManager) + } + + @Test + fun testCameraManagerInitAfterConstructionOnExecutor() { + injectCamera() + backgroundExecutor.runAllReady() + + verify(cameraManager).registerTorchCallback(eq(backgroundExecutor), any()) + } + + @Test + fun testNoCallbackIfNoFlashCamera() { + injectCamera(flash = false) + backgroundExecutor.runAllReady() + + verify(cameraManager, never()).registerTorchCallback(any<Executor>(), any()) + } + + @Test + fun testNoCallbackIfNoBackCamera() { + injectCamera(facing = CameraCharacteristics.LENS_FACING_FRONT) + backgroundExecutor.runAllReady() + + verify(cameraManager, never()).registerTorchCallback(any<Executor>(), any()) + } + + @Test + fun testSetFlashlightInBackgroundExecutor() { + val id = injectCamera() + backgroundExecutor.runAllReady() + + clearInvocations(cameraManager) + val enable = !controller.isEnabled + controller.setFlashlight(enable) + verifyNoMoreInteractions(cameraManager) + + backgroundExecutor.runAllReady() + verify(cameraManager).setTorchMode(id, enable) + } + + private fun injectCamera( + flash: Boolean = true, + facing: Int = CameraCharacteristics.LENS_FACING_BACK + ): String { + val cameraID = "ID" + val camera = CameraCharacteristics(CameraMetadataNative().apply { + set(CameraCharacteristics.FLASH_INFO_AVAILABLE, flash) + set(CameraCharacteristics.LENS_FACING, facing) + }) + `when`(cameraManager.cameraIdList).thenReturn(arrayOf(cameraID)) + `when`(cameraManager.getCameraCharacteristics(cameraID)).thenReturn(camera) + return cameraID + } +} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index ab9966f218c1..c3545619bcb9 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -62,6 +62,7 @@ import android.companion.DeviceNotAssociatedException; import android.companion.IAssociationRequestCallback; import android.companion.ICompanionDeviceManager; import android.companion.IOnAssociationsChangedListener; +import android.companion.ISystemDataTransferCallback; import android.content.ComponentName; import android.content.Context; import android.content.SharedPreferences; @@ -76,6 +77,7 @@ import android.os.Environment; import android.os.Handler; import android.os.Message; import android.os.Parcel; +import android.os.ParcelFileDescriptor; import android.os.PowerWhitelistManager; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -110,6 +112,7 @@ import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; import com.android.server.companion.presence.CompanionDevicePresenceMonitor; import com.android.server.companion.securechannel.CompanionSecureCommunicationsManager; import com.android.server.pm.UserManagerInternal; +import com.android.server.wm.ActivityTaskManagerInternal; import java.io.File; import java.io.FileDescriptor; @@ -153,6 +156,7 @@ public class CompanionDeviceManagerService extends SystemService { private CompanionApplicationController mCompanionAppController; private CompanionSecureCommunicationsManager mSecureCommsManager; + private final ActivityTaskManagerInternal mAtmInternal; private final ActivityManagerInternal mAmInternal; private final IAppOpsService mAppOpsManager; private final PowerWhitelistManager mPowerWhitelistManager; @@ -205,6 +209,7 @@ public class CompanionDeviceManagerService extends SystemService { mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class); mAppOpsManager = IAppOpsService.Stub.asInterface( ServiceManager.getService(Context.APP_OPS_SERVICE)); + mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class); mAmInternal = LocalServices.getService(ActivityManagerInternal.class); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mUserManager = context.getSystemService(UserManager.class); @@ -721,8 +726,22 @@ public class CompanionDeviceManagerService extends SystemService { } @Override - public void startSystemDataTransfer(String packageName, int userId, int associationId) { + public void startSystemDataTransfer(String packageName, int userId, int associationId, + ISystemDataTransferCallback callback) { mSystemDataTransferProcessor.startSystemDataTransfer(packageName, userId, + associationId, callback); + } + + @Override + public void attachSystemDataTransport(String packageName, int userId, int associationId, + ParcelFileDescriptor fd) { + mSystemDataTransferProcessor.attachSystemDataTransport(packageName, userId, + associationId, fd); + } + + @Override + public void detachSystemDataTransport(String packageName, int userId, int associationId) { + mSystemDataTransferProcessor.detachSystemDataTransport(packageName, userId, associationId); } @@ -1246,6 +1265,9 @@ public class CompanionDeviceManagerService extends SystemService { companionAppUids.add(uid); } } + if (mAtmInternal != null) { + mAtmInternal.setCompanionAppUids(userId, companionAppUids); + } if (mAmInternal != null) { // Make a copy of the set and send it to ActivityManager. mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids)); diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index 70745ba0b368..7eede552ac71 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -29,6 +29,7 @@ import android.annotation.UserIdInt; import android.app.PendingIntent; import android.companion.AssociationInfo; import android.companion.DeviceNotAssociatedException; +import android.companion.ISystemDataTransferCallback; import android.companion.datatransfer.PermissionSyncRequest; import android.companion.datatransfer.SystemDataTransferRequest; import android.content.ComponentName; @@ -37,16 +38,26 @@ import android.content.Intent; import android.os.Binder; import android.os.Bundle; import android.os.Handler; +import android.os.ParcelFileDescriptor; import android.os.ResultReceiver; import android.os.UserHandle; import android.permission.PermissionControllerManager; import android.util.Slog; +import android.util.SparseArray; +import com.android.internal.annotations.GuardedBy; import com.android.server.companion.AssociationStore; import com.android.server.companion.CompanionDeviceManagerService; import com.android.server.companion.PermissionsUtils; import com.android.server.companion.proto.CompanionMessage; +import libcore.io.IoUtils; +import libcore.io.Streams; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -77,6 +88,8 @@ public class SystemDataTransferProcessor { private final CompanionMessageProcessor mCompanionMessageProcessor; private final PermissionControllerManager mPermissionControllerManager; private final ExecutorService mExecutor; + @GuardedBy("mTransports") + private final SparseArray<Transport> mTransports = new SparseArray<>(); public SystemDataTransferProcessor(CompanionDeviceManagerService service, AssociationStore associationStore, @@ -92,10 +105,11 @@ public class SystemDataTransferProcessor { } /** - * Build a PendingIntent of permission sync user consent dialog + * Resolve the requested association, throwing if the caller doesn't have + * adequate permissions. */ - public PendingIntent buildPermissionTransferUserConsentIntent(String packageName, - @UserIdInt int userId, int associationId) { + private @NonNull AssociationInfo resolveAssociation(String packageName, int userId, + int associationId) { AssociationInfo association = mAssociationStore.getAssociationById(associationId); association = PermissionsUtils.sanitizeWithCallerChecks(mContext, association); if (association == null) { @@ -103,6 +117,15 @@ public class SystemDataTransferProcessor { + associationId + " is not associated with the app " + packageName + " for user " + userId); } + return association; + } + + /** + * Build a PendingIntent of permission sync user consent dialog + */ + public PendingIntent buildPermissionTransferUserConsentIntent(String packageName, + @UserIdInt int userId, int associationId) { + final AssociationInfo association = resolveAssociation(packageName, userId, associationId); // Check if the request's data type has been requested before. List<SystemDataTransferRequest> storedRequests = @@ -145,18 +168,15 @@ public class SystemDataTransferProcessor { /** * Start system data transfer. It should first try to establish a secure channel and then sync * system data. + * + * TODO: execute callback when the transfer finishes successfully or with errors. */ - public void startSystemDataTransfer(String packageName, int userId, int associationId) { + public void startSystemDataTransfer(String packageName, int userId, int associationId, + ISystemDataTransferCallback callback) { Slog.i(LOG_TAG, "Start system data transfer for package [" + packageName + "] userId [" + userId + "] associationId [" + associationId + "]"); - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - association = PermissionsUtils.sanitizeWithCallerChecks(mContext, association); - if (association == null) { - throw new DeviceNotAssociatedException("Association " - + associationId + " is not associated with the app " + packageName - + " for user " + userId); - } + final AssociationInfo association = resolveAssociation(packageName, userId, associationId); // Check if the request has been consented by the user. List<SystemDataTransferRequest> storedRequests = @@ -188,6 +208,35 @@ public class SystemDataTransferProcessor { } } + public void attachSystemDataTransport(String packageName, int userId, int associationId, + ParcelFileDescriptor fd) { + synchronized (mTransports) { + // TODO: restore once testing has evolved + // resolveAssociation(packageName, userId, associationId); + + if (mTransports.contains(associationId)) { + detachSystemDataTransport(packageName, userId, associationId); + } + + final Transport transport = new Transport(fd); + transport.start(); + mTransports.put(associationId, transport); + } + } + + public void detachSystemDataTransport(String packageName, int userId, int associationId) { + synchronized (mTransports) { + // TODO: restore once testing has evolved + // resolveAssociation(packageName, userId, associationId); + + final Transport transport = mTransports.get(associationId); + if (transport != null) { + mTransports.delete(associationId); + transport.stop(); + } + } + } + /** * Process a complete decrypted message reported by the companion app. */ @@ -242,4 +291,74 @@ public class SystemDataTransferProcessor { Slog.e(LOG_TAG, "Unknown result code:" + resultCode); } }; + + private class Transport { + private final InputStream mRemoteIn; + private final OutputStream mRemoteOut; + + private volatile boolean mStopped; + + public Transport(ParcelFileDescriptor fd) { + mRemoteIn = new ParcelFileDescriptor.AutoCloseInputStream(fd); + mRemoteOut = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + } + + public void start() { + new Thread(() -> { + try { + while (!mStopped) { + processNextCommand(); + } + } catch (IOException e) { + if (!mStopped) { + Slog.w(LOG_TAG, "Trouble during transport", e); + stop(); + } + } + }).start(); + } + + public void stop() { + mStopped = true; + + IoUtils.closeQuietly(mRemoteIn); + IoUtils.closeQuietly(mRemoteOut); + } + + private void processNextCommand() throws IOException { + Slog.d(LOG_TAG, "Waiting for next command..."); + + // Read message header + final byte[] headerBytes = new byte[8]; + Streams.readFully(mRemoteIn, headerBytes); + final ByteBuffer header = ByteBuffer.wrap(headerBytes); + final int command = header.getInt(); + final int length = header.getInt(); + + Slog.d(LOG_TAG, "Received command 0x" + Integer.toHexString(command) + + " length " + length); + switch (command) { + case 0x50490000: // PI(NG) version 0 + // Repeat back the given payload, within reason + final int target = Math.min(length, 1_000_000); + final byte[] payload = new byte[target]; + Streams.readFully(mRemoteIn, payload); + Streams.skipByReading(mRemoteIn, length - target); + + // Respond with PO(NG) version 0 + header.rewind(); + header.putInt(0x504F0000); + header.putInt(target); + mRemoteOut.write(header.array()); + mRemoteOut.write(payload); + break; + + default: + // Emit local warning, and skip message to + // handle next one + Slog.w(LOG_TAG, "Unknown command 0x" + Integer.toHexString(command)); + mRemoteIn.skip(length); + } + } + } } diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index b63a4daab88f..76c897aac051 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -306,11 +306,13 @@ public class Watchdog implements Dumpable { } String describeBlockedStateLocked() { + Thread thread = getThread(); + String threadIdentifier = thread.getName() + ", tid=" + thread.getId(); if (mCurrentMonitor == null) { - return "Blocked in handler on " + mName + " (" + getThread().getName() + ")"; + return "Blocked in handler on " + mName + " (" + threadIdentifier + ")"; } else { return "Blocked in monitor " + mCurrentMonitor.getClass().getName() - + " on " + mName + " (" + getThread().getName() + ")"; + + " on " + mName + " (" + threadIdentifier + ")"; } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index d63fd53a383d..60286bece7d7 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -8755,7 +8755,7 @@ public class ActivityManagerService extends IActivityManager.Stub private static String processClass(ProcessRecord process) { if (process == null || process.getPid() == MY_PID) { return "system_server"; - } else if ((process.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + } else if (process.info.isSystemApp() || process.info.isSystemExt()) { return "system_app"; } else { return "data_app"; @@ -12484,6 +12484,12 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public PendingIntent getRunningServiceControlPanel(ComponentName name) { enforceNotIsolatedCaller("getRunningServiceControlPanel"); + final int callingUid = Binder.getCallingUid(); + final int callingUserId = UserHandle.getUserId(callingUid); + if (name == null || getPackageManagerInternal() + .filterAppAccess(name.getPackageName(), callingUid, callingUserId)) { + return null; + } synchronized (this) { return mServices.getRunningServiceControlPanelLocked(name); } diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index bddc784385e7..a23870567cdb 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -56,7 +56,6 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.INetworkManagementService; import android.os.Parcel; -import android.os.ParcelFileDescriptor; import android.os.ParcelFormatException; import android.os.PowerManager.ServiceType; import android.os.PowerManagerInternal; @@ -728,56 +727,6 @@ public final class BatteryStatsService extends IBatteryStats.Stub return mBatteryUsageStatsProvider.getBatteryUsageStats(queries); } - @Override - @EnforcePermission(BATTERY_STATS) - public byte[] getStatistics() { - //Slog.i("foo", "SENDING BATTERY INFO:"); - //mStats.dumpLocked(new LogPrinter(Log.INFO, "foo", Log.LOG_ID_SYSTEM)); - Parcel out = Parcel.obtain(); - // Drain the handler queue to make sure we've handled all pending works, so we'll get - // an accurate stats. - awaitCompletion(); - syncStats("get-stats", BatteryExternalStatsWorker.UPDATE_ALL); - synchronized (mStats) { - mStats.writeToParcel(out, 0); - } - byte[] data = out.marshall(); - out.recycle(); - return data; - } - - /** - * Returns parceled BatteryStats as a MemoryFile. - * - * @param forceUpdate If true, runs a sync to get fresh battery stats. Otherwise, - * returns the current values. - */ - @Override - @EnforcePermission(BATTERY_STATS) - public ParcelFileDescriptor getStatisticsStream(boolean forceUpdate) { - //Slog.i("foo", "SENDING BATTERY INFO:"); - //mStats.dumpLocked(new LogPrinter(Log.INFO, "foo", Log.LOG_ID_SYSTEM)); - Parcel out = Parcel.obtain(); - if (forceUpdate) { - // Drain the handler queue to make sure we've handled all pending works, so we'll get - // an accurate stats. - awaitCompletion(); - syncStats("get-stats", BatteryExternalStatsWorker.UPDATE_ALL); - } - synchronized (mStats) { - mStats.writeToParcel(out, 0); - } - byte[] data = out.marshall(); - if (DBG) Slog.d(TAG, "getStatisticsStream parcel size is:" + data.length); - out.recycle(); - try { - return ParcelFileDescriptor.fromData(data, "battery-stats"); - } catch (IOException e) { - Slog.w(TAG, "Unable to create shared memory", e); - return null; - } - } - /** Register callbacks for statsd pulled atoms. */ private void registerStatsCallbacks() { final StatsManager statsManager = mContext.getSystemService(StatsManager.class); diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java index 453385938aca..4ff1a129f691 100644 --- a/services/core/java/com/android/server/am/ContentProviderHelper.java +++ b/services/core/java/com/android/server/am/ContentProviderHelper.java @@ -29,6 +29,7 @@ import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU; import static com.android.server.am.ActivityManagerService.TAG_MU; import static com.android.server.am.ProcessProfileRecord.HOSTING_COMPONENT_TYPE_PROVIDER; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; @@ -48,7 +49,6 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.pm.PackageManagerInternal; import android.content.pm.PathPermission; import android.content.pm.ProviderInfo; import android.content.pm.UserInfo; @@ -958,20 +958,22 @@ public class ContentProviderHelper { String getProviderMimeType(Uri uri, int userId) { mService.enforceNotIsolatedCaller("getProviderMimeType"); final String name = uri.getAuthority(); - int callingUid = Binder.getCallingUid(); - int callingPid = Binder.getCallingPid(); - long ident = 0; - boolean clearedIdentity = false; - userId = mService.mUserController.unsafeConvertIncomingUser(userId); - if (canClearIdentity(callingPid, callingUid, userId)) { - clearedIdentity = true; - ident = Binder.clearCallingIdentity(); - } - ContentProviderHolder holder = null; + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + final int safeUserId = mService.mUserController.unsafeConvertIncomingUser(userId); + final long ident = canClearIdentity(callingPid, callingUid, safeUserId) + ? Binder.clearCallingIdentity() : 0; + final ContentProviderHolder holder; try { holder = getContentProviderExternalUnchecked(name, null, callingUid, - "*getmimetype*", userId); - if (holder != null) { + "*getmimetype*", safeUserId); + } finally { + if (ident != 0) { + Binder.restoreCallingIdentity(ident); + } + } + try { + if (isHolderVisibleToCaller(holder, callingUid, safeUserId)) { final IBinder providerConnection = holder.connection; final ComponentName providerName = holder.info.getComponentName(); // Note: creating a new Runnable instead of using a lambda here since lambdas in @@ -1000,15 +1002,13 @@ public class ContentProviderHelper { return null; } finally { // We need to clear the identity to call removeContentProviderExternalUnchecked - if (!clearedIdentity) { - ident = Binder.clearCallingIdentity(); - } + final long token = Binder.clearCallingIdentity(); try { if (holder != null) { - removeContentProviderExternalUnchecked(name, null, userId); + removeContentProviderExternalUnchecked(name, null /* token */, safeUserId); } } finally { - Binder.restoreCallingIdentity(ident); + Binder.restoreCallingIdentity(token); } } @@ -1027,12 +1027,20 @@ public class ContentProviderHelper { final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); final int safeUserId = mService.mUserController.unsafeConvertIncomingUser(userId); - final long ident = canClearIdentity(callingPid, callingUid, userId) + final long ident = canClearIdentity(callingPid, callingUid, safeUserId) ? Binder.clearCallingIdentity() : 0; + final ContentProviderHolder holder; try { - final ContentProviderHolder holder = getContentProviderExternalUnchecked(name, null, + holder = getContentProviderExternalUnchecked(name, null /* token */, callingUid, "*getmimetype*", safeUserId); - if (holder != null) { + } finally { + if (ident != 0) { + Binder.restoreCallingIdentity(ident); + } + } + + try { + if (isHolderVisibleToCaller(holder, callingUid, safeUserId)) { holder.provider.getTypeAsync(uri, new RemoteCallback(result -> { final long identity = Binder.clearCallingIdentity(); try { @@ -1048,8 +1056,6 @@ public class ContentProviderHelper { } catch (RemoteException e) { Log.w(TAG, "Content provider dead retrieving " + uri, e); resultCallback.sendResult(Bundle.EMPTY); - } finally { - Binder.restoreCallingIdentity(ident); } } @@ -1065,6 +1071,16 @@ public class ContentProviderHelper { callingUid, -1, true) == PackageManager.PERMISSION_GRANTED; } + private boolean isHolderVisibleToCaller(@Nullable ContentProviderHolder holder, int callingUid, + @UserIdInt int userId) { + if (holder == null || holder.info == null) { + return false; + } + + return !mService.getPackageManagerInternal() + .filterAppAccess(holder.info.packageName, callingUid, userId); + } + /** * Check if the calling UID has a possible chance at accessing the provider * at the given authority and user. @@ -1133,9 +1149,7 @@ public class ContentProviderHelper { "*checkContentProviderUriPermission*", userId); if (holder != null) { - final PackageManagerInternal packageManagerInt = LocalServices.getService( - PackageManagerInternal.class); - final AndroidPackage androidPackage = packageManagerInt + final AndroidPackage androidPackage = mService.getPackageManagerInternal() .getPackage(Binder.getCallingUid()); if (androidPackage == null) { return PackageManager.PERMISSION_DENIED; diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 5c7af47682e4..d38cd8ef6181 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -3126,8 +3126,8 @@ public class AppOpsService extends IAppOpsService.Stub { if (callback == null) { return; } - final boolean mayWatchPackageName = - packageName != null && !filterAppAccessUnlocked(packageName); + final boolean mayWatchPackageName = packageName != null + && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(callingUid)); synchronized (this) { int switchOp = (op != AppOpsManager.OP_NONE) ? AppOpsManager.opToSwitch(op) : op; @@ -3331,7 +3331,8 @@ public class AppOpsService extends IAppOpsService.Stub { // When the caller is the system, it's possible that the packageName is the special // one (e.g., "root") which isn't actually existed. if (resolveUid(packageName) == uid - || (isPackageExisted(packageName) && !filterAppAccessUnlocked(packageName))) { + || (isPackageExisted(packageName) + && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(uid)))) { return AppOpsManager.MODE_ALLOWED; } return AppOpsManager.MODE_ERRORED; @@ -3350,10 +3351,10 @@ public class AppOpsService extends IAppOpsService.Stub { * * NOTE: This must not be called while synchronized on {@code this} to avoid dead locks */ - private boolean filterAppAccessUnlocked(String packageName) { + private boolean filterAppAccessUnlocked(String packageName, int userId) { final int callingUid = Binder.getCallingUid(); return LocalServices.getService(PackageManagerInternal.class) - .filterAppAccess(packageName, callingUid, UserHandle.getUserId(callingUid)); + .filterAppAccess(packageName, callingUid, userId); } @Override diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 5b26672c7de2..dd44af1b68ee 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -280,18 +280,13 @@ public class SpatializerHelper { } // for both transaural / binaural, we are not forcing enablement as the init() method // could have been called another time after boot in case of audioserver restart - if (mTransauralSupported) { - // not force-enabling as this device might already be in the device list - addCompatibleAudioDevice( - new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""), - false /*forceEnable*/); - } - if (mBinauralSupported) { - // not force-enabling as this device might already be in the device list - addCompatibleAudioDevice( - new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""), - false /*forceEnable*/); - } + addCompatibleAudioDevice( + new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""), + false /*forceEnable*/); + // not force-enabling as this device might already be in the device list + addCompatibleAudioDevice( + new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""), + false /*forceEnable*/); } catch (RemoteException e) { resetCapabilities(); } finally { @@ -497,10 +492,9 @@ public class SpatializerHelper { synchronized @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() { // build unionOf(mCompatibleAudioDevices, mEnabledDevice) - mDisabledAudioDevices ArrayList<AudioDeviceAttributes> compatList = new ArrayList<>(); - for (SADeviceState dev : mSADevices) { - if (dev.mEnabled) { - compatList.add(new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, - dev.mDeviceType, dev.mDeviceAddress == null ? "" : dev.mDeviceAddress)); + for (SADeviceState deviceState : mSADevices) { + if (deviceState.mEnabled) { + compatList.add(deviceState.getAudioDeviceAttributes()); } } return compatList; @@ -521,15 +515,15 @@ public class SpatializerHelper { */ private void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada, boolean forceEnable) { + if (!isDeviceCompatibleWithSpatializationModes(ada)) { + return; + } loglogi("addCompatibleAudioDevice: dev=" + ada); - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); boolean isInList = false; SADeviceState deviceUpdated = null; // non-null on update. for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (!wireless || ada.getAddress().equals(deviceState.mDeviceAddress))) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { isInList = true; if (forceEnable) { deviceState.mEnabled = true; @@ -539,11 +533,10 @@ public class SpatializerHelper { } } if (!isInList) { - final SADeviceState dev = new SADeviceState(deviceType, - wireless ? ada.getAddress() : ""); - dev.mEnabled = true; - mSADevices.add(dev); - deviceUpdated = dev; + final SADeviceState deviceState = new SADeviceState(ada.getType(), ada.getAddress()); + deviceState.mEnabled = true; + mSADevices.add(deviceState); + deviceUpdated = deviceState; } if (deviceUpdated != null) { onRoutingUpdated(); @@ -574,13 +567,10 @@ public class SpatializerHelper { synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { loglogi("removeCompatibleAudioDevice: dev=" + ada); - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); SADeviceState deviceUpdated = null; // non-null on update. for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (!wireless || ada.getAddress().equals(deviceState.mDeviceAddress))) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { deviceState.mEnabled = false; deviceUpdated = deviceState; break; @@ -602,10 +592,9 @@ public class SpatializerHelper { // if not a wireless device, this value will be overwritten to map the type // to TYPE_BUILTIN_SPEAKER or TYPE_WIRED_HEADPHONES @AudioDeviceInfo.AudioDeviceType int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); // if not a wireless device: find if media device is in the speaker, wired headphones - if (!wireless) { + if (!isWireless(deviceType)) { // is the device type capable of doing SA? if (!mSACapableDeviceTypes.contains(deviceType)) { Log.i(TAG, "Device incompatible with Spatial Audio dev:" + ada); @@ -640,9 +629,7 @@ public class SpatializerHelper { boolean enabled = false; boolean available = false; for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { available = true; enabled = deviceState.mEnabled; break; @@ -652,11 +639,12 @@ public class SpatializerHelper { } private synchronized void addWirelessDeviceIfNew(@NonNull AudioDeviceAttributes ada) { + if (!isDeviceCompatibleWithSpatializationModes(ada)) { + return; + } boolean knownDevice = false; for (SADeviceState deviceState : mSADevices) { - // wireless device so always check address - if (ada.getType() == deviceState.mDeviceType - && ada.getAddress().equals(deviceState.mDeviceAddress)) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { knownDevice = true; break; } @@ -704,13 +692,8 @@ public class SpatializerHelper { if (ada.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) { return false; } - - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { return true; } } @@ -719,12 +702,19 @@ public class SpatializerHelper { private synchronized boolean canBeSpatializedOnDevice(@NonNull AudioAttributes attributes, @NonNull AudioFormat format, @NonNull AudioDeviceAttributes[] devices) { - final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(devices[0].getType(), + if (isDeviceCompatibleWithSpatializationModes(devices[0])) { + return AudioSystem.canBeSpatialized(attributes, format, devices); + } + return false; + } + + private boolean isDeviceCompatibleWithSpatializationModes(@NonNull AudioDeviceAttributes ada) { + final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(ada.getType(), /*default when type not found*/ SpatializationMode.SPATIALIZER_BINAURAL); if ((modeForDevice == SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported) || (modeForDevice == SpatializationMode.SPATIALIZER_TRANSAURAL && mTransauralSupported)) { - return AudioSystem.canBeSpatialized(attributes, format, devices); + return true; } return false; } @@ -1089,13 +1079,8 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, ignoring setHeadTrackerEnabled to " + enabled + " for " + ada); } - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); - for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { if (!deviceState.mHasHeadTracker) { Log.e(TAG, "Called setHeadTrackerEnabled enabled:" + enabled + " device:" + ada + " on a device without headtracker"); @@ -1109,7 +1094,7 @@ public class SpatializerHelper { } } // check current routing to see if it affects the headtracking mode - if (ROUTING_DEVICES[0].getType() == deviceType + if (ROUTING_DEVICES[0].getType() == ada.getType() && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) { setDesiredHeadTrackingMode(enabled ? mDesiredHeadTrackingModeWhenEnabled : Spatializer.HEAD_TRACKING_MODE_DISABLED); @@ -1121,13 +1106,8 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, hasHeadTracker always false for " + ada); return false; } - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); - for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { return deviceState.mHasHeadTracker; } } @@ -1144,13 +1124,8 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, setHasHeadTracker always false for " + ada); return false; } - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); - for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { if (!deviceState.mHasHeadTracker) { deviceState.mHasHeadTracker = true; mAudioService.persistSpatialAudioDeviceSettings(); @@ -1168,13 +1143,8 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, isHeadTrackerEnabled always false for " + ada); return false; } - final int deviceType = ada.getType(); - final boolean wireless = isWireless(deviceType); - for (SADeviceState deviceState : mSADevices) { - if (deviceType == deviceState.mDeviceType - && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) - || !wireless) { + if (deviceState.matchesAudioDeviceAttributes(ada)) { if (!deviceState.mHasHeadTracker) { return false; } @@ -1531,7 +1501,7 @@ public class SpatializerHelper { SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @NonNull String address) { mDeviceType = deviceType; - mDeviceAddress = Objects.requireNonNull(address); + mDeviceAddress = isWireless(deviceType) ? Objects.requireNonNull(address) : ""; } @Override @@ -1599,6 +1569,18 @@ public class SpatializerHelper { return null; } } + + public AudioDeviceAttributes getAudioDeviceAttributes() { + return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, + mDeviceType, mDeviceAddress == null ? "" : mDeviceAddress); + } + + public boolean matchesAudioDeviceAttributes(AudioDeviceAttributes ada) { + final int deviceType = ada.getType(); + final boolean wireless = isWireless(deviceType); + return (deviceType == mDeviceType) + && (!wireless || ada.getAddress().equals(mDeviceAddress)); + } } /*package*/ synchronized String getSADeviceSettings() { @@ -1619,7 +1601,9 @@ public class SpatializerHelper { // small list, not worth overhead of Arrays.stream(devSettings) for (String setting : devSettings) { SADeviceState devState = SADeviceState.fromPersistedString(setting); - if (devState != null) { + if (devState != null + && isDeviceCompatibleWithSpatializationModes( + devState.getAudioDeviceAttributes())) { mSADevices.add(devState); logDeviceState(devState, "setSADeviceSettings"); } diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index cc49f07dd0e5..41ca13f5d5f5 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -538,13 +538,12 @@ public final class AuthSession implements IBinder.DeathRecipient { void onDialogAnimatedIn() { if (mState != STATE_AUTH_STARTED) { - Slog.w(TAG, "onDialogAnimatedIn, unexpected state: " + mState); + Slog.e(TAG, "onDialogAnimatedIn, unexpected state: " + mState); + return; } mState = STATE_AUTH_STARTED_UI_SHOWING; - startAllPreparedFingerprintSensors(); - mState = STATE_AUTH_STARTED_UI_SHOWING; } void onTryAgainPressed() { diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java index 968146a166ed..ef2931ff5850 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java @@ -20,14 +20,18 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.biometrics.BiometricConstants; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.function.BooleanSupplier; /** * Contains all the necessary information for a HAL operation. @@ -84,6 +88,8 @@ public class BiometricSchedulerOperation { private final BaseClientMonitor mClientMonitor; @Nullable private final ClientMonitorCallback mClientCallback; + @NonNull + private final BooleanSupplier mIsDebuggable; @Nullable private ClientMonitorCallback mOnStartCallback; @OperationState @@ -99,14 +105,33 @@ public class BiometricSchedulerOperation { this(clientMonitor, callback, STATE_WAITING_IN_QUEUE); } + @VisibleForTesting + BiometricSchedulerOperation( + @NonNull BaseClientMonitor clientMonitor, + @Nullable ClientMonitorCallback callback, + @NonNull BooleanSupplier isDebuggable + ) { + this(clientMonitor, callback, STATE_WAITING_IN_QUEUE, isDebuggable); + } + protected BiometricSchedulerOperation( @NonNull BaseClientMonitor clientMonitor, @Nullable ClientMonitorCallback callback, @OperationState int state ) { + this(clientMonitor, callback, state, Build::isDebuggable); + } + + private BiometricSchedulerOperation( + @NonNull BaseClientMonitor clientMonitor, + @Nullable ClientMonitorCallback callback, + @OperationState int state, + @NonNull BooleanSupplier isDebuggable + ) { mClientMonitor = clientMonitor; mClientCallback = callback; mState = state; + mIsDebuggable = isDebuggable; mCancelWatchdog = () -> { if (!isFinished()) { Slog.e(TAG, "[Watchdog Triggered]: " + this); @@ -144,13 +169,19 @@ public class BiometricSchedulerOperation { * @return if this operation started */ public boolean start(@NonNull ClientMonitorCallback callback) { - checkInState("start", + if (errorWhenNoneOf("start", STATE_WAITING_IN_QUEUE, STATE_WAITING_FOR_COOKIE, - STATE_WAITING_IN_QUEUE_CANCELING); + STATE_WAITING_IN_QUEUE_CANCELING)) { + return false; + } if (mClientMonitor.getCookie() != 0) { - throw new IllegalStateException("operation requires cookie"); + String err = "operation requires cookie"; + if (mIsDebuggable.getAsBoolean()) { + throw new IllegalStateException(err); + } + Slog.e(TAG, err); } return doStart(callback); @@ -164,16 +195,18 @@ public class BiometricSchedulerOperation { * @return if this operation started */ public boolean startWithCookie(@NonNull ClientMonitorCallback callback, int cookie) { - checkInState("start", - STATE_WAITING_IN_QUEUE, - STATE_WAITING_FOR_COOKIE, - STATE_WAITING_IN_QUEUE_CANCELING); - if (mClientMonitor.getCookie() != cookie) { Slog.e(TAG, "Mismatched cookie for operation: " + this + ", received: " + cookie); return false; } + if (errorWhenNoneOf("start", + STATE_WAITING_IN_QUEUE, + STATE_WAITING_FOR_COOKIE, + STATE_WAITING_IN_QUEUE_CANCELING)) { + return false; + } + return doStart(callback); } @@ -217,10 +250,12 @@ public class BiometricSchedulerOperation { * immediately abort the operation and notify the client that it has finished unsuccessfully. */ public void abort() { - checkInState("cannot abort a non-pending operation", + if (errorWhenNoneOf("abort", STATE_WAITING_IN_QUEUE, STATE_WAITING_FOR_COOKIE, - STATE_WAITING_IN_QUEUE_CANCELING); + STATE_WAITING_IN_QUEUE_CANCELING)) { + return; + } if (isHalOperation()) { ((HalClientMonitor<?>) mClientMonitor).unableToStart(); @@ -247,7 +282,9 @@ public class BiometricSchedulerOperation { * the callback used from {@link #start(ClientMonitorCallback)} is used) */ public void cancel(@NonNull Handler handler, @NonNull ClientMonitorCallback callback) { - checkNotInState("cancel", STATE_FINISHED); + if (errorWhenOneOf("cancel", STATE_FINISHED)) { + return; + } final int currentState = mState; if (!isInterruptable()) { @@ -402,21 +439,28 @@ public class BiometricSchedulerOperation { return mClientMonitor; } - private void checkNotInState(String message, @OperationState int... states) { - for (int state : states) { - if (mState == state) { - throw new IllegalStateException(message + ": illegal state= " + state); + private boolean errorWhenOneOf(String op, @OperationState int... states) { + final boolean isError = ArrayUtils.contains(states, mState); + if (isError) { + String err = op + ": mState must not be " + mState; + if (mIsDebuggable.getAsBoolean()) { + throw new IllegalStateException(err); } + Slog.e(TAG, err); } + return isError; } - private void checkInState(String message, @OperationState int... states) { - for (int state : states) { - if (mState == state) { - return; + private boolean errorWhenNoneOf(String op, @OperationState int... states) { + final boolean isError = !ArrayUtils.contains(states, mState); + if (isError) { + String err = op + ": mState=" + mState + " must be one of " + Arrays.toString(states); + if (mIsDebuggable.getAsBoolean()) { + throw new IllegalStateException(err); } + Slog.e(TAG, err); } - throw new IllegalStateException(message + ": illegal state= " + mState); + return isError; } @Override diff --git a/services/core/java/com/android/server/broadcastradio/OWNERS b/services/core/java/com/android/server/broadcastradio/OWNERS index 3e360e7e992c..d2bdd643b0a2 100644 --- a/services/core/java/com/android/server/broadcastradio/OWNERS +++ b/services/core/java/com/android/server/broadcastradio/OWNERS @@ -1,3 +1,3 @@ -keunyoung@google.com +xuweilin@google.com oscarazu@google.com -twasilczyk@google.com +keunyoung@google.com diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 845e932cdff9..5b282ced73b5 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -3301,8 +3301,13 @@ public class Vpn { cancelHandleNetworkLostTimeout(); synchronized (Vpn.this) { + String category = null; + int errorClass = -1; + int errorCode = -1; if (exception instanceof IkeProtocolException) { final IkeProtocolException ikeException = (IkeProtocolException) exception; + category = VpnManager.CATEGORY_EVENT_IKE_ERROR; + errorCode = ikeException.getErrorType(); switch (ikeException.getErrorType()) { case IkeProtocolException.ERROR_TYPE_NO_PROPOSAL_CHOSEN: // Fallthrough @@ -3312,105 +3317,53 @@ public class Vpn { case IkeProtocolException.ERROR_TYPE_FAILED_CP_REQUIRED: // Fallthrough case IkeProtocolException.ERROR_TYPE_TS_UNACCEPTABLE: // All the above failures are configuration errors, and are terminal - // TODO(b/230548427): Remove SDK check once VPN related stuff are - // decoupled from ConnectivityServiceTest. - if (SdkLevel.isAtLeastT()) { - sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_IKE_ERROR, - VpnManager.ERROR_CLASS_NOT_RECOVERABLE, - ikeException.getErrorType(), - getPackage(), mSessionKey, makeVpnProfileStateLocked(), - network, - getRedactedNetworkCapabilitiesOfUnderlyingNetwork( - mUnderlyingNetworkCapabilities), - getRedactedLinkPropertiesOfUnderlyingNetwork( - mUnderlyingLinkProperties)); - } - markFailedAndDisconnect(exception); - return; + errorClass = VpnManager.ERROR_CLASS_NOT_RECOVERABLE; + break; // All other cases possibly recoverable. default: // All the above failures are configuration errors, and are terminal - // TODO(b/230548427): Remove SDK check once VPN related stuff are - // decoupled from ConnectivityServiceTest. - if (SdkLevel.isAtLeastT()) { - sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_IKE_ERROR, - VpnManager.ERROR_CLASS_RECOVERABLE, - ikeException.getErrorType(), - getPackage(), mSessionKey, makeVpnProfileStateLocked(), - network, - getRedactedNetworkCapabilitiesOfUnderlyingNetwork( - mUnderlyingNetworkCapabilities), - getRedactedLinkPropertiesOfUnderlyingNetwork( - mUnderlyingLinkProperties)); - } + errorClass = VpnManager.ERROR_CLASS_RECOVERABLE; } } else if (exception instanceof IllegalArgumentException) { // Failed to build IKE/ChildSessionParams; fatal profile configuration error markFailedAndDisconnect(exception); return; } else if (exception instanceof IkeNetworkLostException) { - // TODO(b/230548427): Remove SDK check once VPN related stuff are - // decoupled from ConnectivityServiceTest. - if (SdkLevel.isAtLeastT()) { - sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_NETWORK_ERROR, - VpnManager.ERROR_CLASS_RECOVERABLE, - VpnManager.ERROR_CODE_NETWORK_LOST, - getPackage(), mSessionKey, makeVpnProfileStateLocked(), - network, - getRedactedNetworkCapabilitiesOfUnderlyingNetwork( - mUnderlyingNetworkCapabilities), - getRedactedLinkPropertiesOfUnderlyingNetwork( - mUnderlyingLinkProperties)); - } + category = VpnManager.CATEGORY_EVENT_NETWORK_ERROR; + errorClass = VpnManager.ERROR_CLASS_RECOVERABLE; + errorCode = VpnManager.ERROR_CODE_NETWORK_LOST; } else if (exception instanceof IkeNonProtocolException) { + category = VpnManager.CATEGORY_EVENT_NETWORK_ERROR; + errorClass = VpnManager.ERROR_CLASS_RECOVERABLE; if (exception.getCause() instanceof UnknownHostException) { - // TODO(b/230548427): Remove SDK check once VPN related stuff are - // decoupled from ConnectivityServiceTest. - if (SdkLevel.isAtLeastT()) { - sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_NETWORK_ERROR, - VpnManager.ERROR_CLASS_RECOVERABLE, - VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST, - getPackage(), mSessionKey, makeVpnProfileStateLocked(), - network, - getRedactedNetworkCapabilitiesOfUnderlyingNetwork( - mUnderlyingNetworkCapabilities), - getRedactedLinkPropertiesOfUnderlyingNetwork( - mUnderlyingLinkProperties)); - } + errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST; } else if (exception.getCause() instanceof IkeTimeoutException) { - // TODO(b/230548427): Remove SDK check once VPN related stuff are - // decoupled from ConnectivityServiceTest. - if (SdkLevel.isAtLeastT()) { - sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_NETWORK_ERROR, - VpnManager.ERROR_CLASS_RECOVERABLE, - VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT, - getPackage(), mSessionKey, makeVpnProfileStateLocked(), - network, - getRedactedNetworkCapabilitiesOfUnderlyingNetwork( - mUnderlyingNetworkCapabilities), - getRedactedLinkPropertiesOfUnderlyingNetwork( - mUnderlyingLinkProperties)); - } + errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT; } else if (exception.getCause() instanceof IOException) { - // TODO(b/230548427): Remove SDK check once VPN related stuff are - // decoupled from ConnectivityServiceTest. - if (SdkLevel.isAtLeastT()) { - sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_NETWORK_ERROR, - VpnManager.ERROR_CLASS_RECOVERABLE, - VpnManager.ERROR_CODE_NETWORK_IO, - getPackage(), mSessionKey, makeVpnProfileStateLocked(), - network, - getRedactedNetworkCapabilitiesOfUnderlyingNetwork( - mUnderlyingNetworkCapabilities), - getRedactedLinkPropertiesOfUnderlyingNetwork( - mUnderlyingLinkProperties)); - } + errorCode = VpnManager.ERROR_CODE_NETWORK_IO; } } else if (exception != null) { Log.wtf(TAG, "onSessionLost: exception = " + exception); } - scheduleRetryNewIkeSession(); + // TODO(b/230548427): Remove SDK check once VPN related stuff are + // decoupled from ConnectivityServiceTest. + if (SdkLevel.isAtLeastT() && category != null) { + sendEventToVpnManagerApp(category, errorClass, errorCode, + getPackage(), mSessionKey, makeVpnProfileStateLocked(), + mActiveNetwork, + getRedactedNetworkCapabilitiesOfUnderlyingNetwork( + mUnderlyingNetworkCapabilities), + getRedactedLinkPropertiesOfUnderlyingNetwork( + mUnderlyingLinkProperties)); + } + + if (errorClass == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) { + markFailedAndDisconnect(exception); + return; + } else { + scheduleRetryNewIkeSession(); + } } mUnderlyingNetworkCapabilities = null; diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index 16bffd9a597b..1de1a7a5e1e5 100755 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -1262,6 +1262,7 @@ abstract class HdmiCecLocalDevice { boolean initiatedByCec, final PendingActionClearedCallback originalCallback) { removeAction(AbsoluteVolumeAudioStatusAction.class); removeAction(SetAudioVolumeLevelDiscoveryAction.class); + removeAction(ActiveSourceAction.class); mPendingActionClearedCallback = new PendingActionClearedCallback() { diff --git a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java index 3dcf72eb38d6..30a4f3134ffd 100644 --- a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java +++ b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java @@ -91,7 +91,8 @@ final class OneTouchPlayAction extends HdmiCecFeatureAction { boolean is20TargetOnBefore = mIsCec20 && getTargetDevicePowerStatus(mSource, mTargetAddress, HdmiControlManager.POWER_STATUS_UNKNOWN) == HdmiControlManager.POWER_STATUS_ON; - broadcastActiveSource(); + // Make the device the active source. + setAndBroadcastActiveSource(); // If the device is not an audio system itself, request the connected audio system to // turn on. if (shouldTurnOnConnectedAudioSystem()) { @@ -108,9 +109,11 @@ final class OneTouchPlayAction extends HdmiCecFeatureAction { queryDevicePowerStatus(); } else if (targetPowerStatus == HdmiControlManager.POWER_STATUS_ON) { if (!is20TargetOnBefore) { + // If the device is still the active source, send the <Active Source> message + // again. // Suppress 2nd <Active Source> message if the target device was already on when // the 1st one was sent. - broadcastActiveSource(); + maySendActiveSource(); } finishWithCallback(HdmiControlManager.RESULT_SUCCESS); return true; @@ -121,7 +124,9 @@ final class OneTouchPlayAction extends HdmiCecFeatureAction { return true; } - private void broadcastActiveSource() { + private void setAndBroadcastActiveSource() { + // If the device wasn´t the active source yet, + // this makes it the active source and wakes it up. mSource.mService.setAndBroadcastActiveSourceFromOneDeviceType( mTargetAddress, getSourcePath(), "OneTouchPlayAction#broadcastActiveSource()"); // When OneTouchPlay is called, client side should be responsible to send out the intent @@ -135,6 +140,11 @@ final class OneTouchPlayAction extends HdmiCecFeatureAction { mSource.setLocalActivePort(Constants.CEC_SWITCH_HOME); } + private void maySendActiveSource() { + // Only send <Active Source> if the device is already the active source at this time. + mSource.maySendActiveSource(mTargetAddress); + } + private void queryDevicePowerStatus() { sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(), mTargetAddress)); @@ -149,7 +159,9 @@ final class OneTouchPlayAction extends HdmiCecFeatureAction { if (cmd.getOpcode() == Constants.MESSAGE_REPORT_POWER_STATUS) { int status = cmd.getParams()[0]; if (status == HdmiControlManager.POWER_STATUS_ON) { - broadcastActiveSource(); + // If the device is still the active source, send the <Active Source> message + // again. + maySendActiveSource(); finishWithCallback(HdmiControlManager.RESULT_SUCCESS); } return true; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java new file mode 100644 index 000000000000..68753ab909b3 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import static com.android.server.inputmethod.SubtypeUtils.SUBTYPE_MODE_ANY; +import static com.android.server.inputmethod.SubtypeUtils.SUBTYPE_MODE_KEYBOARD; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; + +/** + * This class provides utility methods to generate or filter {@link InputMethodInfo} for + * {@link InputMethodManagerService}. + * + * <p>This class is intentionally package-private. Utility methods here are tightly coupled with + * implementation details in {@link InputMethodManagerService}. Hence this class is not suitable + * for other components to directly use.</p> + */ +final class InputMethodInfoUtils { + private static final String TAG = "InputMethodInfoUtils"; + + /** + * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs + * that are mainly used until the system becomes ready. Note that {@link Locale} in this array + * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH} + * doesn't automatically match {@code Locale("en", "IN")}. + */ + private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = { + Locale.ENGLISH, // "en" + Locale.US, // "en_US" + Locale.UK, // "en_GB" + }; + private static final Locale ENGLISH_LOCALE = new Locale("en"); + + private static final class InputMethodListBuilder { + // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration + // order can have non-trivial effect in the call sites. + @NonNull + private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>(); + + InputMethodListBuilder fillImes(ArrayList<InputMethodInfo> imis, Context context, + boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry, + String requiredSubtypeMode) { + for (int i = 0; i < imis.size(); ++i) { + final InputMethodInfo imi = imis.get(i); + if (isSystemImeThatHasSubtypeOf(imi, context, + checkDefaultAttribute, locale, checkCountry, requiredSubtypeMode)) { + mInputMethodSet.add(imi); + } + } + return this; + } + + // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be + // documented more clearly. + InputMethodListBuilder fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context) { + // If one or more auxiliary input methods are available, OK to stop populating the list. + for (final InputMethodInfo imi : mInputMethodSet) { + if (imi.isAuxiliaryIme()) { + return this; + } + } + boolean added = false; + for (int i = 0; i < imis.size(); ++i) { + final InputMethodInfo imi = imis.get(i); + if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, + true /* checkDefaultAttribute */)) { + mInputMethodSet.add(imi); + added = true; + } + } + if (added) { + return this; + } + for (int i = 0; i < imis.size(); ++i) { + final InputMethodInfo imi = imis.get(i); + if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, + false /* checkDefaultAttribute */)) { + mInputMethodSet.add(imi); + } + } + return this; + + } + + public boolean isEmpty() { + return mInputMethodSet.isEmpty(); + } + + @NonNull + public ArrayList<InputMethodInfo> build() { + return new ArrayList<>(mInputMethodSet); + } + } + + private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale( + ArrayList<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale, + @Nullable Locale fallbackLocale) { + // Once the system becomes ready, we pick up at least one keyboard in the following order. + // Secondary users fall into this category in general. + // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true + // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false + // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true + // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false + // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true + // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false + // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. + + final InputMethodListBuilder builder = new InputMethodListBuilder(); + builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) + + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale); + return builder; + } + + static ArrayList<InputMethodInfo> getDefaultEnabledImes( + Context context, ArrayList<InputMethodInfo> imis, boolean onlyMinimum) { + final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context); + // We will primarily rely on the system locale, but also keep relying on the fallback locale + // as a last resort. + // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs), + // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic" + // subtype) + final Locale systemLocale = LocaleUtils.getSystemLocaleFromContext(context); + final InputMethodListBuilder builder = + getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale); + if (!onlyMinimum) { + builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, + true /* checkCountry */, SUBTYPE_MODE_ANY) + .fillAuxiliaryImes(imis, context); + } + return builder.build(); + } + + static ArrayList<InputMethodInfo> getDefaultEnabledImes( + Context context, ArrayList<InputMethodInfo> imis) { + return getDefaultEnabledImes(context, imis, false /* onlyMinimum */); + } + + /** + * Chooses an eligible system voice IME from the given IMEs. + * + * @param methodMap Map from the IME ID to {@link InputMethodInfo}. + * @param systemSpeechRecognizerPackageName System speech recognizer configured by the system + * config. + * @param currentDefaultVoiceImeId IME ID currently set to + * {@link Settings.Secure#DEFAULT_VOICE_INPUT_METHOD} + * @return {@link InputMethodInfo} that is found in {@code methodMap} and most suitable for + * the system voice IME. + */ + @Nullable + static InputMethodInfo chooseSystemVoiceIme( + @NonNull ArrayMap<String, InputMethodInfo> methodMap, + @Nullable String systemSpeechRecognizerPackageName, + @Nullable String currentDefaultVoiceImeId) { + if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { + return null; + } + final InputMethodInfo defaultVoiceIme = methodMap.get(currentDefaultVoiceImeId); + // If the config matches the package of the setting, use the current one. + if (defaultVoiceIme != null && defaultVoiceIme.isSystem() + && defaultVoiceIme.getPackageName().equals(systemSpeechRecognizerPackageName)) { + return defaultVoiceIme; + } + InputMethodInfo firstMatchingIme = null; + final int methodCount = methodMap.size(); + for (int i = 0; i < methodCount; ++i) { + final InputMethodInfo imi = methodMap.valueAt(i); + if (!imi.isSystem()) { + continue; + } + if (!TextUtils.equals(imi.getPackageName(), systemSpeechRecognizerPackageName)) { + continue; + } + if (firstMatchingIme != null) { + Slog.e(TAG, "At most one InputMethodService can be published in " + + "systemSpeechRecognizer: " + systemSpeechRecognizerPackageName + + ". Ignoring all of them."); + return null; + } + firstMatchingIme = imi; + } + return firstMatchingIme; + } + + static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) { + if (enabledImes == null || enabledImes.isEmpty()) { + return null; + } + // We'd prefer to fall back on a system IME, since that is safer. + int i = enabledImes.size(); + int firstFoundSystemIme = -1; + while (i > 0) { + i--; + final InputMethodInfo imi = enabledImes.get(i); + if (imi.isAuxiliaryIme()) { + continue; + } + if (imi.isSystem() && SubtypeUtils.containsSubtypeOf(imi, ENGLISH_LOCALE, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { + return imi; + } + if (firstFoundSystemIme < 0 && imi.isSystem()) { + firstFoundSystemIme = i; + } + } + return enabledImes.get(Math.max(firstFoundSystemIme, 0)); + } + + private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi, + Context context, boolean checkDefaultAttribute) { + if (!imi.isSystem()) { + return false; + } + if (checkDefaultAttribute && !imi.isDefault(context)) { + return false; + } + if (!imi.isAuxiliaryIme()) { + return false; + } + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype s = imi.getSubtypeAt(i); + if (s.overridesImplicitlyEnabledSubtype()) { + return true; + } + } + return false; + } + + @Nullable + private static Locale getFallbackLocaleForDefaultIme(ArrayList<InputMethodInfo> imis, + Context context) { + // At first, find the fallback locale from the IMEs that are declared as "default" in the + // current locale. Note that IME developers can declare an IME as "default" only for + // some particular locales but "not default" for other locales. + for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { + for (int i = 0; i < imis.size(); ++i) { + if (isSystemImeThatHasSubtypeOf(imis.get(i), context, + true /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SubtypeUtils.SUBTYPE_MODE_KEYBOARD)) { + return fallbackLocale; + } + } + } + // If no fallback locale is found in the above condition, find fallback locales regardless + // of the "default" attribute as a last resort. + for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { + for (int i = 0; i < imis.size(); ++i) { + if (isSystemImeThatHasSubtypeOf(imis.get(i), context, + false /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SubtypeUtils.SUBTYPE_MODE_KEYBOARD)) { + return fallbackLocale; + } + } + } + Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray())); + return null; + } + + private static boolean isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context, + boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry, + String requiredSubtypeMode) { + if (!imi.isSystem()) { + return false; + } + if (checkDefaultAttribute && !imi.isDefault(context)) { + return false; + } + if (!SubtypeUtils.containsSubtypeOf(imi, requiredLocale, checkCountry, + requiredSubtypeMode)) { + return false; + } + return true; + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index fa6a608d9d31..b45dc7ff0f6b 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1787,7 +1787,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (selectedMethodId != null && !mMethodMap.get(selectedMethodId).isSystem()) { return; } - final List<InputMethodInfo> suitableImes = InputMethodUtils.getDefaultEnabledImes( + final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes( context, mSettings.getEnabledInputMethodListLocked()); if (suitableImes.isEmpty()) { Slog.i(TAG, "No default found"); @@ -2108,9 +2108,24 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @Override - public boolean isStylusHandwritingAvailable() { + public boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) { + if (UserHandle.getCallingUserId() != userId) { + mContext.enforceCallingPermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); + } + synchronized (ImfLock.class) { - return mBindingController.supportsStylusHandwriting(); + if (userId == mSettings.getCurrentUserId()) { + return mBindingController.supportsStylusHandwriting(); + } + + //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList. + //TODO(b/210039666): use cache. + final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); + final InputMethodSettings settings = new InputMethodSettings(mContext.getResources(), + mContext.getContentResolver(), methodMap, userId, true); + final InputMethodInfo imi = methodMap.get(settings.getSelectedInputMethod()); + return imi != null && imi.supportsStylusHandwriting(); } } @@ -2476,7 +2491,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // We don't start input when session for a11y is created. We start input when // input method start input, a11y manager service is always on. if (startInputReason != StartInputReason.SESSION_CREATED_BY_ACCESSIBILITY) { - final Binder startInputToken = new Binder(); setEnabledSessionForAccessibilityLocked(mCurClient.mAccessibilitySessions); AccessibilityManagerInternal.get().startInput(mCurRemoteAccessibilityInputConnection, mCurEditorInfo, !initial /* restarting */); @@ -4049,7 +4063,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (subtype != null) { setInputMethodWithSubtypeIdLocked(token, id, - InputMethodUtils.getSubtypeIdFromHashCode(mMethodMap.get(id), + SubtypeUtils.getSubtypeIdFromHashCode(mMethodMap.get(id), subtype.hashCode())); } else { setInputMethod(token, id); @@ -4093,7 +4107,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // defined, there is no need to switch to the last IME. if (!imiIdIsSame || lastSubtypeHash != currentSubtypeHash) { targetLastImiId = lastIme.first; - subtypeId = InputMethodUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash); + subtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash); } } @@ -4112,13 +4126,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final InputMethodInfo imi = enabled.get(i); if (imi.getSubtypeCount() > 0 && imi.isSystem()) { InputMethodSubtype keyboardSubtype = - InputMethodUtils.findLastResortApplicableSubtypeLocked(mRes, - InputMethodUtils.getSubtypes(imi), - InputMethodUtils.SUBTYPE_MODE_KEYBOARD, locale, true); + SubtypeUtils.findLastResortApplicableSubtypeLocked(mRes, + SubtypeUtils.getSubtypes(imi), + SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (keyboardSubtype != null) { targetLastImiId = imi.getId(); - subtypeId = InputMethodUtils.getSubtypeIdFromHashCode( - imi, keyboardSubtype.hashCode()); + subtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, + keyboardSubtype.hashCode()); if(keyboardSubtype.getLocale().equals(locale)) { break; } @@ -4188,8 +4202,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (lastImi == null) return null; try { final int lastSubtypeHash = Integer.parseInt(lastIme.second); - final int lastSubtypeId = - InputMethodUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash); + final int lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi, + lastSubtypeHash); if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) { return null; } @@ -4868,7 +4882,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean chooseNewDefaultIMELocked() { - final InputMethodInfo imi = InputMethodUtils.getMostApplicableDefaultIME( + final InputMethodInfo imi = InputMethodInfoUtils.getMostApplicableDefaultIME( mSettings.getEnabledInputMethodListLocked()); if (imi != null) { if (DEBUG) { @@ -5011,7 +5025,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (resetDefaultEnabledIme || reenableMinimumNonAuxSystemImes) { final ArrayList<InputMethodInfo> defaultEnabledIme = - InputMethodUtils.getDefaultEnabledImes(mContext, mMethodList, + InputMethodInfoUtils.getDefaultEnabledImes(mContext, mMethodList, reenableMinimumNonAuxSystemImes); final int N = defaultEnabledIme.size(); for (int i = 0; i < N; ++i) { @@ -5067,7 +5081,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final String systemSpeechRecognizer = mContext.getString(com.android.internal.R.string.config_systemSpeechRecognizer); final String currentDefaultVoiceImeId = mSettings.getDefaultVoiceInputMethod(); - final InputMethodInfo newSystemVoiceIme = InputMethodUtils.chooseSystemVoiceIme( + final InputMethodInfo newSystemVoiceIme = InputMethodInfoUtils.chooseSystemVoiceIme( mMethodMap, systemSpeechRecognizer, currentDefaultVoiceImeId); if (newSystemVoiceIme == null) { if (DEBUG) { @@ -5193,8 +5207,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub String subtypeHashCode = mSettings.getLastSubtypeForInputMethodLocked(newDefaultIme); if (subtypeHashCode != null) { try { - lastSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode( - imi, Integer.parseInt(subtypeHashCode)); + lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, + Integer.parseInt(subtypeHashCode)); } catch (NumberFormatException e) { Slog.w(TAG, "HashCode for subtype looks broken: " + subtypeHashCode, e); } @@ -5229,7 +5243,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return null; } if (!subtypeIsSelected || mCurrentSubtype == null - || !InputMethodUtils.isValidSubtypeId(imi, mCurrentSubtype.hashCode())) { + || !SubtypeUtils.isValidSubtypeId(imi, mCurrentSubtype.hashCode())) { int subtypeId = mSettings.getSelectedInputMethodSubtypeId(selectedMethodId); if (subtypeId == NOT_A_SUBTYPE_ID) { // If there are no selected subtypes, the framework will try to find @@ -5242,17 +5256,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { mCurrentSubtype = explicitlyOrImplicitlyEnabledSubtypes.get(0); } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) { - mCurrentSubtype = InputMethodUtils.findLastResortApplicableSubtypeLocked( + mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( mRes, explicitlyOrImplicitlyEnabledSubtypes, - InputMethodUtils.SUBTYPE_MODE_KEYBOARD, null, true); + SubtypeUtils.SUBTYPE_MODE_KEYBOARD, null, true); if (mCurrentSubtype == null) { - mCurrentSubtype = InputMethodUtils.findLastResortApplicableSubtypeLocked( - mRes, explicitlyOrImplicitlyEnabledSubtypes, null, null, - true); + mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + mRes, explicitlyOrImplicitlyEnabledSubtypes, null, null, true); } } } else { - mCurrentSubtype = InputMethodUtils.getSubtypes(imi).get(subtypeId); + mCurrentSubtype = SubtypeUtils.getSubtypes(imi).get(subtypeId); } } return mCurrentSubtype; @@ -6169,8 +6182,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub setInputMethodEnabledLocked(inputMethodInfo.getId(), false); } // Re-enable with default enabled IMEs. - for (InputMethodInfo imi : - InputMethodUtils.getDefaultEnabledImes(mContext, mMethodList)) { + for (InputMethodInfo imi : InputMethodInfoUtils.getDefaultEnabledImes( + mContext, mMethodList)) { setInputMethodEnabledLocked(imi.getId(), true); } updateInputMethodsFromSettingsLocked(true /* enabledMayChange */); @@ -6191,8 +6204,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mContext.getResources(), mContext.getContentResolver(), methodMap, userId, false); - nextEnabledImes = InputMethodUtils.getDefaultEnabledImes(mContext, methodList); - nextIme = InputMethodUtils.getMostApplicableDefaultIME(nextEnabledImes).getId(); + nextEnabledImes = InputMethodInfoUtils.getDefaultEnabledImes(mContext, + methodList); + nextIme = InputMethodInfoUtils.getMostApplicableDefaultIME( + nextEnabledImes).getId(); // Reset enabled IMEs. settings.putEnabledInputMethodsStr(""); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java index c255fe14c03e..11e6923aa75a 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java @@ -105,7 +105,7 @@ final class InputMethodMenuController { if (currentSubtype != null) { final String curMethodId = mService.getSelectedMethodIdLocked(); final InputMethodInfo currentImi = mMethodMap.get(curMethodId); - lastInputMethodSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode( + lastInputMethodSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode( currentImi, currentSubtype.hashCode()); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java index f8894c64304d..a64322625797 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java @@ -241,8 +241,8 @@ final class InputMethodSubtypeSwitchingController { } private static int calculateSubtypeId(InputMethodInfo imi, InputMethodSubtype subtype) { - return subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi, - subtype.hashCode()) : NOT_A_SUBTYPE_ID; + return subtype != null ? SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()) + : NOT_A_SUBTYPE_ID; } private static class StaticRotationList { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java index 2d1a22e7552d..70132670e68e 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java @@ -27,7 +27,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Build; -import android.os.LocaleList; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; @@ -40,8 +39,6 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.textservice.SpellCheckerInfo; -import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.StartInputFlags; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; @@ -50,9 +47,7 @@ import com.android.server.textservices.TextServicesManagerInternal; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; import java.util.function.Predicate; /** @@ -66,40 +61,13 @@ import java.util.function.Predicate; final class InputMethodUtils { public static final boolean DEBUG = false; static final int NOT_A_SUBTYPE_ID = -1; - private static final String SUBTYPE_MODE_ANY = null; - static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; private static final String TAG = "InputMethodUtils"; - private static final Locale ENGLISH_LOCALE = new Locale("en"); private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID); - private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE = - "EnabledWhenDefaultIsNotAsciiCapable"; // The string for enabled input method is saved as follows: // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0") private static final char INPUT_METHOD_SEPARATOR = ':'; private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';'; - /** - * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs - * that are mainly used until the system becomes ready. Note that {@link Locale} in this array - * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH} - * doesn't automatically match {@code Locale("en", "IN")}. - */ - private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = { - Locale.ENGLISH, // "en" - Locale.US, // "en_US" - Locale.UK, // "en_GB" - }; - - // A temporary workaround for the performance concerns in - // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo). - // TODO: Optimize all the critical paths including this one. - private static final Object sCacheLock = new Object(); - @GuardedBy("sCacheLock") - private static LocaleList sCachedSystemLocales; - @GuardedBy("sCacheLock") - private static InputMethodInfo sCachedInputMethodInfo; - @GuardedBy("sCacheLock") - private static ArrayList<InputMethodSubtype> sCachedResult; private InputMethodUtils() { // This utility class is not publicly instantiable. @@ -130,533 +98,6 @@ final class InputMethodUtils { } // ---------------------------------------------------------------------- - private static boolean isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context, - boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry, - String requiredSubtypeMode) { - if (!imi.isSystem()) { - return false; - } - if (checkDefaultAttribute && !imi.isDefault(context)) { - return false; - } - if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) { - return false; - } - return true; - } - - @Nullable - private static Locale getFallbackLocaleForDefaultIme(ArrayList<InputMethodInfo> imis, - Context context) { - // At first, find the fallback locale from the IMEs that are declared as "default" in the - // current locale. Note that IME developers can declare an IME as "default" only for - // some particular locales but "not default" for other locales. - for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { - for (int i = 0; i < imis.size(); ++i) { - if (isSystemImeThatHasSubtypeOf(imis.get(i), context, - true /* checkDefaultAttribute */, fallbackLocale, - true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { - return fallbackLocale; - } - } - } - // If no fallback locale is found in the above condition, find fallback locales regardless - // of the "default" attribute as a last resort. - for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { - for (int i = 0; i < imis.size(); ++i) { - if (isSystemImeThatHasSubtypeOf(imis.get(i), context, - false /* checkDefaultAttribute */, fallbackLocale, - true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { - return fallbackLocale; - } - } - } - Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray())); - return null; - } - - private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi, - Context context, boolean checkDefaultAttribute) { - if (!imi.isSystem()) { - return false; - } - if (checkDefaultAttribute && !imi.isDefault(context)) { - return false; - } - if (!imi.isAuxiliaryIme()) { - return false; - } - final int subtypeCount = imi.getSubtypeCount(); - for (int i = 0; i < subtypeCount; ++i) { - final InputMethodSubtype s = imi.getSubtypeAt(i); - if (s.overridesImplicitlyEnabledSubtype()) { - return true; - } - } - return false; - } - - private static Locale getSystemLocaleFromContext(Context context) { - try { - return context.getResources().getConfiguration().locale; - } catch (Resources.NotFoundException ex) { - return null; - } - } - - private static final class InputMethodListBuilder { - // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration - // order can have non-trivial effect in the call sites. - @NonNull - private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>(); - - InputMethodListBuilder fillImes(ArrayList<InputMethodInfo> imis, Context context, - boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry, - String requiredSubtypeMode) { - for (int i = 0; i < imis.size(); ++i) { - final InputMethodInfo imi = imis.get(i); - if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale, - checkCountry, requiredSubtypeMode)) { - mInputMethodSet.add(imi); - } - } - return this; - } - - // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be - // documented more clearly. - InputMethodListBuilder fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context) { - // If one or more auxiliary input methods are available, OK to stop populating the list. - for (final InputMethodInfo imi : mInputMethodSet) { - if (imi.isAuxiliaryIme()) { - return this; - } - } - boolean added = false; - for (int i = 0; i < imis.size(); ++i) { - final InputMethodInfo imi = imis.get(i); - if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, - true /* checkDefaultAttribute */)) { - mInputMethodSet.add(imi); - added = true; - } - } - if (added) { - return this; - } - for (int i = 0; i < imis.size(); ++i) { - final InputMethodInfo imi = imis.get(i); - if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, - false /* checkDefaultAttribute */)) { - mInputMethodSet.add(imi); - } - } - return this; - } - - public boolean isEmpty() { - return mInputMethodSet.isEmpty(); - } - - @NonNull - public ArrayList<InputMethodInfo> build() { - return new ArrayList<>(mInputMethodSet); - } - } - - private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale( - ArrayList<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale, - @Nullable Locale fallbackLocale) { - // Once the system becomes ready, we pick up at least one keyboard in the following order. - // Secondary users fall into this category in general. - // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true - // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false - // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true - // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false - // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true - // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false - // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. - - final InputMethodListBuilder builder = new InputMethodListBuilder(); - builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, - true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, - false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, - true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, - false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, - true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, - false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) - + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale); - return builder; - } - - static ArrayList<InputMethodInfo> getDefaultEnabledImes( - Context context, ArrayList<InputMethodInfo> imis, boolean onlyMinimum) { - final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context); - // We will primarily rely on the system locale, but also keep relying on the fallback locale - // as a last resort. - // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs), - // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic" - // subtype) - final Locale systemLocale = getSystemLocaleFromContext(context); - final InputMethodListBuilder builder = - getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale); - if (!onlyMinimum) { - builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, - true /* checkCountry */, SUBTYPE_MODE_ANY) - .fillAuxiliaryImes(imis, context); - } - return builder.build(); - } - - static ArrayList<InputMethodInfo> getDefaultEnabledImes( - Context context, ArrayList<InputMethodInfo> imis) { - return getDefaultEnabledImes(context, imis, false /* onlyMinimum */); - } - - /** - * Chooses an eligible system voice IME from the given IMEs. - * - * @param methodMap Map from the IME ID to {@link InputMethodInfo}. - * @param systemSpeechRecognizerPackageName System speech recognizer configured by the system - * config. - * @param currentDefaultVoiceImeId IME ID currently set to - * {@link Settings.Secure#DEFAULT_VOICE_INPUT_METHOD} - * @return {@link InputMethodInfo} that is found in {@code methodMap} and most suitable for - * the system voice IME. - */ - @Nullable - static InputMethodInfo chooseSystemVoiceIme( - @NonNull ArrayMap<String, InputMethodInfo> methodMap, - @Nullable String systemSpeechRecognizerPackageName, - @Nullable String currentDefaultVoiceImeId) { - if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { - return null; - } - final InputMethodInfo defaultVoiceIme = methodMap.get(currentDefaultVoiceImeId); - // If the config matches the package of the setting, use the current one. - if (defaultVoiceIme != null && defaultVoiceIme.isSystem() - && defaultVoiceIme.getPackageName().equals(systemSpeechRecognizerPackageName)) { - return defaultVoiceIme; - } - InputMethodInfo firstMatchingIme = null; - final int methodCount = methodMap.size(); - for (int i = 0; i < methodCount; ++i) { - final InputMethodInfo imi = methodMap.valueAt(i); - if (!imi.isSystem()) { - continue; - } - if (!TextUtils.equals(imi.getPackageName(), systemSpeechRecognizerPackageName)) { - continue; - } - if (firstMatchingIme != null) { - Slog.e(TAG, "At most one InputMethodService can be published in " - + "systemSpeechRecognizer: " + systemSpeechRecognizerPackageName - + ". Ignoring all of them."); - return null; - } - firstMatchingIme = imi; - } - return firstMatchingIme; - } - - static boolean containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale, - boolean checkCountry, String mode) { - if (locale == null) { - return false; - } - final int N = imi.getSubtypeCount(); - for (int i = 0; i < N; ++i) { - final InputMethodSubtype subtype = imi.getSubtypeAt(i); - if (checkCountry) { - final Locale subtypeLocale = subtype.getLocaleObject(); - if (subtypeLocale == null || - !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) || - !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) { - continue; - } - } else { - final Locale subtypeLocale = new Locale(getLanguageFromLocaleString( - subtype.getLocale())); - if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) { - continue; - } - } - if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) || - mode.equalsIgnoreCase(subtype.getMode())) { - return true; - } - } - return false; - } - - static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) { - ArrayList<InputMethodSubtype> subtypes = new ArrayList<>(); - final int subtypeCount = imi.getSubtypeCount(); - for (int i = 0; i < subtypeCount; ++i) { - subtypes.add(imi.getSubtypeAt(i)); - } - return subtypes; - } - - static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) { - if (enabledImes == null || enabledImes.isEmpty()) { - return null; - } - // We'd prefer to fall back on a system IME, since that is safer. - int i = enabledImes.size(); - int firstFoundSystemIme = -1; - while (i > 0) { - i--; - final InputMethodInfo imi = enabledImes.get(i); - if (imi.isAuxiliaryIme()) { - continue; - } - if (imi.isSystem() && containsSubtypeOf( - imi, ENGLISH_LOCALE, false /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { - return imi; - } - if (firstFoundSystemIme < 0 && imi.isSystem()) { - firstFoundSystemIme = i; - } - } - return enabledImes.get(Math.max(firstFoundSystemIme, 0)); - } - - static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) { - return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID; - } - - static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) { - if (imi != null) { - final int subtypeCount = imi.getSubtypeCount(); - for (int i = 0; i < subtypeCount; ++i) { - InputMethodSubtype ims = imi.getSubtypeAt(i); - if (subtypeHashCode == ims.hashCode()) { - return i; - } - } - } - return NOT_A_SUBTYPE_ID; - } - - private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale = - new LocaleUtils.LocaleExtractor<InputMethodSubtype>() { - @Override - public Locale get(InputMethodSubtype source) { - return source != null ? source.getLocaleObject() : null; - } - }; - - @VisibleForTesting - static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( - Resources res, InputMethodInfo imi) { - final LocaleList systemLocales = res.getConfiguration().getLocales(); - - synchronized (sCacheLock) { - // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because - // it does not check if subtypes are also identical. - if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) { - return new ArrayList<>(sCachedResult); - } - } - - // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl(). - // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive - // LocaleList rather than Resource. - final ArrayList<InputMethodSubtype> result = - getImplicitlyApplicableSubtypesLockedImpl(res, imi); - synchronized (sCacheLock) { - // Both LocaleList and InputMethodInfo are immutable. No need to copy them here. - sCachedSystemLocales = systemLocales; - sCachedInputMethodInfo = imi; - sCachedResult = new ArrayList<>(result); - } - return result; - } - - private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl( - Resources res, InputMethodInfo imi) { - final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi); - final LocaleList systemLocales = res.getConfiguration().getLocales(); - final String systemLocale = systemLocales.get(0).toString(); - if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>(); - final int numSubtypes = subtypes.size(); - - // Handle overridesImplicitlyEnabledSubtype mechanism. - final ArrayMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new ArrayMap<>(); - for (int i = 0; i < numSubtypes; ++i) { - // scan overriding implicitly enabled subtypes. - final InputMethodSubtype subtype = subtypes.get(i); - if (subtype.overridesImplicitlyEnabledSubtype()) { - final String mode = subtype.getMode(); - if (!applicableModeAndSubtypesMap.containsKey(mode)) { - applicableModeAndSubtypesMap.put(mode, subtype); - } - } - } - if (applicableModeAndSubtypesMap.size() > 0) { - return new ArrayList<>(applicableModeAndSubtypesMap.values()); - } - - final ArrayMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap = - new ArrayMap<>(); - final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>(); - - for (int i = 0; i < numSubtypes; ++i) { - final InputMethodSubtype subtype = subtypes.get(i); - final String mode = subtype.getMode(); - if (SUBTYPE_MODE_KEYBOARD.equals(mode)) { - keyboardSubtypes.add(subtype); - } else { - if (!nonKeyboardSubtypesMap.containsKey(mode)) { - nonKeyboardSubtypesMap.put(mode, new ArrayList<>()); - } - nonKeyboardSubtypesMap.get(mode).add(subtype); - } - } - - final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>(); - LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales, - applicableSubtypes); - - if (!applicableSubtypes.isEmpty()) { - boolean hasAsciiCapableKeyboard = false; - final int numApplicationSubtypes = applicableSubtypes.size(); - for (int i = 0; i < numApplicationSubtypes; ++i) { - final InputMethodSubtype subtype = applicableSubtypes.get(i); - if (subtype.isAsciiCapable()) { - hasAsciiCapableKeyboard = true; - break; - } - } - if (!hasAsciiCapableKeyboard) { - final int numKeyboardSubtypes = keyboardSubtypes.size(); - for (int i = 0; i < numKeyboardSubtypes; ++i) { - final InputMethodSubtype subtype = keyboardSubtypes.get(i); - final String mode = subtype.getMode(); - if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey( - TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) { - applicableSubtypes.add(subtype); - } - } - } - } - - if (applicableSubtypes.isEmpty()) { - InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( - res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); - if (lastResortKeyboardSubtype != null) { - applicableSubtypes.add(lastResortKeyboardSubtype); - } - } - - // For each non-keyboard mode, extract subtypes with system locales. - for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) { - LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales, - applicableSubtypes); - } - - return applicableSubtypes; - } - - /** - * Returns the language component of a given locale string. - * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)} - */ - private static String getLanguageFromLocaleString(String locale) { - final int idx = locale.indexOf('_'); - if (idx < 0) { - return locale; - } else { - return locale.substring(0, idx); - } - } - - /** - * If there are no selected subtypes, tries finding the most applicable one according to the - * given locale. - * @param subtypes this function will search the most applicable subtype in subtypes - * @param mode subtypes will be filtered by mode - * @param locale subtypes will be filtered by locale - * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype, - * it will return the first subtype matched with mode - * @return the most applicable subtypeId - */ - static InputMethodSubtype findLastResortApplicableSubtypeLocked( - Resources res, List<InputMethodSubtype> subtypes, String mode, String locale, - boolean canIgnoreLocaleAsLastResort) { - if (subtypes == null || subtypes.size() == 0) { - return null; - } - if (TextUtils.isEmpty(locale)) { - locale = res.getConfiguration().locale.toString(); - } - final String language = getLanguageFromLocaleString(locale); - boolean partialMatchFound = false; - InputMethodSubtype applicableSubtype = null; - InputMethodSubtype firstMatchedModeSubtype = null; - final int N = subtypes.size(); - for (int i = 0; i < N; ++i) { - InputMethodSubtype subtype = subtypes.get(i); - final String subtypeLocale = subtype.getLocale(); - final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale); - // An applicable subtype should match "mode". If mode is null, mode will be ignored, - // and all subtypes with all modes can be candidates. - if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) { - if (firstMatchedModeSubtype == null) { - firstMatchedModeSubtype = subtype; - } - if (locale.equals(subtypeLocale)) { - // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US") - applicableSubtype = subtype; - break; - } else if (!partialMatchFound && language.equals(subtypeLanguage)) { - // Partial match (e.g. system locale is "en_US" and subtype locale is "en") - applicableSubtype = subtype; - partialMatchFound = true; - } - } - } - - if (applicableSubtype == null && canIgnoreLocaleAsLastResort) { - return firstMatchedModeSubtype; - } - - // The first subtype applicable to the system locale will be defined as the most applicable - // subtype. - if (DEBUG) { - if (applicableSubtype != null) { - Slog.d(TAG, "Applicable InputMethodSubtype was found: " - + applicableSubtype.getMode() + "," + applicableSubtype.getLocale()); - } - } - return applicableSubtype; - } - static boolean canAddToLastInputMethod(InputMethodSubtype subtype) { if (subtype == null) return true; return !subtype.isAuxiliary(); @@ -790,6 +231,7 @@ final class InputMethodUtils { /** * Utility class for putting and getting settings for InputMethod * TODO: Move all putters and getters of settings to this class. + * TODO(b/235661780): Make the setting supports multi-users. */ public static class InputMethodSettings { private final TextUtils.SimpleStringSplitter mInputMethodSplitter = @@ -967,7 +409,7 @@ final class InputMethodUtils { List<InputMethodSubtype> enabledSubtypes = getEnabledInputMethodSubtypeListLocked(imi); if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) { - enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked( context.getResources(), imi); } return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes); @@ -1198,7 +640,7 @@ final class InputMethodUtils { // are enabled implicitly, so needs to treat them to be enabled. if (imi != null && imi.getSubtypeCount() > 0) { List<InputMethodSubtype> implicitlySelectedSubtypes = - getImplicitlyApplicableSubtypesLocked(mRes, imi); + SubtypeUtils.getImplicitlyApplicableSubtypesLocked(mRes, imi); if (implicitlySelectedSubtypes != null) { final int N = implicitlySelectedSubtypes.size(); for (int i = 0; i < N; ++i) { @@ -1216,7 +658,7 @@ final class InputMethodUtils { try { final int hashCode = Integer.parseInt(subtypeHashCode); // Check whether the subtype id is valid or not - if (isValidSubtypeId(imi, hashCode)) { + if (SubtypeUtils.isValidSubtypeId(imi, hashCode)) { return s; } else { return NOT_A_SUBTYPE_ID_STR; @@ -1336,7 +778,7 @@ final class InputMethodUtils { return NOT_A_SUBTYPE_ID; } final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); - return getSubtypeIdFromHashCode(imi, subtypeHashCode); + return SubtypeUtils.getSubtypeIdFromHashCode(imi, subtypeHashCode); } void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId, diff --git a/services/core/java/com/android/server/inputmethod/LocaleUtils.java b/services/core/java/com/android/server/inputmethod/LocaleUtils.java index 7a6853a25e5b..3d02b3af6bc1 100644 --- a/services/core/java/com/android/server/inputmethod/LocaleUtils.java +++ b/services/core/java/com/android/server/inputmethod/LocaleUtils.java @@ -19,6 +19,8 @@ package com.android.server.inputmethod; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; import android.icu.util.ULocale; import android.os.LocaleList; import android.text.TextUtils; @@ -207,4 +209,25 @@ final class LocaleUtils { dest.add(sources.get(entry.mIndex)); } } + + /** + * Returns the language component of a given locale string. + * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)} + */ + static String getLanguageFromLocaleString(String locale) { + final int idx = locale.indexOf('_'); + if (idx < 0) { + return locale; + } else { + return locale.substring(0, idx); + } + } + + static Locale getSystemLocaleFromContext(Context context) { + try { + return context.getResources().getConfiguration().locale; + } catch (Resources.NotFoundException ex) { + return null; + } + } } diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java new file mode 100644 index 000000000000..eb85dd011288 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import android.annotation.Nullable; +import android.content.res.Resources; +import android.os.LocaleList; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * This class provides utility methods to handle and manage {@link InputMethodSubtype} for + * {@link InputMethodManagerService}. + * + * <p>This class is intentionally package-private. Utility methods here are tightly coupled with + * implementation details in {@link InputMethodManagerService}. Hence this class is not suitable + * for other components to directly use.</p> + */ +final class SubtypeUtils { + private static final String TAG = "SubtypeUtils"; + public static final boolean DEBUG = false; + + static final String SUBTYPE_MODE_ANY = null; + static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; + + static final int NOT_A_SUBTYPE_ID = -1; + private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE = + "EnabledWhenDefaultIsNotAsciiCapable"; + + // A temporary workaround for the performance concerns in + // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo). + // TODO: Optimize all the critical paths including this one. + // TODO(b/235661780): Make the cache supports multi-users. + private static final Object sCacheLock = new Object(); + @GuardedBy("sCacheLock") + private static LocaleList sCachedSystemLocales; + @GuardedBy("sCacheLock") + private static InputMethodInfo sCachedInputMethodInfo; + @GuardedBy("sCacheLock") + private static ArrayList<InputMethodSubtype> sCachedResult; + + static boolean containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale, + boolean checkCountry, String mode) { + if (locale == null) { + return false; + } + final int N = imi.getSubtypeCount(); + for (int i = 0; i < N; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + if (checkCountry) { + final Locale subtypeLocale = subtype.getLocaleObject(); + if (subtypeLocale == null || + !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) || + !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) { + continue; + } + } else { + final Locale subtypeLocale = new Locale(LocaleUtils.getLanguageFromLocaleString( + subtype.getLocale())); + if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) { + continue; + } + } + if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) || + mode.equalsIgnoreCase(subtype.getMode())) { + return true; + } + } + return false; + } + + static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) { + ArrayList<InputMethodSubtype> subtypes = new ArrayList<>(); + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + subtypes.add(imi.getSubtypeAt(i)); + } + return subtypes; + } + + static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) { + return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID; + } + + static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) { + if (imi != null) { + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + InputMethodSubtype ims = imi.getSubtypeAt(i); + if (subtypeHashCode == ims.hashCode()) { + return i; + } + } + } + return NOT_A_SUBTYPE_ID; + } + + private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale = + source -> source != null ? source.getLocaleObject() : null; + + @VisibleForTesting + static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( + Resources res, InputMethodInfo imi) { + final LocaleList systemLocales = res.getConfiguration().getLocales(); + + synchronized (sCacheLock) { + // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because + // it does not check if subtypes are also identical. + if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) { + return new ArrayList<>(sCachedResult); + } + } + + // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl(). + // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive + // LocaleList rather than Resource. + final ArrayList<InputMethodSubtype> result = + getImplicitlyApplicableSubtypesLockedImpl(res, imi); + synchronized (sCacheLock) { + // Both LocaleList and InputMethodInfo are immutable. No need to copy them here. + sCachedSystemLocales = systemLocales; + sCachedInputMethodInfo = imi; + sCachedResult = new ArrayList<>(result); + } + return result; + } + + private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl( + Resources res, InputMethodInfo imi) { + final List<InputMethodSubtype> subtypes = getSubtypes(imi); + final LocaleList systemLocales = res.getConfiguration().getLocales(); + final String systemLocale = systemLocales.get(0).toString(); + if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>(); + final int numSubtypes = subtypes.size(); + + // Handle overridesImplicitlyEnabledSubtype mechanism. + final ArrayMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new ArrayMap<>(); + for (int i = 0; i < numSubtypes; ++i) { + // scan overriding implicitly enabled subtypes. + final InputMethodSubtype subtype = subtypes.get(i); + if (subtype.overridesImplicitlyEnabledSubtype()) { + final String mode = subtype.getMode(); + if (!applicableModeAndSubtypesMap.containsKey(mode)) { + applicableModeAndSubtypesMap.put(mode, subtype); + } + } + } + if (applicableModeAndSubtypesMap.size() > 0) { + return new ArrayList<>(applicableModeAndSubtypesMap.values()); + } + + final ArrayMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap = + new ArrayMap<>(); + final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>(); + + for (int i = 0; i < numSubtypes; ++i) { + final InputMethodSubtype subtype = subtypes.get(i); + final String mode = subtype.getMode(); + if (SUBTYPE_MODE_KEYBOARD.equals(mode)) { + keyboardSubtypes.add(subtype); + } else { + if (!nonKeyboardSubtypesMap.containsKey(mode)) { + nonKeyboardSubtypesMap.put(mode, new ArrayList<>()); + } + nonKeyboardSubtypesMap.get(mode).add(subtype); + } + } + + final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>(); + LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales, + applicableSubtypes); + + if (!applicableSubtypes.isEmpty()) { + boolean hasAsciiCapableKeyboard = false; + final int numApplicationSubtypes = applicableSubtypes.size(); + for (int i = 0; i < numApplicationSubtypes; ++i) { + final InputMethodSubtype subtype = applicableSubtypes.get(i); + if (subtype.isAsciiCapable()) { + hasAsciiCapableKeyboard = true; + break; + } + } + if (!hasAsciiCapableKeyboard) { + final int numKeyboardSubtypes = keyboardSubtypes.size(); + for (int i = 0; i < numKeyboardSubtypes; ++i) { + final InputMethodSubtype subtype = keyboardSubtypes.get(i); + final String mode = subtype.getMode(); + if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey( + TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) { + applicableSubtypes.add(subtype); + } + } + } + } + + if (applicableSubtypes.isEmpty()) { + InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( + res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); + if (lastResortKeyboardSubtype != null) { + applicableSubtypes.add(lastResortKeyboardSubtype); + } + } + + // For each non-keyboard mode, extract subtypes with system locales. + for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) { + LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales, + applicableSubtypes); + } + + return applicableSubtypes; + } + + /** + * If there are no selected subtypes, tries finding the most applicable one according to the + * given locale. + * @param subtypes this function will search the most applicable subtype in subtypes + * @param mode subtypes will be filtered by mode + * @param locale subtypes will be filtered by locale + * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype, + * it will return the first subtype matched with mode + * @return the most applicable subtypeId + */ + static InputMethodSubtype findLastResortApplicableSubtypeLocked( + Resources res, List<InputMethodSubtype> subtypes, String mode, String locale, + boolean canIgnoreLocaleAsLastResort) { + if (subtypes == null || subtypes.size() == 0) { + return null; + } + if (TextUtils.isEmpty(locale)) { + locale = res.getConfiguration().locale.toString(); + } + final String language = LocaleUtils.getLanguageFromLocaleString(locale); + boolean partialMatchFound = false; + InputMethodSubtype applicableSubtype = null; + InputMethodSubtype firstMatchedModeSubtype = null; + final int N = subtypes.size(); + for (int i = 0; i < N; ++i) { + InputMethodSubtype subtype = subtypes.get(i); + final String subtypeLocale = subtype.getLocale(); + final String subtypeLanguage = LocaleUtils.getLanguageFromLocaleString(subtypeLocale); + // An applicable subtype should match "mode". If mode is null, mode will be ignored, + // and all subtypes with all modes can be candidates. + if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) { + if (firstMatchedModeSubtype == null) { + firstMatchedModeSubtype = subtype; + } + if (locale.equals(subtypeLocale)) { + // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US") + applicableSubtype = subtype; + break; + } else if (!partialMatchFound && language.equals(subtypeLanguage)) { + // Partial match (e.g. system locale is "en_US" and subtype locale is "en") + applicableSubtype = subtype; + partialMatchFound = true; + } + } + } + + if (applicableSubtype == null && canIgnoreLocaleAsLastResort) { + return firstMatchedModeSubtype; + } + + // The first subtype applicable to the system locale will be defined as the most applicable + // subtype. + if (DEBUG) { + if (applicableSubtype != null) { + Slog.d(TAG, "Applicable InputMethodSubtype was found: " + + applicableSubtype.getMode() + "," + applicableSubtype.getLocale()); + } + } + return applicableSubtype; + } +} diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java index 1235352b0590..549fd4918023 100644 --- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java +++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java @@ -2523,10 +2523,16 @@ public class LocationProviderManager extends filtered = locationResult; } - Location last = getLastLocationUnsafe(USER_CURRENT, PERMISSION_FINE, true, Long.MAX_VALUE); - if (last != null && locationResult.get(0).getElapsedRealtimeNanos() - < last.getElapsedRealtimeNanos()) { - Log.e(TAG, "non-monotonic location received from " + mName + " provider"); + // check for non-monotonic locations if we're not the passive manager. the passive manager + // is much more likely to see non-monotonic locations since it gets locations from all + // providers, so this error log is not very useful there. + if (mPassiveManager != null) { + Location last = getLastLocationUnsafe(USER_CURRENT, PERMISSION_FINE, true, + Long.MAX_VALUE); + if (last != null && locationResult.get(0).getElapsedRealtimeNanos() + < last.getElapsedRealtimeNanos()) { + Log.e(TAG, "non-monotonic location received from " + mName + " provider"); + } } // update last location diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java index 24bd42e7d775..3edbfe038ce6 100644 --- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java +++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java @@ -122,9 +122,9 @@ public class SyntheticPasswordManager { // 256-bit synthetic password private static final byte SYNTHETIC_PASSWORD_LENGTH = 256 / 8; - private static final int PASSWORD_SCRYPT_N = 11; - private static final int PASSWORD_SCRYPT_R = 3; - private static final int PASSWORD_SCRYPT_P = 1; + private static final int PASSWORD_SCRYPT_LOG_N = 11; + private static final int PASSWORD_SCRYPT_LOG_R = 3; + private static final int PASSWORD_SCRYPT_LOG_P = 1; private static final int PASSWORD_SALT_LENGTH = 16; private static final int PASSWORD_TOKEN_LENGTH = 32; private static final String TAG = "SyntheticPasswordManager"; @@ -192,7 +192,11 @@ public class SyntheticPasswordManager { mVersion = version; } - private byte[] derivePassword(byte[] personalization) { + /** + * Derives a subkey from the synthetic password. For v3 and later synthetic passwords the + * subkeys are 256-bit; for v1 and v2 they are 512-bit. + */ + private byte[] deriveSubkey(byte[] personalization) { if (mVersion == SYNTHETIC_PASSWORD_VERSION_V3) { return (new SP800Derive(mSyntheticPassword)) .withContext(personalization, PERSONALISATION_CONTEXT); @@ -203,28 +207,28 @@ public class SyntheticPasswordManager { } public byte[] deriveKeyStorePassword() { - return bytesToHex(derivePassword(PERSONALIZATION_KEY_STORE_PASSWORD)); + return bytesToHex(deriveSubkey(PERSONALIZATION_KEY_STORE_PASSWORD)); } public byte[] deriveGkPassword() { - return derivePassword(PERSONALIZATION_SP_GK_AUTH); + return deriveSubkey(PERSONALIZATION_SP_GK_AUTH); } public byte[] deriveDiskEncryptionKey() { - return derivePassword(PERSONALIZATION_FBE_KEY); + return deriveSubkey(PERSONALIZATION_FBE_KEY); } public byte[] deriveVendorAuthSecret() { - return derivePassword(PERSONALIZATION_AUTHSECRET_KEY); + return deriveSubkey(PERSONALIZATION_AUTHSECRET_KEY); } public byte[] derivePasswordHashFactor() { - return derivePassword(PERSONALIZATION_PASSWORD_HASH); + return deriveSubkey(PERSONALIZATION_PASSWORD_HASH); } /** Derives key used to encrypt password metrics */ public byte[] deriveMetricsKey() { - return derivePassword(PERSONALIZATION_PASSWORD_METRICS); + return deriveSubkey(PERSONALIZATION_PASSWORD_METRICS); } /** @@ -274,9 +278,8 @@ public class SyntheticPasswordManager { * AuthenticationToken.mSyntheticPassword for details on what each block means. */ private void recreate(byte[] escrowSplit0, byte[] escrowSplit1) { - mSyntheticPassword = String.valueOf(HexEncoding.encode( - SyntheticPasswordCrypto.personalisedHash( - PERSONALIZATION_SP_SPLIT, escrowSplit0, escrowSplit1))).getBytes(); + mSyntheticPassword = bytesToHex(SyntheticPasswordCrypto.personalisedHash( + PERSONALIZATION_SP_SPLIT, escrowSplit0, escrowSplit1)); } /** @@ -310,9 +313,9 @@ public class SyntheticPasswordManager { } static class PasswordData { - byte scryptN; - byte scryptR; - byte scryptP; + byte scryptLogN; + byte scryptLogR; + byte scryptLogP; public int credentialType; byte[] salt; // For GateKeeper-based credential, this is the password handle returned by GK, @@ -321,9 +324,9 @@ public class SyntheticPasswordManager { public static PasswordData create(int passwordType) { PasswordData result = new PasswordData(); - result.scryptN = PASSWORD_SCRYPT_N; - result.scryptR = PASSWORD_SCRYPT_R; - result.scryptP = PASSWORD_SCRYPT_P; + result.scryptLogN = PASSWORD_SCRYPT_LOG_N; + result.scryptLogR = PASSWORD_SCRYPT_LOG_R; + result.scryptLogP = PASSWORD_SCRYPT_LOG_P; result.credentialType = passwordType; result.salt = secureRandom(PASSWORD_SALT_LENGTH); return result; @@ -335,9 +338,9 @@ public class SyntheticPasswordManager { buffer.put(data, 0, data.length); buffer.flip(); result.credentialType = buffer.getInt(); - result.scryptN = buffer.get(); - result.scryptR = buffer.get(); - result.scryptP = buffer.get(); + result.scryptLogN = buffer.get(); + result.scryptLogR = buffer.get(); + result.scryptLogP = buffer.get(); int saltLen = buffer.getInt(); result.salt = new byte[saltLen]; buffer.get(result.salt); @@ -357,9 +360,9 @@ public class SyntheticPasswordManager { + Integer.BYTES + salt.length + Integer.BYTES + (passwordHandle != null ? passwordHandle.length : 0)); buffer.putInt(credentialType); - buffer.put(scryptN); - buffer.put(scryptR); - buffer.put(scryptP); + buffer.put(scryptLogN); + buffer.put(scryptLogR); + buffer.put(scryptLogP); buffer.putInt(salt.length); buffer.put(salt); if (passwordHandle != null && passwordHandle.length > 0) { @@ -930,26 +933,6 @@ public class SyntheticPasswordManager { private ArrayMap<Integer, ArrayMap<Long, TokenData>> tokenMap = new ArrayMap<>(); /** - * Create a token based Synthetic password for the given user. - * @return the handle of the token - */ - public long createStrongTokenBasedSyntheticPassword(byte[] token, int userId, - @Nullable EscrowTokenStateChangeCallback changeCallback) { - return createTokenBasedSyntheticPassword(token, TOKEN_TYPE_STRONG, userId, - changeCallback); - } - - /** - * Create a weak token based Synthetic password for the given user. - * @return the handle of the weak token - */ - public long createWeakTokenBasedSyntheticPassword(byte[] token, int userId, - @Nullable EscrowTokenStateChangeCallback changeCallback) { - return createTokenBasedSyntheticPassword(token, TOKEN_TYPE_WEAK, userId, - changeCallback); - } - - /** * Create a token based Synthetic password of the given type for the given user. * @return the handle of the token */ @@ -1499,8 +1482,8 @@ public class SyntheticPasswordManager { private byte[] computePasswordToken(LockscreenCredential credential, PasswordData data) { final byte[] password = credential.isNone() ? DEFAULT_PASSWORD : credential.getCredential(); - return scrypt(password, data.salt, 1 << data.scryptN, 1 << data.scryptR, 1 << data.scryptP, - PASSWORD_TOKEN_LENGTH); + return scrypt(password, data.salt, 1 << data.scryptLogN, 1 << data.scryptLogR, + 1 << data.scryptLogP, PASSWORD_TOKEN_LENGTH); } private byte[] passwordTokenToGkInput(byte[] token) { @@ -1541,18 +1524,9 @@ public class SyntheticPasswordManager { return result; } - protected static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(); - private static byte[] bytesToHex(byte[] bytes) { - if (bytes == null) { - return "null".getBytes(); - } - byte[] hexBytes = new byte[bytes.length * 2]; - for ( int j = 0; j < bytes.length; j++ ) { - int v = bytes[j] & 0xFF; - hexBytes[j * 2] = HEX_ARRAY[v >>> 4]; - hexBytes[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return hexBytes; + @VisibleForTesting + static byte[] bytesToHex(byte[] bytes) { + return HexEncoding.encodeToString(bytes).getBytes(); } /** diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 098e8f74749c..7d12ede754ef 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -46,7 +46,6 @@ import android.os.UserHandle; import android.util.ArrayMap; import android.util.Slog; import android.view.ContentRecordingSession; -import android.window.WindowContainerToken; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; @@ -433,7 +432,7 @@ public final class MediaProjectionManagerService extends SystemService private IBinder mToken; private IBinder.DeathRecipient mDeathEater; private boolean mRestoreSystemAlertWindow; - private WindowContainerToken mTaskRecordingWindowContainerToken = null; + private IBinder mLaunchCookie = null; MediaProjection(int type, int uid, String packageName, int targetSdkVersion, boolean isPrivileged) { @@ -609,14 +608,13 @@ public final class MediaProjectionManagerService extends SystemService } @Override // Binder call - public void setTaskRecordingWindowContainerToken(WindowContainerToken token) { - // TODO(b/221417940) set the task id to record from sysui, for the package chosen. - mTaskRecordingWindowContainerToken = token; + public void setLaunchCookie(IBinder launchCookie) { + mLaunchCookie = launchCookie; } @Override // Binder call - public WindowContainerToken getTaskRecordingWindowContainerToken() { - return mTaskRecordingWindowContainerToken; + public IBinder getLaunchCookie() { + return mLaunchCookie; } public MediaProjectionInfo getProjectionInfo() { diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index de9102a69a2e..6135fe8acbed 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -340,7 +340,8 @@ public class ZenModeHelper { int newRuleInstanceCount = getCurrentInstanceCount(automaticZenRule.getOwner()) + getCurrentInstanceCount(automaticZenRule.getConfigurationActivity()) + 1; - if (newRuleInstanceCount > RULE_LIMIT_PER_PACKAGE + int newPackageRuleCount = getPackageRuleCount(pkg) + 1; + if (newPackageRuleCount > RULE_LIMIT_PER_PACKAGE || (ruleInstanceLimit > 0 && ruleInstanceLimit < newRuleInstanceCount)) { throw new IllegalArgumentException("Rule instance limit exceeded"); } @@ -521,6 +522,23 @@ public class ZenModeHelper { return count; } + // Equivalent method to getCurrentInstanceCount, but for all rules associated with a specific + // package rather than a condition provider service or activity. + private int getPackageRuleCount(String pkg) { + if (pkg == null) { + return 0; + } + int count = 0; + synchronized (mConfig) { + for (ZenRule rule : mConfig.automaticRules.values()) { + if (pkg.equals(rule.getPkg())) { + count++; + } + } + } + return count; + } + public boolean canManageAutomaticZenRule(ZenRule rule) { final int callingUid = Binder.getCallingUid(); if (callingUid == 0 || callingUid == Process.SYSTEM_UID) { diff --git a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java index bdde4f6ad86b..b05e44f9e625 100644 --- a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java +++ b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java @@ -62,7 +62,8 @@ final class OverlayManagerShellCommand extends ShellCommand { private final Context mContext; private final IOverlayManager mInterface; private static final Map<String, Integer> TYPE_MAP = Map.of( - "color", TypedValue.TYPE_FIRST_COLOR_INT); + "color", TypedValue.TYPE_FIRST_COLOR_INT, + "string", TypedValue.TYPE_STRING); OverlayManagerShellCommand(@NonNull final Context ctx, @NonNull final IOverlayManager iom) { mContext = ctx; @@ -390,13 +391,17 @@ final class OverlayManagerShellCommand extends ShellCommand { type = Integer.parseUnsignedInt(typeString); } } - final int intData; - if (valueString.startsWith("0x")) { - intData = Integer.parseUnsignedInt(valueString.substring(2), 16); + if (type == TypedValue.TYPE_STRING) { + overlayBuilder.setResourceValue(resourceName, type, valueString); } else { - intData = Integer.parseUnsignedInt(valueString); + final int intData; + if (valueString.startsWith("0x")) { + intData = Integer.parseUnsignedInt(valueString.substring(2), 16); + } else { + intData = Integer.parseUnsignedInt(valueString); + } + overlayBuilder.setResourceValue(resourceName, type, intData); } - overlayBuilder.setResourceValue(resourceName, type, intData); } private int runEnableExclusive() throws RemoteException { diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 109e7071469c..a909977583b4 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -6760,6 +6760,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService } long deleteOatArtifactsOfPackage(@NonNull Computer snapshot, String packageName) { + PackageManagerServiceUtils.enforceSystemOrRootOrShell( + "Only the system or shell can delete oat artifacts"); + PackageStateInternal packageState = snapshot.getPackageStateInternal(packageName); if (packageState == null || packageState.getPkg() == null) { return -1; // error code of deleteOptimizedFiles diff --git a/services/core/java/com/android/server/pm/ResolveIntentHelper.java b/services/core/java/com/android/server/pm/ResolveIntentHelper.java index 2db1c2029d9c..24ed6216f7fe 100644 --- a/services/core/java/com/android/server/pm/ResolveIntentHelper.java +++ b/services/core/java/com/android/server/pm/ResolveIntentHelper.java @@ -294,6 +294,10 @@ final class ResolveIntentHelper { // non-launchable IntentSender which contains the failed intent is created. The // SendIntentException is thrown if the IntentSender#sendIntent is invoked. if (ris != null && !ris.isEmpty()) { + // am#isIntentSenderTargetedToPackage returns false if both package name and component + // name are set in the intent. Clear the package name to have the api return true and + // prevent the package existence info from side channel leaks by the api. + intent.setPackage(null); intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name); } diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index 6e4acde501c3..201ca9c4e9cd 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java @@ -1530,6 +1530,7 @@ public class ParsingPackageUtils { try { int minVers = ParsingUtils.DEFAULT_MIN_SDK_VERSION; String minCode = null; + boolean minAssigned = false; int targetVers = ParsingUtils.DEFAULT_TARGET_SDK_VERSION; String targetCode = null; int maxVers = Integer.MAX_VALUE; @@ -1538,9 +1539,11 @@ public class ParsingPackageUtils { if (val != null) { if (val.type == TypedValue.TYPE_STRING && val.string != null) { minCode = val.string.toString(); + minAssigned = !TextUtils.isEmpty(minCode); } else { // If it's not a string, it's an integer. minVers = val.data; + minAssigned = true; } } @@ -1548,7 +1551,7 @@ public class ParsingPackageUtils { if (val != null) { if (val.type == TypedValue.TYPE_STRING && val.string != null) { targetCode = val.string.toString(); - if (minCode == null) { + if (!minAssigned) { minCode = targetCode; } } else { diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 50d1bd6c3e33..70be8b82ba78 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -42,12 +42,13 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.SynchronousUserSwitchObserver; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.ContentObserver; @@ -63,6 +64,7 @@ import android.os.BatteryManager; import android.os.BatteryManagerInternal; import android.os.BatterySaverPolicyConfig; import android.os.Binder; +import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.IBinder; @@ -94,6 +96,7 @@ import android.service.dreams.DreamManagerInternal; import android.service.vr.IVrManager; import android.service.vr.IVrStateCallbacks; import android.sysprop.InitProperties; +import android.sysprop.PowerProperties; import android.util.ArrayMap; import android.util.KeyValueListParser; import android.util.LongArray; @@ -124,6 +127,7 @@ import com.android.server.UiThread; import com.android.server.UserspaceRebootLogger; import com.android.server.Watchdog; import com.android.server.am.BatteryStatsService; +import com.android.server.compat.PlatformCompat; import com.android.server.lights.LightsManager; import com.android.server.lights.LogicalLight; import com.android.server.policy.WindowManagerPolicy; @@ -279,6 +283,17 @@ public final class PowerManagerService extends SystemService */ private static final long ENHANCED_DISCHARGE_PREDICTION_BROADCAST_MIN_DELAY_MS = 60 * 1000L; + /** + * Apps targeting Android U and above need to define + * {@link android.Manifest.permission#TURN_SCREEN_ON} in their manifest for + * {@link android.os.PowerManager#ACQUIRE_CAUSES_WAKEUP} to have any effect. + * Note that most applications should use {@link android.R.attr#turnScreenOn} or + * {@link android.app.Activity#setTurnScreenOn(boolean)} instead, as this prevents the + * previous foreground app from being resumed first when the screen turns on. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public static final long REQUIRE_TURN_SCREEN_ON_PERMISSION = 216114297L; /** Reason ID for holding display suspend blocker. */ private static final String HOLDING_DISPLAY_SUSPEND_BLOCKER = "holding display"; @@ -302,6 +317,7 @@ public final class PowerManagerService extends SystemService private final SystemPropertiesWrapper mSystemProperties; private final Clock mClock; private final Injector mInjector; + private final PlatformCompat mPlatformCompat; private AppOpsManager mAppOpsManager; private LightsManager mLightsManager; @@ -980,6 +996,11 @@ public final class PowerManagerService extends SystemService public void set(String key, String val) { SystemProperties.set(key, val); } + + @Override + public boolean getBoolean(String key, boolean def) { + return SystemProperties.getBoolean(key, def); + } }; } @@ -1005,6 +1026,10 @@ public final class PowerManagerService extends SystemService AppOpsManager createAppOpsManager(Context context) { return context.getSystemService(AppOpsManager.class); } + + PlatformCompat createPlatformCompat(Context context) { + return context.getSystemService(PlatformCompat.class); + } } final Constants mConstants; @@ -1061,6 +1086,8 @@ public final class PowerManagerService extends SystemService mAppOpsManager = injector.createAppOpsManager(mContext); + mPlatformCompat = injector.createPlatformCompat(mContext); + mPowerGroupWakefulnessChangeListener = new PowerGroupWakefulnessChangeListener(); // Save brightness values: @@ -1470,18 +1497,10 @@ public final class PowerManagerService extends SystemService updatePowerStateLocked(); } + @RequiresPermission(value = android.Manifest.permission.TURN_SCREEN_ON, conditional = true) private void acquireWakeLockInternal(IBinder lock, int displayId, int flags, String tag, String packageName, WorkSource ws, String historyTag, int uid, int pid, @Nullable IWakeLockCallback callback) { - - boolean isCallerPrivileged = false; - try { - ApplicationInfo appInfo = mContext.getPackageManager().getApplicationInfo(packageName, - PackageManager.ApplicationInfoFlags.of(0)); - isCallerPrivileged = appInfo.uid == uid && appInfo.isPrivilegedApp(); - } catch (PackageManager.NameNotFoundException e) { - // assume app is not privileged - } synchronized (mLock) { if (displayId != Display.INVALID_DISPLAY) { final DisplayInfo displayInfo = @@ -1528,7 +1547,7 @@ public final class PowerManagerService extends SystemService notifyAcquire = true; } - applyWakeLockFlagsOnAcquireLocked(wakeLock, isCallerPrivileged); + applyWakeLockFlagsOnAcquireLocked(wakeLock); mDirty |= DIRTY_WAKE_LOCKS; updatePowerStateLocked(); if (notifyAcquire) { @@ -1567,33 +1586,44 @@ public final class PowerManagerService extends SystemService return null; } - private boolean isAcquireCausesWakeupFlagAllowed(String opPackageName, int opUid, - boolean isCallerPrivileged) { + @RequiresPermission(value = android.Manifest.permission.TURN_SCREEN_ON, conditional = true) + private boolean isAcquireCausesWakeupFlagAllowed(String opPackageName, int opUid) { if (opPackageName == null) { return false; } - if (isCallerPrivileged) { - if (DEBUG_SPEW) { - Slog.d(TAG, "Allowing device wake-up for privileged app, call attributed to " - + opPackageName); - } - return true; - } if (mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, opUid, opPackageName) == AppOpsManager.MODE_ALLOWED) { - if (DEBUG_SPEW) { - Slog.d(TAG, "Allowing device wake-up for app with special access " + opPackageName); + if (mPlatformCompat.isChangeEnabledByPackageName(REQUIRE_TURN_SCREEN_ON_PERMISSION, + opPackageName, UserHandle.getUserId(opUid))) { + if (mContext.checkCallingOrSelfPermission( + android.Manifest.permission.TURN_SCREEN_ON) + == PackageManager.PERMISSION_GRANTED) { + if (DEBUG_SPEW) { + Slog.d(TAG, "Allowing device wake-up from app " + opPackageName); + } + return true; + } + } else { + // android.permission.TURN_SCREEN_ON has only been introduced in Android U, only + // check for appOp for apps targeting lower SDK versions + if (DEBUG_SPEW) { + Slog.d(TAG, "Allowing device wake-up from app with " + + "REQUIRE_TURN_SCREEN_ON_PERMISSION disabled " + opPackageName); + } + return true; } - return true; } - if (DEBUG_SPEW) { - Slog.d(TAG, "Not allowing device wake-up for " + opPackageName); + if (PowerProperties.permissionless_turn_screen_on().orElse(true)) { + Slog.d(TAG, "Device wake-up will be denied without android.permission.TURN_SCREEN_ON"); + return true; } + Slog.w(TAG, "Not allowing device wake-up for " + opPackageName); return false; } @GuardedBy("mLock") - private void applyWakeLockFlagsOnAcquireLocked(WakeLock wakeLock, boolean isCallerPrivileged) { + @RequiresPermission(value = android.Manifest.permission.TURN_SCREEN_ON, conditional = true) + private void applyWakeLockFlagsOnAcquireLocked(WakeLock wakeLock) { if ((wakeLock.mFlags & PowerManager.ACQUIRE_CAUSES_WAKEUP) != 0 && isScreenLock(wakeLock)) { String opPackageName; @@ -1615,8 +1645,7 @@ public final class PowerManagerService extends SystemService } Integer powerGroupId = wakeLock.getPowerGroupId(); // powerGroupId is null if the wakelock associated display is no longer available - if (powerGroupId != null && isAcquireCausesWakeupFlagAllowed(opPackageName, opUid, - isCallerPrivileged)) { + if (powerGroupId != null && isAcquireCausesWakeupFlagAllowed(opPackageName, opUid)) { if (powerGroupId == Display.INVALID_DISPLAY_GROUP) { // wake up all display groups if (DEBUG_SPEW) { @@ -5438,6 +5467,7 @@ public final class PowerManagerService extends SystemService } @Override // Binder call + @RequiresPermission(value = android.Manifest.permission.TURN_SCREEN_ON, conditional = true) public void acquireWakeLock(IBinder lock, int flags, String tag, String packageName, WorkSource ws, String historyTag, int displayId, @Nullable IWakeLockCallback callback) { diff --git a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java index 1acf798eb099..c68f9c63b13b 100644 --- a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java +++ b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java @@ -48,4 +48,19 @@ interface SystemPropertiesWrapper { * SELinux. libc will log the underlying reason. */ void set(@NonNull String key, @Nullable String val); + + /** + * Get the value for the given {@code key}, returned as a boolean. + * Values 'n', 'no', '0', 'false' or 'off' are considered false. + * Values 'y', 'yes', '1', 'true' or 'on' are considered true. + * (case sensitive). + * If the key does not exist, or has any other value, then the default + * result is returned. + * + * @param key the key to lookup + * @param def a default value to return + * @return the key parsed as a boolean, or def if the key isn't found or is + * not able to be parsed as a boolean. + */ + boolean getBoolean(@NonNull String key, boolean def); } diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 622de57a1078..270891fcf421 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -56,6 +56,10 @@ import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_T import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_WARM_LAUNCH; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE; import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__APPEARED_APPLY_TREATMENT; @@ -1376,7 +1380,7 @@ class ActivityMetricsLogger { return; } - logAppCompatStateInternal(activity, state, packageUid, compatStateInfo); + logAppCompatStateInternal(activity, state, compatStateInfo); } /** @@ -1416,18 +1420,61 @@ class ActivityMetricsLogger { } } if (activityToLog != null && stateToLog != APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE) { - logAppCompatStateInternal(activityToLog, stateToLog, packageUid, compatStateInfo); + logAppCompatStateInternal(activityToLog, stateToLog, compatStateInfo); } } + private static boolean isAppCompateStateChangedToLetterboxed(int state) { + return state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO + || state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION + || state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE; + } + private void logAppCompatStateInternal(@NonNull ActivityRecord activity, int state, - int packageUid, PackageCompatStateInfo compatStateInfo) { + PackageCompatStateInfo compatStateInfo) { compatStateInfo.mLastLoggedState = state; compatStateInfo.mLastLoggedActivity = activity; - FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED, packageUid, state); + int packageUid = activity.info.applicationInfo.uid; + + int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION; + if (isAppCompateStateChangedToLetterboxed(state)) { + positionToLog = activity.mLetterboxUiController.getLetterboxPositionForLogging(); + } + FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED, + packageUid, state, positionToLog); + + if (DEBUG_METRICS) { + Slog.i(TAG, String.format("APP_COMPAT_STATE_CHANGED(%s, %s, %s)", + packageUid, state, positionToLog)); + } + } + + /** + * Logs the changing of the letterbox position along with its package UID + */ + void logLetterboxPositionChange(@NonNull ActivityRecord activity, int position) { + int packageUid = activity.info.applicationInfo.uid; + FrameworkStatsLog.write(FrameworkStatsLog.LETTERBOX_POSITION_CHANGED, packageUid, position); + + if (!mPackageUidToCompatStateInfo.contains(packageUid)) { + // There is no last logged activity for this packageUid so we should not log the + // position change as we can only log the position change for the current activity + return; + } + final PackageCompatStateInfo compatStateInfo = mPackageUidToCompatStateInfo.get(packageUid); + final ActivityRecord lastLoggedActivity = compatStateInfo.mLastLoggedActivity; + if (activity != lastLoggedActivity) { + // Only log the position change for the current activity to be consistent with + // findAppCompatStateToLog and ensure that metrics for the state changes are computed + // correctly + return; + } + int state = activity.getAppCompatState(); + logAppCompatStateInternal(activity, state, compatStateInfo); if (DEBUG_METRICS) { - Slog.i(TAG, String.format("APP_COMPAT_STATE_CHANGED(%s, %s)", packageUid, state)); + Slog.i(TAG, String.format("LETTERBOX_POSITION_CHANGED(%s, %s)", + packageUid, position)); } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b70f7a0fd213..0a44cda8d7e2 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3215,12 +3215,29 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return false; } - if (mRootWindowContainer.getTopResumedActivity() == this - && getDisplayContent().mFocusedApp == this) { - ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: already on top, " - + "activity=%s", this); - return !isState(RESUMED); + // If this activity already positions on the top focused task, moving the task to front + // is not needed. But we still need to ensure this activity is focused because the + // current focused activity could be another activity in the same Task if activities are + // displayed on adjacent TaskFragments. + final ActivityRecord currentFocusedApp = mDisplayContent.mFocusedApp; + if (currentFocusedApp != null && currentFocusedApp.task == task) { + final Task topFocusableTask = mDisplayContent.getTask( + (t) -> t.isLeafTask() && t.isFocusable(), true /* traverseTopToBottom */); + if (task == topFocusableTask) { + if (currentFocusedApp == this) { + ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: already on top " + + "and focused, activity=%s", this); + } else { + ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: set focused, " + + "activity=%s", this); + mDisplayContent.setFocusedApp(this); + mAtmService.mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, + true /* updateInputWindows */); + } + return !isState(RESUMED); + } } + ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: activity=%s", this); rootTask.moveToFront(reason, task); @@ -7796,11 +7813,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A newParentConfiguration.windowConfiguration.getWindowingMode(); final boolean isFixedOrientationLetterboxAllowed = parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW - || parentWindowingMode == WINDOWING_MODE_FULLSCREEN; + || parentWindowingMode == WINDOWING_MODE_FULLSCREEN + // Switching from PiP to fullscreen. + || (parentWindowingMode == WINDOWING_MODE_PINNED + && resolvedConfig.windowConfiguration.getWindowingMode() + == WINDOWING_MODE_FULLSCREEN); // TODO(b/181207944): Consider removing the if condition and always run // resolveFixedOrientationConfiguration() since this should be applied for all cases. if (isFixedOrientationLetterboxAllowed) { - resolveFixedOrientationConfiguration(newParentConfiguration, parentWindowingMode); + resolveFixedOrientationConfiguration(newParentConfiguration); } if (mCompatDisplayInsets != null) { @@ -8092,8 +8113,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * <p>If letterboxed due to fixed orientation then aspect ratio restrictions are also applied * in this method. */ - private void resolveFixedOrientationConfiguration(@NonNull Configuration newParentConfig, - int windowingMode) { + private void resolveFixedOrientationConfiguration(@NonNull Configuration newParentConfig) { mLetterboxBoundsForFixedOrientationAndAspectRatio = null; mIsEligibleForFixedOrientationLetterbox = false; final Rect parentBounds = newParentConfig.windowConfiguration.getBounds(); @@ -8113,11 +8133,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (organizedTf != null && !organizedTf.fillsParent()) { return; } - if (windowingMode == WINDOWING_MODE_PINNED) { - // PiP bounds have higher priority than the requested orientation. Otherwise the - // activity may be squeezed into a small piece. - return; - } final Rect resolvedBounds = getResolvedOverrideConfiguration().windowConfiguration.getBounds(); diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index c14e54ef586a..9637aca4c690 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2576,6 +2576,7 @@ class ActivityStarter { mInTask = null; } mInTaskFragment = inTaskFragment; + sendNewTaskFragmentResultRequestIfNeeded(); mStartFlags = startFlags; // If the onlyIfNeeded flag is set, then we can do this if the activity being launched @@ -2618,6 +2619,18 @@ class ActivityStarter { } } + private void sendNewTaskFragmentResultRequestIfNeeded() { + if (mStartActivity.resultTo != null && mInTaskFragment != null + && mInTaskFragment != mStartActivity.resultTo.getTaskFragment()) { + Slog.w(TAG, + "Activity is launching as a new TaskFragment, so cancelling activity result."); + mStartActivity.resultTo.sendResult(INVALID_UID, mStartActivity.resultWho, + mStartActivity.requestCode, RESULT_CANCELED, + null /* data */, null /* dataGrants */); + mStartActivity.resultTo = null; + } + } + private void computeLaunchingTaskFlags() { // If the caller is not coming from another activity, but has given us an explicit task into // which they would like us to launch the new activity, then let's see about doing that. diff --git a/services/core/java/com/android/server/wm/ContentRecordingController.java b/services/core/java/com/android/server/wm/ContentRecordingController.java index fca4942d4b79..fff7637acc7e 100644 --- a/services/core/java/com/android/server/wm/ContentRecordingController.java +++ b/services/core/java/com/android/server/wm/ContentRecordingController.java @@ -63,6 +63,7 @@ final class ContentRecordingController { */ void setContentRecordingSessionLocked(@Nullable ContentRecordingSession incomingSession, @NonNull WindowManagerService wmService) { + // TODO(b/219761722) handle a null session arriving due to task setup failing if (incomingSession != null && (!ContentRecordingSession.isValid(incomingSession) || ContentRecordingSession.isSameDisplay(mSession, incomingSession))) { // Ignore an invalid session, or a session for the same display as currently recording. diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index afcb21a45106..6619f985df58 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1611,7 +1611,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (mTransitionController.useShellTransitionsRotation()) { return ROTATION_UNDEFINED; } - if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM) { + if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM + || getIgnoreOrientationRequest()) { return ROTATION_UNDEFINED; } if (r.mOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) { @@ -2942,9 +2943,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Set some sort of reasonable bounds on the size of the display that we will try // to emulate. final int minSize = 200; - final int maxScale = 2; - width = Math.min(Math.max(width, minSize), mInitialDisplayWidth * maxScale); - height = Math.min(Math.max(height, minSize), mInitialDisplayHeight * maxScale); + final int maxScale = 3; + final int maxSize = Math.max(mInitialDisplayWidth, mInitialDisplayHeight) * maxScale; + width = Math.min(Math.max(width, minSize), maxSize); + height = Math.min(Math.max(height, minSize), maxSize); } Slog.i(TAG_WM, "Using new display size: " + width + "x" + height); @@ -6194,6 +6196,14 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp .getKeyguardController().isAodShowing(mDisplayId); } + /** + * @return whether the keyguard is occluded on this display + */ + boolean isKeyguardOccluded() { + return mRootWindowContainer.mTaskSupervisor + .getKeyguardController().isDisplayOccluded(mDisplayId); + } + @VisibleForTesting void removeAllTasks() { forAllTasks((t) -> { t.getRootTask().removeChild(t, "removeAllTasks"); }); diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index d2c71f57e701..b9d83198139d 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -620,7 +620,8 @@ public class DisplayRotation { // We only enable seamless rotation if the top window has requested it and is in the // fullscreen opaque state. Seamless rotation requires freezing various Surface states and // won't work well with animations, so we disable it in the animation case for now. - if (w.getAttrs().rotationAnimation != ROTATION_ANIMATION_SEAMLESS || w.isAnimatingLw()) { + if (w.getAttrs().rotationAnimation != ROTATION_ANIMATION_SEAMLESS || w.inMultiWindowMode() + || w.isAnimatingLw()) { return false; } diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index 08715b160b9a..91b2fb63a543 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -687,6 +687,24 @@ final class LetterboxConfiguration { } } + /* + * Gets the horizontal position of the letterboxed app window when horizontal reachability is + * enabled. + */ + @LetterboxHorizontalReachabilityPosition + int getLetterboxPositionForHorizontalReachability() { + return mLetterboxPositionForHorizontalReachability; + } + + /* + * Gets the vertical position of the letterboxed app window when vertical reachability is + * enabled. + */ + @LetterboxVerticalReachabilityPosition + int getLetterboxPositionForVerticalReachability() { + return mLetterboxPositionForVerticalReachability; + } + /** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */ static String letterboxHorizontalReachabilityPositionToString( @LetterboxHorizontalReachabilityPosition int position) { diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index f849d2886ba1..d65276793700 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -21,6 +21,20 @@ import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER; +import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER; import static com.android.server.wm.ActivityRecord.computeAspectRatio; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -28,6 +42,12 @@ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString; @@ -259,12 +279,26 @@ final class LetterboxUiController { return; } + int letterboxPositionForHorizontalReachability = mLetterboxConfiguration + .getLetterboxPositionForHorizontalReachability(); if (mLetterbox.getInnerFrame().left > x) { // Moving to the next stop on the left side of the app window: right > center > left. mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(); + int changeToLog = + letterboxPositionForHorizontalReachability + == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER + ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT + : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER; + logLetterboxPositionChange(changeToLog); } else if (mLetterbox.getInnerFrame().right < x) { // Moving to the next stop on the right side of the app window: left > center > right. mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(); + int changeToLog = + letterboxPositionForHorizontalReachability + == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER + ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT + : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER; + logLetterboxPositionChange(changeToLog); } // TODO(197549949): Add animation for transition. @@ -280,13 +314,26 @@ final class LetterboxUiController { // Only react to clicks at the top and bottom of the letterboxed app window. return; } - + int letterboxPositionForVerticalReachability = mLetterboxConfiguration + .getLetterboxPositionForVerticalReachability(); if (mLetterbox.getInnerFrame().top > y) { // Moving to the next stop on the top side of the app window: bottom > center > top. mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop(); + int changeToLog = + letterboxPositionForVerticalReachability + == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER + ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP + : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER; + logLetterboxPositionChange(changeToLog); } else if (mLetterbox.getInnerFrame().bottom < y) { // Moving to the next stop on the bottom side of the app window: top > center > bottom. mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(); + int changeToLog = + letterboxPositionForVerticalReachability + == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER + ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM + : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER; + logLetterboxPositionChange(changeToLog); } // TODO(197549949): Add animation for transition. @@ -577,4 +624,63 @@ final class LetterboxUiController { return "UNKNOWN_REASON"; } + private int letterboxHorizontalReachabilityPositionToLetterboxPosition( + @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition int position) { + switch (position) { + case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT; + case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER; + case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT; + default: + throw new AssertionError( + "Unexpected letterbox horizontal reachability position type: " + + position); + } + } + + private int letterboxVerticalReachabilityPositionToLetterboxPosition( + @LetterboxConfiguration.LetterboxVerticalReachabilityPosition int position) { + switch (position) { + case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP; + case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER; + case LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM: + return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM; + default: + throw new AssertionError( + "Unexpected letterbox vertical reachability position type: " + + position); + } + } + + int getLetterboxPositionForLogging() { + int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION; + if (isHorizontalReachabilityEnabled()) { + int letterboxPositionForHorizontalReachability = getLetterboxConfiguration() + .getLetterboxPositionForHorizontalReachability(); + positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition( + letterboxPositionForHorizontalReachability); + } else if (isVerticalReachabilityEnabled()) { + int letterboxPositionForVerticalReachability = getLetterboxConfiguration() + .getLetterboxPositionForVerticalReachability(); + positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition( + letterboxPositionForVerticalReachability); + } + return positionToLog; + } + + private LetterboxConfiguration getLetterboxConfiguration() { + return mLetterboxConfiguration; + } + + /** + * Logs letterbox position changes via {@link ActivityMetricsLogger#logLetterboxPositionChange}. + */ + private void logLetterboxPositionChange(int letterboxPositionChange) { + mActivityRecord.mTaskSupervisor.getActivityMetricsLogger() + .logLetterboxPositionChange(mActivityRecord, letterboxPositionChange); + } } diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java index ad158c7b45b9..ac1a2b17603a 100644 --- a/services/core/java/com/android/server/wm/RemoteAnimationController.java +++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java @@ -331,8 +331,10 @@ class RemoteAnimationController implements DeathRecipient { private void invokeAnimationCancelled(String reason) { ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "cancelAnimation(): reason=%s", reason); + final boolean isKeyguardOccluded = mDisplayContent.isKeyguardOccluded(); + try { - mRemoteAnimationAdapter.getRunner().onAnimationCancelled(); + mRemoteAnimationAdapter.getRunner().onAnimationCancelled(isKeyguardOccluded); } catch (RemoteException e) { Slog.e(TAG, "Failed to notify cancel", e); } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 4bb023c1c218..52bf220a647a 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -1114,7 +1114,10 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { // If a task is launching from a created-by-organizer task, it should be launched into the // same created-by-organizer task as well. Unless, the candidate task is already positioned // in the another adjacent task. - if (sourceTask != null) { + if (sourceTask != null && (candidateTask == null + // A pinned task relaunching should be handled by its task organizer. Skip fallback + // launch target of a pinned task from source task. + || candidateTask.getWindowingMode() != WINDOWING_MODE_PINNED)) { Task launchTarget = sourceTask.getCreatedByOrganizerTask(); if (launchTarget != null && launchTarget.getAdjacentTaskFragment() != null) { if (candidateTask != null) { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 3c0cac0079e8..c27a3d34a02f 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -149,9 +149,6 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe final @TransitionType int mType; private int mSyncId = -1; - // Used for tracking a Transition throughout a lifecycle (i.e. from STATE_COLLECTING to - // STATE_FINISHED or STATE_ABORT), and should only be used for testing and debugging. - private int mDebugId = -1; private @TransitionFlags int mFlags; private final TransitionController mController; private final BLASTSyncEngine mSyncEngine; @@ -295,11 +292,6 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return mSyncId; } - @VisibleForTesting - int getDebugId() { - return mDebugId; - } - @TransitionFlags int getFlags() { return mFlags; @@ -315,6 +307,10 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return mFinishTransaction; } + private boolean isCollecting() { + return mState == STATE_COLLECTING || mState == STATE_STARTED; + } + /** Starts collecting phase. Once this starts, all relevant surface operations are sync. */ void startCollecting(long timeoutMs) { if (mState != STATE_PENDING) { @@ -322,7 +318,6 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } mState = STATE_COLLECTING; mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG); - mDebugId = mSyncId; mController.mTransitionTracer.logState(this); } @@ -353,7 +348,10 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe if (mState < STATE_COLLECTING) { throw new IllegalStateException("Transition hasn't started collecting."); } - if (mSyncId < 0) return; + if (!isCollecting()) { + // Too late, transition already started playing, so don't collect. + return; + } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Collecting in transition %d: %s", mSyncId, wc); // "snapshot" all parents (as potential promotion targets). Do this before checking @@ -403,7 +401,10 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe * or waiting until after the animation to close). */ void collectExistenceChange(@NonNull WindowContainer wc) { - if (mSyncId < 0) return; + if (mState >= STATE_PLAYING) { + // Too late to collect. Don't check too-early here since `collect` will check that. + return; + } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Existence Changed in transition %d:" + " %s", mSyncId, wc); collect(wc); @@ -437,7 +438,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe */ void setOverrideAnimation(TransitionInfo.AnimationOptions options, @Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) { - if (mSyncId < 0) return; + if (!isCollecting()) return; mOverrideOptions = options; sendRemoteCallback(mClientAnimationStartCallback); mClientAnimationStartCallback = startCallback; @@ -455,7 +456,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe * The transition will wait for all groups to be ready. */ void setReady(WindowContainer wc, boolean ready) { - if (mSyncId < 0) return; + if (!isCollecting() || mSyncId < 0) return; mReadyTracker.setReadyFrom(wc, ready); applyReady(); } @@ -473,7 +474,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe * @see ReadyTracker#setAllReady. */ void setAllReady() { - if (mSyncId < 0) return; + if (!isCollecting() || mSyncId < 0) return; mReadyTracker.setAllReady(); applyReady(); } @@ -889,7 +890,6 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe // No player registered, so just finish/apply immediately cleanUpOnFailure(); } - mSyncId = -1; mOverrideOptions = null; reportStartReasonsToLogger(); @@ -1614,7 +1614,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } boolean getLegacyIsReady() { - return (mState == STATE_STARTED || mState == STATE_COLLECTING) && mSyncId >= 0; + return isCollecting() && mSyncId >= 0; } static Transition fromBinder(IBinder binder) { diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java index b1951e038177..c1927d864320 100644 --- a/services/core/java/com/android/server/wm/TransitionTracer.java +++ b/services/core/java/com/android/server/wm/TransitionTracer.java @@ -79,7 +79,7 @@ public class TransitionTracer { final ProtoOutputStream outputStream = new ProtoOutputStream(); final long transitionEntryToken = outputStream.start(TRANSITION); - outputStream.write(ID, transition.getDebugId()); + outputStream.write(ID, transition.getSyncId()); outputStream.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos()); outputStream.write(TRANSITION_TYPE, transition.mType); outputStream.write(STATE, transition.getState()); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 9d5b9455da13..d7c85d4c7853 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -48,6 +48,7 @@ import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES; import static android.provider.Settings.Global.DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR; import static android.provider.Settings.Global.DEVELOPMENT_WM_DISPLAY_SETTINGS_PATH; +import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; @@ -122,7 +123,6 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN; import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION; -import static com.android.server.wm.WindowContainer.SYNC_STATE_NONE; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_INPUT_METHOD; @@ -289,6 +289,7 @@ import android.view.displayhash.VerifiedDisplayHash; import android.window.ClientWindowFrames; import android.window.ITaskFpsCallback; import android.window.TaskSnapshot; +import android.window.WindowContainerToken; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; @@ -2245,7 +2246,7 @@ public class WindowManagerService extends IWindowManager.Stub return 0; } - if (win.cancelAndRedraw()) { + if (win.cancelAndRedraw() && win.mPrepareSyncSeqId <= win.mLastSeqIdSentToRelayout) { result |= RELAYOUT_RES_CANCEL_AND_REDRAW; } @@ -2551,11 +2552,6 @@ public class WindowManagerService extends IWindowManager.Stub win.mLastSeqIdSentToRelayout = win.mSyncSeqId; outSyncIdBundle.putInt("seqid", win.mSyncSeqId); - // Only mark mAlreadyRequestedSync if there's an explicit sync request, and not if - // we're syncing due to mDrawHandlers - if (win.mSyncState != SYNC_STATE_NONE) { - win.mAlreadyRequestedSync = true; - } } else { outSyncIdBundle.putInt("seqid", -1); } @@ -8281,6 +8277,26 @@ public class WindowManagerService extends IWindowManager.Stub @Override public void setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) { synchronized (mGlobalLock) { + // Allow the controller to handle teardown or a non-task session. + if (incomingSession == null + || incomingSession.getContentToRecord() != RECORD_CONTENT_TASK) { + mContentRecordingController.setContentRecordingSessionLocked(incomingSession, + WindowManagerService.this); + return; + } + // For a task session, find the activity identified by the launch cookie. + final WindowContainerToken wct = getTaskWindowContainerTokenForLaunchCookie( + incomingSession.getTokenToRecord()); + if (wct == null) { + Slog.w(TAG, "Handling a new recording session; unable to find the " + + "WindowContainerToken"); + mContentRecordingController.setContentRecordingSessionLocked(null, + WindowManagerService.this); + return; + } + // Replace the launch cookie in the session details with the task's + // WindowContainerToken. + incomingSession.setTokenToRecord(wct.asBinder()); mContentRecordingController.setContentRecordingSessionLocked(incomingSession, WindowManagerService.this); } @@ -8539,6 +8555,38 @@ public class WindowManagerService extends IWindowManager.Stub } /** + * Retrieve the {@link WindowContainerToken} of the task that contains the activity started + * with the given launch cookie. + * + * @param launchCookie the launch cookie set on the {@link ActivityOptions} when starting an + * activity + * @return a token representing the task containing the activity started with the given launch + * cookie, or {@code null} if the token couldn't be found. + */ + @VisibleForTesting + @Nullable + WindowContainerToken getTaskWindowContainerTokenForLaunchCookie(@NonNull IBinder launchCookie) { + // Find the activity identified by the launch cookie. + final ActivityRecord targetActivity = mRoot.getActivity( + activity -> activity.mLaunchCookie == launchCookie); + if (targetActivity == null) { + Slog.w(TAG, "Unable to find the activity for this launch cookie"); + return null; + } + if (targetActivity.getTask() == null) { + Slog.w(TAG, "Unable to find the task for this launch cookie"); + return null; + } + WindowContainerToken taskWindowContainerToken = + targetActivity.getTask().mRemoteToken.toWindowContainerToken(); + if (taskWindowContainerToken == null) { + Slog.w(TAG, "Unable to find the WindowContainerToken for " + targetActivity.getName()); + return null; + } + return taskWindowContainerToken; + } + + /** * You need ALLOW_SLIPPERY_TOUCHES permission to be able to set FLAG_SLIPPERY. */ private int sanitizeFlagSlippery(int flags, String windowName, int callingUid, int callingPid) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 6728e63d055f..46091d842c2a 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -115,6 +115,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS_LIGHT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RESIZE; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW; +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_INSETS; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; import static com.android.server.policy.WindowManagerPolicy.TRANSIT_ENTER; @@ -391,7 +392,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP */ int mSyncSeqId = 0; int mLastSeqIdSentToRelayout = 0; - boolean mAlreadyRequestedSync; + + /** The last syncId associated with a prepareSync or 0 when no sync is active. */ + int mPrepareSyncSeqId = 0; /** * {@code true} when the client was still drawing for sync when the sync-set was finished or @@ -4421,7 +4424,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } } - pw.println(prefix + "mAlreadyRequestedSync=" + mAlreadyRequestedSync); + pw.println(prefix + "mPrepareSyncSeqId=" + mPrepareSyncSeqId); } @Override @@ -5913,6 +5916,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mWinAnimator.getSurfaceControl(); } + /** Drops a buffer for this window's view-root from a transaction */ + private void dropBufferFrom(Transaction t) { + SurfaceControl viewSurface = getClientViewRootSurface(); + if (viewSurface == null) return; + t.setBuffer(viewSurface, (android.hardware.HardwareBuffer) null); + } + @Override boolean prepareSync() { if (!mDrawHandlers.isEmpty()) { @@ -5928,7 +5938,18 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // to draw even if the children draw first or don't need to sync, so we start // in WAITING state rather than READY. mSyncState = SYNC_STATE_WAITING_FOR_DRAW; + + if (mPrepareSyncSeqId > 0) { + // another prepareSync during existing sync (eg. reparented), so pre-emptively + // drop buffer (if exists). If the buffer hasn't been received yet, it will be + // dropped in finishDrawing. + ProtoLog.d(WM_DEBUG_SYNC_ENGINE, "Preparing to sync a window that was already in the" + + " sync, so try dropping buffer. win=%s", this); + dropBufferFrom(mSyncTransaction); + } + mSyncSeqId++; + mPrepareSyncSeqId = mSyncSeqId; requestRedrawForSync(); return true; } @@ -5949,7 +5970,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mRedrawForSyncReported) { mClientWasDrawingForSync = true; } - mAlreadyRequestedSync = false; + mPrepareSyncSeqId = 0; + if (cancel) { + // This is leaving sync so any buffers left in the sync have a chance of + // being applied out-of-order and can also block the buffer queue for this + // window. To prevent this, drop the buffer. + dropBufferFrom(mSyncTransaction); + } super.finishSync(outMergedTransaction, cancel); } @@ -5971,6 +5998,17 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP .notifyStartingWindowDrawn(mActivityRecord); } + final boolean syncActive = mPrepareSyncSeqId > 0; + final boolean syncStillPending = syncActive && mPrepareSyncSeqId > syncSeqId; + if (syncStillPending && postDrawTransaction != null) { + ProtoLog.d(WM_DEBUG_SYNC_ENGINE, "Got a buffer for request id=%d but latest request is" + + " id=%d. Since the buffer is out-of-date, drop it. win=%s", syncSeqId, + mPrepareSyncSeqId, this); + // sync is waiting for a newer seqId, so this buffer is obsolete and can be dropped + // to free up the buffer queue. + dropBufferFrom(postDrawTransaction); + } + final boolean hasSyncHandlers = executeDrawHandlers(postDrawTransaction, syncSeqId); boolean skipLayout = false; @@ -5983,10 +6021,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // Layout is not needed because the window will be hidden by the fade leash. postDrawTransaction = null; skipLayout = true; - } else if (onSyncFinishedDrawing() && postDrawTransaction != null) { - mSyncTransaction.merge(postDrawTransaction); - // Consume the transaction because the sync group will merge it. - postDrawTransaction = null; + } else if (syncActive) { + if (!syncStillPending) { + onSyncFinishedDrawing(); + } + if (postDrawTransaction != null) { + mSyncTransaction.merge(postDrawTransaction); + // Consume the transaction because the sync group will merge it. + postDrawTransaction = null; + } } final boolean layoutNeeded = @@ -6218,6 +6261,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } public boolean cancelAndRedraw() { - return mSyncState != SYNC_STATE_NONE && mAlreadyRequestedSync; + // Cancel any draw requests during a sync. + return mPrepareSyncSeqId > 0; } } diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index ec38f777593f..40789964f4e1 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -119,7 +119,7 @@ cc_defaults { "libutils", "libui", "libvibratorservice", - "PlatformProperties", + "libPlatformProperties", "libinput", "libinputflinger", "libinputflinger_base", diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 06c0e366ad1a..cbd574348de0 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -1981,6 +1981,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { synchronized (getLockObject()) { mOwners.load(); setDeviceOwnershipSystemPropertyLocked(); + if (mOwners.hasDeviceOwner()) { + setGlobalSettingDeviceOwnerType( + mOwners.getDeviceOwnerType(mOwners.getDeviceOwnerPackageName())); + } } } @@ -8804,6 +8808,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { deleteTransferOwnershipBundleLocked(userId); toggleBackupServiceActive(UserHandle.USER_SYSTEM, true); pushUserControlDisabledPackagesLocked(userId); + setGlobalSettingDeviceOwnerType(DEVICE_OWNER_TYPE_DEFAULT); } private void clearApplicationRestrictions(int userId) { @@ -18363,6 +18368,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { "Test only admins can only set the device owner type more than once"); mOwners.setDeviceOwnerType(packageName, deviceOwnerType, isAdminTestOnly); + setGlobalSettingDeviceOwnerType(deviceOwnerType); + } + + // TODO(b/237065504): Allow mainline modules to get the device owner type. This is a workaround + // to get the device owner type in PermissionController. See HibernationPolicy.kt. + private void setGlobalSettingDeviceOwnerType(int deviceOwnerType) { + mInjector.binderWithCleanCallingIdentity( + () -> mInjector.settingsGlobalPutInt("device_owner_type", deviceOwnerType)); } @Override diff --git a/services/net/Android.bp b/services/net/Android.bp index 886a397c87e6..804ccc514df5 100644 --- a/services/net/Android.bp +++ b/services/net/Android.bp @@ -28,30 +28,6 @@ java_library_static { ], } -// Version of services.net for usage by the wifi mainline module. -// Note: This is compiled against module_current. -// TODO(b/172457099): This should be moved to networkstack-client, -// with dependencies moved to frameworks/libs/net right. -java_library { - name: "services.net-module-wifi", - sdk_version: "module_current", - min_sdk_version: "30", - static_libs: [ - // All the classes in netd_aidl_interface must be jarjar so they do not conflict with the - // classes generated by netd_aidl_interfaces-platform-java above. - "netd_aidl_interface-V3-java", - "networkstack-client", - "net-utils-services-common", - ], - apex_available: [ - "com.android.wifi", - ], - visibility: [ - "//packages/modules/Wifi/service", - "//packages/modules/Wifi/service/tests/wifitests", - ], -} - filegroup { name: "services-tethering-shared-srcs", srcs: [ diff --git a/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/AppEnumerationInternalTests.java b/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/AppEnumerationInternalTests.java index 08cea06fd314..31fe18410182 100644 --- a/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/AppEnumerationInternalTests.java +++ b/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/AppEnumerationInternalTests.java @@ -21,14 +21,19 @@ import static com.android.compatibility.common.util.ShellUtils.runShellCommand; import static com.google.common.truth.Truth.assertThat; import android.app.AppGlobals; +import android.app.PendingIntent; +import android.content.Context; +import android.content.IntentSender; import android.content.pm.IPackageManager; import android.content.pm.ProviderInfo; import android.os.Process; import android.os.UserHandle; +import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -54,6 +59,7 @@ public class AppEnumerationInternalTests { private static final String TARGET_HAS_APPOP_PERMISSION = "com.android.appenumeration.hasappoppermission"; private static final String TARGET_SHARED_USER = "com.android.appenumeration.shareduid"; + private static final String TARGET_NON_EXISTENT = "com.android.appenumeration.nonexistent.pkg"; private static final String SYNC_PROVIDER_AUTHORITY = TARGET_SYNC_PROVIDER; private static final String PERMISSION_REQUEST_INSTALL_PACKAGES = @@ -134,6 +140,26 @@ public class AppEnumerationInternalTests { assertThat(uid).isEqualTo(Process.INVALID_UID); } + @Test + public void getLaunchIntentSenderForPackage_intentSender_cannotDetectPackage() + throws Exception { + installPackage(SHARED_USER_APK_PATH, false /* forceQueryable */); + + final Context context = InstrumentationRegistry.getInstrumentation().getContext(); + final IntentSender sender = context.getPackageManager() + .getLaunchIntentSenderForPackage(TARGET_SHARED_USER); + assertThat(new PendingIntent(sender.getTarget()).isTargetedToPackage()).isTrue(); + sender.sendIntent(context, 0 /* code */, null /* intent */, + null /* onFinished */, null /* handler */); + + final IntentSender failedSender = InstrumentationRegistry.getInstrumentation().getContext() + .getPackageManager().getLaunchIntentSenderForPackage(TARGET_NON_EXISTENT); + assertThat(new PendingIntent(failedSender.getTarget()).isTargetedToPackage()).isTrue(); + Assert.assertThrows(IntentSender.SendIntentException.class, + () -> failedSender.sendIntent(context, 0 /* code */, null /* intent */, + null /* onFinished */, null /* handler */)); + } + private static void installPackage(String apkPath, boolean forceQueryable) { final StringBuilder cmd = new StringBuilder("pm install "); if (forceQueryable) { diff --git a/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/AndroidManifest-sharedUser.xml b/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/AndroidManifest-sharedUser.xml index b424298e0ae0..65d0f218461a 100644 --- a/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/AndroidManifest-sharedUser.xml +++ b/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/AndroidManifest-sharedUser.xml @@ -19,6 +19,13 @@ package="com.android.appenumeration.shareduid" android:sharedUserId="com.android.appenumeration.shareduid"> <application> + <activity android:name="com.android.appenumeration.testapp.DummyActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.INFO"/> + </intent-filter> + </activity> <uses-library android:name="android.test.runner" /> </application> </manifest>
\ No newline at end of file diff --git a/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/src/com/android/appenumeration/testapp/DummyActivity.java b/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/src/com/android/appenumeration/testapp/DummyActivity.java new file mode 100644 index 000000000000..bc5d3b5d022d --- /dev/null +++ b/services/tests/PackageManagerServiceTests/appenumeration/test-apps/target/src/com/android/appenumeration/testapp/DummyActivity.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.appenumeration.testapp; + +import android.app.Activity; + +public class DummyActivity extends Activity { +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index 25cf8a86baad..e95924ad7109 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -20,7 +20,9 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_NEGATIVE; -import static com.android.server.biometrics.BiometricServiceStateProto.*; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -32,6 +34,8 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -280,6 +284,43 @@ public class AuthSessionTest { } @Test + public void testOnDialogAnimatedInDoesNothingDuringInvalidState() throws Exception { + setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); + final long operationId = 123; + final int userId = 10; + + final AuthSession session = createAuthSession(mSensors, + false /* checkDevicePolicyManager */, + Authenticators.BIOMETRIC_STRONG, + TEST_REQUEST_ID, + operationId, + userId); + final IBiometricAuthenticator impl = session.mPreAuthInfo.eligibleSensors.get(0).impl; + + session.goToInitialState(); + for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { + assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState()); + session.onCookieReceived( + session.mPreAuthInfo.eligibleSensors.get(sensor.id).getCookie()); + } + assertTrue(session.allCookiesReceived()); + assertEquals(STATE_AUTH_STARTED, session.getState()); + verify(impl, never()).startPreparedClient(anyInt()); + + // First invocation should start the client monitor. + session.onDialogAnimatedIn(); + assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); + verify(impl).startPreparedClient(anyInt()); + + // Subsequent invocations should not start the client monitor again. + session.onDialogAnimatedIn(); + session.onDialogAnimatedIn(); + session.onDialogAnimatedIn(); + assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); + verify(impl, times(1)).startPreparedClient(anyInt()); + } + + @Test public void testCancelAuthentication_whenStateAuthCalled_invokesCancel() throws RemoteException { testInvokesCancel(session -> session.onCancelAuthSession(false /* force */)); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java index c17347320f52..9e9d70332f00 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java @@ -80,11 +80,14 @@ public class BiometricSchedulerOperationTest { private Handler mHandler; private BiometricSchedulerOperation mOperation; + private boolean mIsDebuggable; @Before public void setUp() { mHandler = new Handler(TestableLooper.get(this).getLooper()); - mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback); + mIsDebuggable = false; + mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback, + () -> mIsDebuggable); } @Test @@ -126,6 +129,34 @@ public class BiometricSchedulerOperationTest { } @Test + public void testSecondStartWithCookieCrashesWhenDebuggable() { + final int cookie = 5; + mIsDebuggable = true; + when(mClientMonitor.getCookie()).thenReturn(cookie); + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie); + assertThat(started).isTrue(); + + assertThrows(IllegalStateException.class, + () -> mOperation.startWithCookie(mOnStartCallback, cookie)); + } + + @Test + public void testSecondStartWithCookieFailsNicelyWhenNotDebuggable() { + final int cookie = 5; + mIsDebuggable = false; + when(mClientMonitor.getCookie()).thenReturn(cookie); + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie); + assertThat(started).isTrue(); + + final boolean startedAgain = mOperation.startWithCookie(mOnStartCallback, cookie); + assertThat(startedAgain).isFalse(); + } + + @Test public void startsWhenReadyAndHalAvailable() { when(mClientMonitor.getCookie()).thenReturn(0); when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); @@ -170,7 +201,34 @@ public class BiometricSchedulerOperationTest { } @Test + public void secondStartCrashesWhenDebuggable() { + mIsDebuggable = true; + when(mClientMonitor.getCookie()).thenReturn(0); + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + final boolean started = mOperation.start(mOnStartCallback); + assertThat(started).isTrue(); + + assertThrows(IllegalStateException.class, () -> mOperation.start(mOnStartCallback)); + } + + @Test + public void secondStartFailsNicelyWhenNotDebuggable() { + mIsDebuggable = false; + when(mClientMonitor.getCookie()).thenReturn(0); + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + final boolean started = mOperation.start(mOnStartCallback); + assertThat(started).isTrue(); + + final boolean startedAgain = mOperation.start(mOnStartCallback); + assertThat(startedAgain).isFalse(); + } + + @Test public void doesNotStartWithCookie() { + // This class only throws exceptions when debuggable. + mIsDebuggable = true; when(mClientMonitor.getCookie()).thenReturn(9); assertThrows(IllegalStateException.class, () -> mOperation.start(mock(ClientMonitorCallback.class))); @@ -178,6 +236,8 @@ public class BiometricSchedulerOperationTest { @Test public void cannotRestart() { + // This class only throws exceptions when debuggable. + mIsDebuggable = true; when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); mOperation.start(mOnStartCallback); @@ -188,6 +248,8 @@ public class BiometricSchedulerOperationTest { @Test public void abortsNotRunning() { + // This class only throws exceptions when debuggable. + mIsDebuggable = true; when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); mOperation.abort(); @@ -200,7 +262,8 @@ public class BiometricSchedulerOperationTest { } @Test - public void cannotAbortRunning() { + public void abortCrashesWhenDebuggableIfOperationIsRunning() { + mIsDebuggable = true; when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); mOperation.start(mOnStartCallback); @@ -209,6 +272,16 @@ public class BiometricSchedulerOperationTest { } @Test + public void abortFailsNicelyWhenNotDebuggableIfOperationIsRunning() { + mIsDebuggable = false; + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + mOperation.start(mOnStartCallback); + + mOperation.abort(); + } + + @Test public void cancel() { when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); @@ -254,6 +327,30 @@ public class BiometricSchedulerOperationTest { } @Test + public void cancelCrashesWhenDebuggableIfOperationIsFinished() { + mIsDebuggable = true; + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + mOperation.abort(); + assertThat(mOperation.isFinished()).isTrue(); + + final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); + assertThrows(IllegalStateException.class, () -> mOperation.cancel(mHandler, cancelCb)); + } + + @Test + public void cancelFailsNicelyWhenNotDebuggableIfOperationIsFinished() { + mIsDebuggable = false; + when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + + mOperation.abort(); + assertThat(mOperation.isFinished()).isTrue(); + + final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); + mOperation.cancel(mHandler, cancelCb); + } + + @Test public void markCanceling() { when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 388170bd24cb..a7ca08385b9f 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -7848,7 +7848,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { setDeviceOwner(); dpm.setDeviceOwnerType(admin1, DEVICE_OWNER_TYPE_FINANCED); when(getServices().ipackageManager.getBlockUninstallForUser( - eq(packageName), eq(UserHandle.USER_SYSTEM))) + eq(packageName), eq(UserHandle.getCallingUserId()))) .thenReturn(true); assertThat(dpm.isUninstallBlocked(admin1, packageName)).isTrue(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java index 46a4e862c300..1fa3871347f8 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java @@ -628,6 +628,46 @@ public class OneTouchPlayActionTest { assertThat(mNativeWrapper.getResultMessages()).contains(standbyMessage); } + @Test + public void noWakeUpOnReportPowerStatus() throws Exception { + setUp(true); + + HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback( + mHdmiControlService); + playbackDevice.init(); + mLocalDevices.add(playbackDevice); + mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + mTestLooper.dispatchAll(); + + mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS); + mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV); + mTestLooper.dispatchAll(); + mNativeWrapper.clearResultMessages(); + + TestActionTimer actionTimer = new TestActionTimer(); + TestCallback callback = new TestCallback(); + OneTouchPlayAction action = createOneTouchPlayAction(playbackDevice, actionTimer, callback, + false); + playbackDevice.addAndStartAction(action); + mTestLooper.dispatchAll(); + + assertThat(mPowerManager.isInteractive()).isTrue(); + mPowerManager.setInteractive(false); + mTestLooper.dispatchAll(); + + HdmiCecMessage reportPowerStatusOn = + HdmiCecMessage.build( + ADDR_TV, + playbackDevice.getDeviceInfo().getLogicalAddress(), + Constants.MESSAGE_REPORT_POWER_STATUS, + POWER_ON); + action.processCommand(reportPowerStatusOn); + mTestLooper.dispatchAll(); + + assertThat(mPowerManager.isInteractive()).isFalse(); + assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); + } + private static class TestActionTimer implements ActionTimer { private int mState; diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java index 6f1268e5de24..cc6f2cc5ba3e 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java +++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java @@ -275,7 +275,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_EN_US), imi); assertEquals(1, result.size()); verifyEquality(autoSubtype, result.get(0)); @@ -299,7 +299,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_EN_US), imi); assertEquals(2, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -323,7 +323,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_EN_GB), imi); assertEquals(2, result.size()); verifyEquality(nonAutoEnGB, result.get(0)); @@ -348,7 +348,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_FR), imi); assertEquals(2, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); @@ -369,7 +369,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_FR_CA), imi); assertEquals(2, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); @@ -391,7 +391,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_JA_JP), imi); assertEquals(3, result.size()); verifyEquality(nonAutoJa, result.get(0)); @@ -413,7 +413,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoHi, result.get(0)); @@ -430,7 +430,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -447,7 +447,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -469,7 +469,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(Locale.forLanguageTag("sr-Latn-RS")), imi); assertEquals(2, result.size()); assertThat(nonAutoSrLatn, is(in(result))); @@ -489,7 +489,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(Locale.forLanguageTag("sr-Cyrl-RS")), imi); assertEquals(2, result.size()); assertThat(nonAutoSrCyrl, is(in(result))); @@ -515,7 +515,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales( Locale.forLanguageTag("sr-Latn-RS-x-android"), Locale.forLanguageTag("ja-JP"), @@ -542,7 +542,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_FIL_PH), imi); assertEquals(1, result.size()); verifyEquality(nonAutoFil, result.get(0)); @@ -560,7 +560,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_FI), imi); assertEquals(1, result.size()); verifyEquality(nonAutoJa, result.get(0)); @@ -576,7 +576,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); @@ -590,7 +590,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); @@ -604,7 +604,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); @@ -618,7 +618,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); @@ -640,7 +640,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_FR, LOCALE_EN_US, LOCALE_JA_JP), imi); assertThat(nonAutoFrCA, is(in(result))); assertThat(nonAutoEnUS, is(in(result))); @@ -680,26 +680,26 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, SUBTYPE_MODE_VOICE)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, SUBTYPE_MODE_VOICE)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, SUBTYPE_MODE_ANY)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, SUBTYPE_MODE_ANY)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_GB, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_GB, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_GB, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_GB, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); } @@ -711,22 +711,22 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI, !CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI_FI, !CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI_FI, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI_FI, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI_FI, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); } @@ -738,22 +738,22 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI, !CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI_FI, !CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI_FI, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI_FI, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI_FI, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); } @@ -766,13 +766,13 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_IN, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_IN, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_IN, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_IN, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_ID, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_ID, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_ID, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_ID, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); } @@ -785,13 +785,13 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_IN, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_IN, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_IN, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_IN, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_ID, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_ID, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_ID, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_ID, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); } } @@ -805,19 +805,19 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, null, "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, null, "")); } // Returns null when the config value is empty. { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, "", "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, "", "")); } // Returns null when the configured package doesn't have an IME. { - assertNull(InputMethodUtils.chooseSystemVoiceIme(new ArrayMap<>(), + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(), systemIme.getPackageName(), "")); } @@ -825,7 +825,7 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertEquals(systemIme, InputMethodUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, systemIme.getPackageName(), null)); } @@ -833,13 +833,13 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertEquals(systemIme, InputMethodUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, systemIme.getPackageName(), "")); } // Returns null when the current default isn't found. { - assertNull(InputMethodUtils.chooseSystemVoiceIme(new ArrayMap<>(), + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(), systemIme.getPackageName(), systemIme.getId())); } @@ -850,8 +850,8 @@ public class InputMethodUtilsTest { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); methodMap.put(secondIme.getId(), secondIme); - assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, systemIme.getPackageName(), - "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + systemIme.getPackageName(), "")); } // Returns the current one when the current default and config point to the same package. @@ -861,7 +861,7 @@ public class InputMethodUtilsTest { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); methodMap.put(secondIme.getId(), secondIme); - assertEquals(systemIme, InputMethodUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, systemIme.getPackageName(), systemIme.getId())); } @@ -871,7 +871,7 @@ public class InputMethodUtilsTest { final InputMethodInfo nonSystemIme = createFakeInputMethodInfo("NonSystemIme", "fake.voice0", false /* isSystem */); methodMap.put(nonSystemIme.getId(), nonSystemIme); - assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, nonSystemIme.getPackageName(), nonSystemIme.getId())); } @@ -882,7 +882,7 @@ public class InputMethodUtilsTest { "FakeDefaultAutoVoiceIme", "fake.voice0", false /* isSystem */); methodMap.put(systemIme.getId(), systemIme); methodMap.put(nonSystemIme.getId(), nonSystemIme); - assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, nonSystemIme.getPackageName(), "")); } } @@ -891,7 +891,7 @@ public class InputMethodUtilsTest { final Locale systemLocale, String... expectedImeNames) { final Context context = createTargetContextWithLocales(new LocaleList(systemLocale)); final String[] actualImeNames = getPackageNames( - InputMethodUtils.getDefaultEnabledImes(context, preinstalledImes)); + InputMethodInfoUtils.getDefaultEnabledImes(context, preinstalledImes)); assertEquals(expectedImeNames.length, actualImeNames.length); for (int i = 0; i < expectedImeNames.length; ++i) { assertEquals(expectedImeNames[i], actualImeNames[i]); @@ -902,7 +902,7 @@ public class InputMethodUtilsTest { final Locale systemLocale, String... expectedImeNames) { final Context context = createTargetContextWithLocales(new LocaleList(systemLocale)); final String[] actualImeNames = getPackageNames( - InputMethodUtils.getDefaultEnabledImes(context, preinstalledImes, + InputMethodInfoUtils.getDefaultEnabledImes(context, preinstalledImes, true /* onlyMinimum */)); assertEquals(expectedImeNames.length, actualImeNames.length); for (int i = 0; i < expectedImeNames.length; ++i) { diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java index eae9ee7fc55a..09aa345bef22 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java @@ -468,18 +468,18 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { @Test public void testPasswordData_serializeDeserialize() { PasswordData data = new PasswordData(); - data.scryptN = 11; - data.scryptR = 22; - data.scryptP = 33; + data.scryptLogN = 11; + data.scryptLogR = 22; + data.scryptLogP = 33; data.credentialType = CREDENTIAL_TYPE_PASSWORD; data.salt = PAYLOAD; data.passwordHandle = PAYLOAD2; PasswordData deserialized = PasswordData.fromBytes(data.toBytes()); - assertEquals(11, deserialized.scryptN); - assertEquals(22, deserialized.scryptR); - assertEquals(33, deserialized.scryptP); + assertEquals(11, deserialized.scryptLogN); + assertEquals(22, deserialized.scryptLogR); + assertEquals(33, deserialized.scryptLogP); assertEquals(CREDENTIAL_TYPE_PASSWORD, deserialized.credentialType); assertArrayEquals(PAYLOAD, deserialized.salt); assertArrayEquals(PAYLOAD2, deserialized.passwordHandle); @@ -491,9 +491,9 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { // wire format. byte[] serialized = new byte[] { 0, 0, 0, 2, /* CREDENTIAL_TYPE_PASSWORD_OR_PIN */ - 11, /* scryptN */ - 22, /* scryptR */ - 33, /* scryptP */ + 11, /* scryptLogN */ + 22, /* scryptLogR */ + 33, /* scryptLogP */ 0, 0, 0, 5, /* salt.length */ 1, 2, -1, -2, 55, /* salt */ 0, 0, 0, 6, /* passwordHandle.length */ @@ -501,9 +501,9 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { }; PasswordData deserialized = PasswordData.fromBytes(serialized); - assertEquals(11, deserialized.scryptN); - assertEquals(22, deserialized.scryptR); - assertEquals(33, deserialized.scryptP); + assertEquals(11, deserialized.scryptLogN); + assertEquals(22, deserialized.scryptLogR); + assertEquals(33, deserialized.scryptLogP); assertEquals(CREDENTIAL_TYPE_PASSWORD_OR_PIN, deserialized.credentialType); assertArrayEquals(PAYLOAD, deserialized.salt); assertArrayEquals(PAYLOAD2, deserialized.passwordHandle); @@ -574,6 +574,13 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { } } + @Test + public void testHexEncodingIsUppercase() { + final byte[] raw = new byte[] { (byte)0xAB, (byte)0xCD, (byte)0xEF }; + final byte[] expected = new byte[] { 'A', 'B', 'C', 'D', 'E', 'F' }; + assertArrayEquals(expected, SyntheticPasswordManager.bytesToHex(raw)); + } + // b/62213311 //TODO: add non-migration work profile case, and unify/un-unify transition. //TODO: test token after user resets password diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index 40ce956a0790..78f7f66ec7eb 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -54,6 +54,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.hardware.SensorManager; import android.hardware.display.AmbientDisplayConfiguration; @@ -75,6 +76,7 @@ import android.os.UserHandle; import android.os.test.TestLooper; import android.provider.Settings; import android.service.dreams.DreamManagerInternal; +import android.sysprop.PowerProperties; import android.test.mock.MockContentResolver; import android.view.Display; import android.view.DisplayInfo; @@ -85,6 +87,7 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.LocalServices; import com.android.server.SystemService; +import com.android.server.compat.PlatformCompat; import com.android.server.lights.LightsManager; import com.android.server.policy.WindowManagerPolicy; import com.android.server.power.PowerManagerService.BatteryReceiver; @@ -144,6 +147,7 @@ public class PowerManagerServiceTest { @Mock private SystemPropertiesWrapper mSystemPropertiesMock; @Mock private AppOpsManager mAppOpsManagerMock; @Mock private LowPowerStandbyController mLowPowerStandbyControllerMock; + @Mock private PlatformCompat mPlatformCompat; @Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock; @@ -305,6 +309,11 @@ public class PowerManagerServiceTest { AppOpsManager createAppOpsManager(Context context) { return mAppOpsManagerMock; } + + @Override + PlatformCompat createPlatformCompat(Context context) { + return mPlatformCompat; + } }); return mService; } @@ -478,6 +487,12 @@ public class PowerManagerServiceTest { String packageName = "pkg.name"; when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED); + when(mPlatformCompat.isChangeEnabledByPackageName( + eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(), + anyInt())).thenReturn(true); + when(mContextSpy.checkCallingOrSelfPermission( + android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( + PackageManager.PERMISSION_GRANTED); // First, ensure that a normal full wake lock does not cause a wakeup int flags = PowerManager.FULL_WAKE_LOCK; @@ -499,6 +514,23 @@ public class PowerManagerServiceTest { null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */); + + // Verify that on older platforms only the appOp is necessary and the permission isn't + // checked + when(mPlatformCompat.isChangeEnabledByPackageName( + eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(), + anyInt())).thenReturn(false); + when(mContextSpy.checkCallingOrSelfPermission( + android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( + PackageManager.PERMISSION_DENIED); + forceSleep(); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); + + flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; + mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, + null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); + mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */); } @Test @@ -518,7 +550,32 @@ public class PowerManagerServiceTest { int flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); - assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); + if (PowerProperties.permissionless_turn_screen_on().orElse(true)) { + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); + } else { + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); + } + mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */); + + when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, + Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED); + when(mPlatformCompat.isChangeEnabledByPackageName( + eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(), + anyInt())).thenReturn(true); + when(mContextSpy.checkCallingOrSelfPermission( + android.Manifest.permission.TURN_SCREEN_ON)).thenReturn( + PackageManager.PERMISSION_DENIED); + + // Verify that the flag has no effect when OP_TURN_SCREEN_ON is allowed but + // android.permission.TURN_SCREEN_ON is denied + flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; + mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, + null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null); + if (PowerProperties.permissionless_turn_screen_on().orElse(true)) { + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); + } else { + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); + } mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index fd1536c5c0f1..4550b56f6fd0 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -1622,7 +1622,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(si), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelperSpy.addAutomaticZenRule("android", zenRule, "test"); + // We need the package name to be something that's not "android" so there aren't any + // existing rules under that package. + String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test"); assertNotNull(id); } try { @@ -1632,12 +1634,41 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelperSpy.addAutomaticZenRule("android", zenRule, "test"); + String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test"); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { // yay } + } + @Test + public void testAddAutomaticZenRule_beyondSystemLimit_differentComponents() { + // Make sure the system limit is enforced per-package even with different component provider + // names. + for (int i = 0; i < RULE_LIMIT_PER_PACKAGE; i++) { + ScheduleInfo si = new ScheduleInfo(); + si.startHour = i; + AutomaticZenRule zenRule = new AutomaticZenRule("name" + i, + null, + new ComponentName("android", "ScheduleConditionProvider" + i), + ZenModeConfig.toScheduleConditionId(si), + new ZenPolicy.Builder().build(), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test"); + assertNotNull(id); + } + try { + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName("android", "ScheduleConditionProviderFinal"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + new ZenPolicy.Builder().build(), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test"); + fail("allowed too many rules to be created"); + } catch (IllegalArgumentException e) { + // yay + } } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index cfeaf850da03..0c3b270518cf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -29,6 +29,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT; +import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; +import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; import static android.content.pm.ActivityInfo.FLAG_SUPPORTS_PICTURE_IN_PICTURE; import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_ALWAYS; import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_DEFAULT; @@ -182,6 +184,10 @@ public class ActivityRecordTests extends WindowTestsBase { private final String mPackageName = getInstrumentation().getTargetContext().getPackageName(); + private static final int ORIENTATION_CONFIG_CHANGES = + CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT | CONFIG_SCREEN_SIZE + | CONFIG_SMALLEST_SCREEN_SIZE; + @Before public void setUp() throws Exception { setBooted(mAtm); @@ -487,7 +493,7 @@ public class ActivityRecordTests extends WindowTestsBase { public void testSetRequestedOrientationUpdatesConfiguration() throws Exception { final ActivityRecord activity = new ActivityBuilder(mAtm) .setCreateTask(true) - .setConfigChanges(CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT) + .setConfigChanges(ORIENTATION_CONFIG_CHANGES) .build(); activity.setState(RESUMED, "Testing"); @@ -710,7 +716,7 @@ public class ActivityRecordTests extends WindowTestsBase { final ActivityRecord activity = new ActivityBuilder(mAtm) .setCreateTask(true) .setLaunchTaskBehind(true) - .setConfigChanges(CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT) + .setConfigChanges(ORIENTATION_CONFIG_CHANGES) .build(); final Task task = activity.getTask(); activity.setState(STOPPED, "Testing"); @@ -779,7 +785,7 @@ public class ActivityRecordTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } }, 0, 0)); activity.updateOptionsLocked(opts); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java index 71f19148d616..b5764f54ff92 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java @@ -87,7 +87,7 @@ public class AppChangeTransitionTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java index 8656a4fecef1..f2d6273f2b26 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java @@ -806,7 +806,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { } @Override - public void onAnimationCancelled() throws RemoteException { + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { mFinishedCallback = null; } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java index 436cf36587d8..74154609b22e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java @@ -522,7 +522,7 @@ public class AppTransitionTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { mCancelled = true; } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index d737963f80e7..40e266c71328 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1699,6 +1699,13 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(displayContent.mPinnedTaskController.isFreezingTaskConfig(pinnedTask)); assertEquals(pinnedActivity.getConfiguration().orientation, displayContent.getConfiguration().orientation); + + // No need to apply rotation if the display ignores orientation request. + doCallRealMethod().when(displayContent).rotationForActivityInDifferentOrientation(any()); + pinnedActivity.mOrientation = SCREEN_ORIENTATION_LANDSCAPE; + displayContent.setIgnoreOrientationRequest(true); + assertEquals(WindowConfiguration.ROTATION_UNDEFINED, + displayContent.rotationForActivityInDifferentOrientation(pinnedActivity)); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java index 204c7e6fb8d4..027f5218f820 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java @@ -43,6 +43,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -210,7 +211,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); adapter.onAnimationCancelled(mMockLeash); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); } @Test @@ -226,7 +227,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { mClock.fastForward(10500); mHandler.timeAdvance(); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), eq(adapter)); } @@ -247,12 +248,12 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { mClock.fastForward(10500); mHandler.timeAdvance(); - verify(mMockRunner, never()).onAnimationCancelled(); + verify(mMockRunner, never()).onAnimationCancelled(anyBoolean()); mClock.fastForward(52500); mHandler.timeAdvance(); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), eq(adapter)); } finally { @@ -264,7 +265,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { public void testZeroAnimations() throws Exception { mController.goodToGo(TRANSIT_OLD_NONE); verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); } @Test @@ -274,7 +275,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); } @Test @@ -316,7 +317,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { win.mActivityRecord.removeImmediately(); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), eq(adapter)); } @@ -574,7 +575,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { // Cancel the wallpaper window animator and ensure the runner is not canceled wallpaperWindowToken.cancelAnimation(); - verify(mMockRunner, never()).onAnimationCancelled(); + verify(mMockRunner, never()).onAnimationCancelled(anyBoolean()); } finally { mDisplayContent.mOpeningApps.clear(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java index 2a9fcb9d070b..7f09606d1c3a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java @@ -762,12 +762,20 @@ public class TaskDisplayAreaTests extends WindowTestsBase { Task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */, 0 /* launchFlags */, candidateTask); - assertSame(rootTask, actualRootTask.getRootTask()); + assertSame(rootTask, actualRootTask); // Verify the launch root task without candidate task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */, 0 /* launchFlags */); - assertSame(adjacentRootTask, actualRootTask.getRootTask()); + assertSame(adjacentRootTask, actualRootTask); + + final Task pinnedTask = createTask( + mDisplayContent, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); + // Verify not adjusting launch target for pinned candidate task + actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED, + ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */, + 0 /* launchFlags */, pinnedTask /* candidateTask */); + assertNull(actualRootTask); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 228cb65aab38..5f3096356bc5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -475,5 +475,13 @@ public class TaskFragmentTest extends WindowTestsBase { assertFalse(activity0.isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(activity1.isLetterboxedForFixedOrientationAndAspectRatio()); assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation()); + + tf0.setResumedActivity(activity0, "test"); + tf1.setResumedActivity(activity1, "test"); + mDisplayContent.mFocusedApp = activity1; + + // Making the activity0 be the focused activity and ensure the focused app is updated. + activity0.moveFocusableActivityToTop("test"); + assertEquals(activity0, mDisplayContent.mFocusedApp); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index 5743922d0428..1715a295ded3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -979,7 +979,7 @@ public class WindowContainerTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } }, 0, 0, false); adapter.setCallingPidUid(123, 456); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index e09a94f3bcf9..1a64f5e3a356 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -61,6 +61,7 @@ import android.view.InsetsState; import android.view.InsetsVisibilities; import android.view.View; import android.view.WindowManager; +import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; @@ -317,4 +318,76 @@ public class WindowManagerServiceTests extends WindowTestsBase { verify(mWm.mInputManager).setInTouchMode( !currentTouchMode, callingPid, callingUid, /* hasPermission= */ false); } + + @Test + public void testGetTaskWindowContainerTokenForLaunchCookie_nullCookie() { + WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(null); + assertThat(wct).isNull(); + } + + @Test + public void testGetTaskWindowContainerTokenForLaunchCookie_invalidCookie() { + Binder cookie = new Binder("test cookie"); + WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie); + assertThat(wct).isNull(); + + final ActivityRecord testActivity = new ActivityBuilder(mAtm) + .setCreateTask(true) + .build(); + + wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie); + assertThat(wct).isNull(); + } + + @Test + public void testGetTaskWindowContainerTokenForLaunchCookie_validCookie() { + final Binder cookie = new Binder("ginger cookie"); + final WindowContainerToken launchRootTask = mock(WindowContainerToken.class); + setupActivityWithLaunchCookie(cookie, launchRootTask); + + WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie); + assertThat(wct).isEqualTo(launchRootTask); + } + + @Test + public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies() { + final Binder cookie1 = new Binder("ginger cookie"); + final WindowContainerToken launchRootTask1 = mock(WindowContainerToken.class); + setupActivityWithLaunchCookie(cookie1, launchRootTask1); + + setupActivityWithLaunchCookie(new Binder("choc chip cookie"), + mock(WindowContainerToken.class)); + + setupActivityWithLaunchCookie(new Binder("peanut butter cookie"), + mock(WindowContainerToken.class)); + + WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie1); + assertThat(wct).isEqualTo(launchRootTask1); + } + + @Test + public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies_noneValid() { + setupActivityWithLaunchCookie(new Binder("ginger cookie"), + mock(WindowContainerToken.class)); + + setupActivityWithLaunchCookie(new Binder("choc chip cookie"), + mock(WindowContainerToken.class)); + + setupActivityWithLaunchCookie(new Binder("peanut butter cookie"), + mock(WindowContainerToken.class)); + + WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie( + new Binder("some other cookie")); + assertThat(wct).isNull(); + } + + private void setupActivityWithLaunchCookie(IBinder launchCookie, WindowContainerToken wct) { + final WindowContainer.RemoteToken remoteToken = mock(WindowContainer.RemoteToken.class); + when(remoteToken.toWindowContainerToken()).thenReturn(wct); + final ActivityRecord testActivity = new ActivityBuilder(mAtm) + .setCreateTask(true) + .build(); + testActivity.mLaunchCookie = launchCookie; + testActivity.getTask().mRemoteToken = remoteToken; + } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index bb2e9639881a..1e0e8366c385 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -136,6 +136,7 @@ final class HotwordDetectionConnection { // The error codes are used for onError callback private static final int HOTWORD_DETECTION_SERVICE_DIED = -1; private static final int CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION = -2; + private static final int CALLBACK_DETECT_TIMEOUT = -3; // Hotword metrics private static final int METRICS_INIT_UNKNOWN_TIMEOUT = @@ -545,66 +546,7 @@ final class HotwordDetectionConnection { if (DEBUG) { Slog.d(TAG, "triggerHardwareRecognitionEventForTestLocked"); } - detectFromDspSourceForTest(event, callback); - } - - private void detectFromDspSourceForTest(SoundTrigger.KeyphraseRecognitionEvent recognitionEvent, - IHotwordRecognitionStatusCallback externalCallback) { - Slog.v(TAG, "detectFromDspSourceForTest"); - IDspHotwordDetectionCallback internalCallback = new IDspHotwordDetectionCallback.Stub() { - @Override - public void onDetected(HotwordDetectedResult result) throws RemoteException { - Slog.v(TAG, "onDetected"); - synchronized (mLock) { - if (!mValidatingDspTrigger) { - Slog.i(TAG, "Ignored hotword detected since trigger has been handled"); - return; - } - mValidatingDspTrigger = false; - try { - enforcePermissionsForDataDelivery(); - enforceExtraKeyphraseIdNotLeaked(result, recognitionEvent); - } catch (SecurityException e) { - externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION); - return; - } - externalCallback.onKeyphraseDetected(recognitionEvent, result); - if (result != null) { - Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result) - + " bits from hotword trusted process"); - if (mDebugHotwordLogging) { - Slog.i(TAG, "Egressed detected result: " + result); - } - } - } - } - - @Override - public void onRejected(HotwordRejectedResult result) throws RemoteException { - Slog.v(TAG, "onRejected"); - synchronized (mLock) { - if (mValidatingDspTrigger) { - mValidatingDspTrigger = false; - externalCallback.onRejected(result); - if (mDebugHotwordLogging && result != null) { - Slog.i(TAG, "Egressed rejected result: " + result); - } - } else { - Slog.i(TAG, "Ignored hotword rejected since trigger has been handled"); - } - } - } - }; - - synchronized (mLock) { - mValidatingDspTrigger = true; - mRemoteHotwordDetectionService.run( - service -> service.detectFromDspSource( - recognitionEvent, - recognitionEvent.getCaptureFormat(), - VALIDATION_TIMEOUT_MILLIS, - internalCallback)); - } + detectFromDspSource(event, callback); } private void detectFromDspSource(SoundTrigger.KeyphraseRecognitionEvent recognitionEvent, @@ -613,6 +555,7 @@ final class HotwordDetectionConnection { Slog.d(TAG, "detectFromDspSource"); } + AtomicBoolean timeoutDetected = new AtomicBoolean(false); // TODO: consider making this a non-anonymous class. IDspHotwordDetectionCallback internalCallback = new IDspHotwordDetectionCallback.Stub() { @Override @@ -621,12 +564,12 @@ final class HotwordDetectionConnection { Slog.d(TAG, "onDetected"); } synchronized (mLock) { - // TODO: If the dsp trigger comes in after the timeout, we will log both events. - // Because we don't enforce the timeout yet. We should add some synchronizations - // within the runnable to prevent the race condition to log both events. if (mCancellationKeyPhraseDetectionFuture != null) { mCancellationKeyPhraseDetectionFuture.cancel(true); } + if (timeoutDetected.get()) { + return; + } HotwordMetricsLogger.writeKeyphraseTriggerEvent( mDetectorType, HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED); @@ -668,6 +611,9 @@ final class HotwordDetectionConnection { if (mCancellationKeyPhraseDetectionFuture != null) { mCancellationKeyPhraseDetectionFuture.cancel(true); } + if (timeoutDetected.get()) { + return; + } HotwordMetricsLogger.writeKeyphraseTriggerEvent( mDetectorType, HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED); @@ -689,21 +635,29 @@ final class HotwordDetectionConnection { synchronized (mLock) { mValidatingDspTrigger = true; - mRemoteHotwordDetectionService.run( - service -> { - // TODO: avoid allocate every time - mCancellationKeyPhraseDetectionFuture = mScheduledExecutorService.schedule( - () -> HotwordMetricsLogger - .writeKeyphraseTriggerEvent(mDetectorType, - HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_TIMEOUT), - VALIDATION_TIMEOUT_MILLIS, - TimeUnit.MILLISECONDS); - service.detectFromDspSource( - recognitionEvent, - recognitionEvent.getCaptureFormat(), - VALIDATION_TIMEOUT_MILLIS, - internalCallback); - }); + mRemoteHotwordDetectionService.run(service -> { + mCancellationKeyPhraseDetectionFuture = mScheduledExecutorService.schedule( + () -> { + // TODO: avoid allocate every time + timeoutDetected.set(true); + Slog.w(TAG, "Timed out on #detectFromDspSource"); + HotwordMetricsLogger.writeKeyphraseTriggerEvent( + mDetectorType, + HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_TIMEOUT); + try { + externalCallback.onError(CALLBACK_DETECT_TIMEOUT); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to report onError status: ", e); + } + }, + VALIDATION_TIMEOUT_MILLIS, + TimeUnit.MILLISECONDS); + service.detectFromDspSource( + recognitionEvent, + recognitionEvent.getCaptureFormat(), + VALIDATION_TIMEOUT_MILLIS, + internalCallback); + }); } } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index d1729f886f6e..432d08725e87 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -17069,4 +17069,35 @@ public class TelephonyManager { } return false; } + + /** + * Fetches the EFPSISMSC value from the SIM that contains the Public Service Identity + * of the SM-SC (either a SIP URI or tel URI), the value is common for both appType + * {@link #APPTYPE_ISIM} and {@link #APPTYPE_SIM}. + * The EFPSISMSC value is used by the ME to submit SMS over IP as defined in 24.341 [55]. + * + * @param appType ICC Application type {@link #APPTYPE_ISIM} or {@link #APPTYPE_USIM} + * @return SIP URI or tel URI of the Public Service Identity of the SM-SC + * @throws SecurityException if the caller does not have the required permission/privileges + * @hide + */ + @NonNull + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) + public String getSmscIdentity(int appType) { + try { + IPhoneSubInfo info = getSubscriberInfoService(); + if (info == null) { + Rlog.e(TAG, "getSmscIdentity(): IPhoneSubInfo instance is NULL"); + return null; + } + /** Fetches the SIM PSISMSC params based on subId and appType */ + return info.getSmscIdentity(getSubId(), appType); + } catch (RemoteException ex) { + Rlog.e(TAG, "getSmscIdentity(): RemoteException: " + ex.getMessage()); + } catch (NullPointerException ex) { + Rlog.e(TAG, "getSmscIdentity(): NullPointerException: " + ex.getMessage()); + } + return null; + } } diff --git a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl index ce2017bb9a35..78335fd54917 100644 --- a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl +++ b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl @@ -218,4 +218,18 @@ interface IPhoneSubInfo { */ String getIccSimChallengeResponse(int subId, int appType, int authType, String data, String callingPackage, String callingFeatureId); + + /** + * Fetches the EFPSISMSC value from the SIM that contains the Public Service Identity + * of the SM-SC (either a SIP URI or tel URI), the value is common for both appType + * {@link #APPTYPE_ISIM} and {@link #APPTYPE_SIM}. + * The EFPSISMSC value is used by the ME to submit SMS over IP as defined in 24.341 [55]. + * + * @param appType ICC Application type {@link #APPTYPE_ISIM} or {@link #APPTYPE_USIM} + * @return SIP URI or tel URI of the Public Service Identity of the SM-SC + * @throws SecurityException if the caller does not have the required permission/privileges + * @hide + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)") + String getSmscIdentity(int subId, int appType); } diff --git a/tests/ApkVerityTest/Android.bp b/tests/ApkVerityTest/Android.bp index 62e16a5b83de..f026bea80470 100644 --- a/tests/ApkVerityTest/Android.bp +++ b/tests/ApkVerityTest/Android.bp @@ -37,8 +37,8 @@ java_test_host { "general-tests", "vts", ], - target_required: [ - "block_device_writer_module", + data_device_bins_both: [ + "block_device_writer", ], data: [ ":ApkVerityTestCertDer", diff --git a/tests/ApkVerityTest/AndroidTest.xml b/tests/ApkVerityTest/AndroidTest.xml index 3c8e1ed99604..2a0a2c76e50f 100644 --- a/tests/ApkVerityTest/AndroidTest.xml +++ b/tests/ApkVerityTest/AndroidTest.xml @@ -31,10 +31,18 @@ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> <option name="cleanup" value="true" /> - <option name="push" value="block_device_writer->/data/local/tmp/block_device_writer" /> <option name="push" value="ApkVerityTestCert.der->/data/local/tmp/ApkVerityTestCert.der" /> </target_preparer> + <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> + <!-- The build system produces both 32 and 64 bit variants with bitness suffix. Let + FilePusher find the filename with bitness and push to a remote name without bitness. + --> + <option name="append-bitness" value="true" /> + <option name="cleanup" value="true" /> + <option name="push" value="block_device_writer->/data/local/tmp/block_device_writer" /> + </target_preparer> + <!-- Skip on HWASan. TODO(b/232288278): Re-enable --> <object type="module_controller" class="com.android.tradefed.testtype.suite.module.SkipHWASanModuleController" /> <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > diff --git a/tests/ApkVerityTest/block_device_writer/Android.bp b/tests/ApkVerityTest/block_device_writer/Android.bp index fdfa41fd4ca9..0002447d17f2 100644 --- a/tests/ApkVerityTest/block_device_writer/Android.bp +++ b/tests/ApkVerityTest/block_device_writer/Android.bp @@ -24,12 +24,7 @@ package { } cc_test { - // Depending on how the test runs, the executable may be uploaded to different location. - // Before the bug in the file pusher is fixed, workaround by making the name unique. - // See b/124718249#comment12. - name: "block_device_writer_module", - stem: "block_device_writer", - + name: "block_device_writer", srcs: ["block_device_writer.cpp"], cflags: [ "-D_FILE_OFFSET_BITS=64", @@ -42,20 +37,13 @@ cc_test { "libbase", "libutils", ], - // For some reasons, cuttlefish (x86) uses x86_64 test suites for testing. Unfortunately, when - // the uploader does not pick up the executable from correct output location. The following - // workaround allows the test to: - // * upload the 32-bit exectuable for both 32 and 64 bits devices to use - // * refer to the same executable name in Java - // * no need to force the Java test to be archiecture specific. - // - // See b/145573317 for details. + compile_multilib: "both", multilib: { lib32: { - suffix: "", + suffix: "32", }, lib64: { - suffix: "64", // not really used + suffix: "64", }, }, diff --git a/tests/ApkVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java b/tests/ApkVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java index 5c2c15b22bb0..9be02ec3be86 100644 --- a/tests/ApkVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java +++ b/tests/ApkVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java @@ -32,11 +32,12 @@ import java.util.ArrayList; * <p>To use this class, please push block_device_writer binary to /data/local/tmp. * 1. In Android.bp, add: * <pre> - * target_required: ["block_device_writer_module"], + * data_device_bins_both: ["block_device_writer"], * </pre> * 2. In AndroidText.xml, add: * <pre> - * <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + * <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> + * <option name="append-bitness" value="true" /> * <option name="push" value="block_device_writer->/data/local/tmp/block_device_writer" /> * </target_preparer> * </pre> diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt index 315c40ffa9ba..896c7309cf4c 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt @@ -111,8 +111,8 @@ fun FlickerTestParameter.statusBarLayerIsVisible() { */ fun FlickerTestParameter.navBarLayerPositionStart() { assertLayersStart { - val display = this.entry.displays.minByOrNull { it.id } - ?: throw RuntimeException("There is no display!") + val display = this.entry.displays.firstOrNull { !it.isVirtual } + ?: error("There is no display!") this.visibleRegion(FlickerComponentName.NAV_BAR) .coversExactly(WindowUtils.getNavigationBarPosition(display, isGesturalNavigation)) } @@ -180,7 +180,8 @@ fun FlickerTestParameter.statusBarLayerRotatesScales() { * the visibleRegion of the given app component exactly */ fun FlickerTestParameter.snapshotStartingWindowLayerCoversExactlyOnApp( - component: FlickerComponentName) { + component: FlickerComponentName +) { assertLayers { invoke("snapshotStartingWindowLayerCoversExactlyOnApp") { val snapshotLayers = it.subjects.filter { subject -> diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt index 6c5f6bace49d..d0f1dfddaed0 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt @@ -72,13 +72,15 @@ class CloseAppBackButtonTest(testSpec: FlickerTestParameter) : CloseAppTransitio get() = { super.transition(this) transitions { - device.pressBack() - wmHelper.waitForHomeActivityVisible() + tapl.pressBack() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() } } /** {@inheritDoc} */ - @FlakyTest + @FlakyTest(bugId = 206753786) @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() @@ -118,4 +120,4 @@ class CloseAppBackButtonTest(testSpec: FlickerTestParameter) : CloseAppTransitio .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt index 7451a523f260..b178e8c7eced 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.close -import android.platform.test.annotations.Presubmit import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory @@ -69,38 +68,30 @@ class CloseAppHomeButtonTest(testSpec: FlickerTestParameter) : CloseAppTransitio get() = { super.transition(this) transitions { + // Can't use TAPL at the moment because of rotation test issues + // When pressing home, TAPL expects the orientation to remain constant + // However, when closing a landscape app back to a portrait-only launcher + // this causes an error in verifyActiveContainer(); device.pressHome() - wmHelper.waitForHomeActivityVisible() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() } } /** {@inheritDoc} */ - @FlakyTest + @FlakyTest(bugId = 206753786) @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() /** {@inheritDoc} */ - @FlakyTest(bugId = 227430489) + @FlakyTest(bugId = 206753786) @Test override fun statusBarLayerRotatesScales() { super.statusBarLayerRotatesScales() } /** {@inheritDoc} */ - @Presubmit - @Test - override fun launcherLayerReplacesApp() { - super.launcherLayerReplacesApp() - } - - /** {@inheritDoc} */ - @Presubmit - @Test - override fun entireScreenCovered() { - super.entireScreenCovered() - } - - /** {@inheritDoc} */ @FlakyTest(bugId = 229762973) @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = @@ -120,4 +111,4 @@ class CloseAppHomeButtonTest(testSpec: FlickerTestParameter) : CloseAppTransitio .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt index aaa2db768792..ae002c9458f5 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt @@ -19,21 +19,22 @@ package com.android.server.wm.flicker.close import android.app.Instrumentation import android.platform.test.annotations.Presubmit import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.helpers.StandardAppHelper import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.entireScreenCovered +import com.android.server.wm.flicker.replacesLayer import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.server.wm.flicker.replacesLayer +import com.android.server.wm.traces.common.FlickerComponentName.Companion.LAUNCHER import org.junit.Test /** @@ -42,12 +43,16 @@ import org.junit.Test abstract class CloseAppTransition(protected val testSpec: FlickerTestParameter) { protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() protected open val testApp: StandardAppHelper = SimpleAppHelper(instrumentation) + protected val tapl = LauncherInstrumentation() /** * Specification of the test transition to execute */ protected open val transition: FlickerBuilder.() -> Unit = { setup { + test { + tapl.setExpectedRotation(testSpec.startRotation) + } eachRun { testApp.launchViaIntent(wmHelper) this.setRotation(testSpec.startRotation) @@ -154,7 +159,7 @@ abstract class CloseAppTransition(protected val testSpec: FlickerTestParameter) /** * Checks that [testApp] is the top visible app window at the start of the transition and - * that it is replaced by [LAUNCHER_COMPONENT] during the transition + * that it is replaced by [LAUNCHER] during the transition */ @Presubmit @Test @@ -162,30 +167,30 @@ abstract class CloseAppTransition(protected val testSpec: FlickerTestParameter) testSpec.assertWm { this.isAppWindowOnTop(testApp.component) .then() - .isAppWindowOnTop(LAUNCHER_COMPONENT) + .isAppWindowOnTop(LAUNCHER) } } /** - * Checks that [LAUNCHER_COMPONENT] is invisible at the start of the transition and that + * Checks that [LAUNCHER] is invisible at the start of the transition and that * it becomes visible during the transition */ @Presubmit @Test open fun launcherWindowBecomesVisible() { testSpec.assertWm { - this.isAppWindowNotOnTop(LAUNCHER_COMPONENT) + this.isAppWindowNotOnTop(LAUNCHER) .then() - .isAppWindowOnTop(LAUNCHER_COMPONENT) + .isAppWindowOnTop(LAUNCHER) } } /** - * Checks that [LAUNCHER_COMPONENT] layer becomes visible when [testApp] becomes invisible + * Checks that [LAUNCHER] layer becomes visible when [testApp] becomes invisible */ @Presubmit @Test open fun launcherLayerReplacesApp() { - testSpec.replacesLayer(testApp.component, LAUNCHER_COMPONENT) + testSpec.replacesLayer(testApp.component, LAUNCHER) } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt index aacc17a49a24..cd184ac10c22 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt @@ -16,19 +16,16 @@ package com.android.server.wm.flicker.helpers +import android.app.Instrumentation import android.view.WindowInsets.Type.ime import android.view.WindowInsets.Type.navigationBars import android.view.WindowInsets.Type.statusBars - -import android.app.Instrumentation import androidx.test.uiautomator.By -import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper - import java.util.regex.Pattern class ImeAppAutoFocusHelper @JvmOverloads constructor( @@ -39,12 +36,9 @@ class ImeAppAutoFocusHelper @JvmOverloads constructor( component: FlickerComponentName = ActivityOptions.IME_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME.toFlickerComponent() ) : ImeAppHelper(instr, launcherName, component) { - override fun openIME( - device: UiDevice, - wmHelper: WindowManagerStateHelper? - ) { + override fun openIME(wmHelper: WindowManagerStateHelper) { // do nothing (the app is focused automatically) - waitIMEShown(device, wmHelper) + waitIMEShown(wmHelper) } override fun launchViaIntent( @@ -54,7 +48,7 @@ class ImeAppAutoFocusHelper @JvmOverloads constructor( stringExtras: Map<String, String> ) { super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras) - waitIMEShown(uiDevice, wmHelper) + waitIMEShown(wmHelper) } override fun open() { @@ -70,15 +64,15 @@ class ImeAppAutoFocusHelper @JvmOverloads constructor( val button = uiDevice.wait(Until.findObject(By.res(getPackage(), "start_dialog_themed_activity_btn")), FIND_TIMEOUT) - require(button != null) { + requireNotNull(button) { "Button not found, this usually happens when the device " + "was left in an unknown state (e.g. Screen turned off)" } button.click() - wmHelper.waitForAppTransitionIdle() - wmHelper.waitForFullScreenApp( + wmHelper.StateSyncBuilder() + .withFullScreenApp( ActivityOptions.DIALOG_THEMED_ACTIVITY_COMPONENT_NAME.toFlickerComponent()) - mInstrumentation.waitForIdleSync() + .waitForAndVerify() } fun dismissDialog(wmHelper: WindowManagerStateHelper) { val dialog = uiDevice.wait( @@ -87,14 +81,16 @@ class ImeAppAutoFocusHelper @JvmOverloads constructor( // Pressing back key to dismiss the dialog if (dialog != null) { uiDevice.pressBack() - wmHelper.waitForAppTransitionIdle() + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .waitForAndVerify() } } fun getInsetsVisibleFromDialog(type: Int): Boolean { - var insetsVisibilityTextView = uiDevice.wait( + val insetsVisibilityTextView = uiDevice.wait( Until.findObject(By.res("android:id/text1")), FIND_TIMEOUT) if (insetsVisibilityTextView != null) { - var visibility = insetsVisibilityTextView.text.toString() + val visibility = insetsVisibilityTextView.text.toString() val matcher = when (type) { ime() -> { Pattern.compile("IME\\: (VISIBLE|INVISIBLE)").matcher(visibility) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt index 5bd365c7eefd..8ac9a75aff94 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt @@ -20,7 +20,6 @@ import android.app.Instrumentation import android.support.test.launcherhelper.ILauncherStrategy import android.support.test.launcherhelper.LauncherStrategyFactory import androidx.test.uiautomator.By -import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.server.wm.traces.common.FlickerComponentName @@ -39,64 +38,49 @@ open class ImeAppHelper @JvmOverloads constructor( /** * Opens the IME and wait for it to be displayed * - * @param device UIDevice instance to interact with the device * @param wmHelper Helper used to wait for WindowManager states */ - @JvmOverloads - open fun openIME(device: UiDevice, wmHelper: WindowManagerStateHelper? = null) { - val editText = device.wait( + open fun openIME(wmHelper: WindowManagerStateHelper) { + val editText = uiDevice.wait( Until.findObject(By.res(getPackage(), "plain_text_input")), FIND_TIMEOUT) - require(editText != null) { + requireNotNull(editText) { "Text field not found, this usually happens when the device " + "was left in an unknown state (e.g. in split screen)" } editText.click() - waitIMEShown(device, wmHelper) + waitIMEShown(wmHelper) } - protected fun waitIMEShown( - device: UiDevice, - wmHelper: WindowManagerStateHelper? = null - ) { - if (wmHelper == null) { - device.waitForIdle() - } else { - wmHelper.waitImeShown() - } + protected fun waitIMEShown(wmHelper: WindowManagerStateHelper) { + wmHelper.StateSyncBuilder() + .withImeShown() + .waitForAndVerify() } /** * Opens the IME and wait for it to be gone * - * @param device UIDevice instance to interact with the device * @param wmHelper Helper used to wait for WindowManager states */ - @JvmOverloads - open fun closeIME(device: UiDevice, wmHelper: WindowManagerStateHelper? = null) { - device.pressBack() - // Using only the AccessibilityInfo it is not possible to identify if the IME is active - if (wmHelper == null) { - device.waitForIdle() - } else { - wmHelper.waitImeGone() - } + open fun closeIME(wmHelper: WindowManagerStateHelper) { + uiDevice.pressBack() + wmHelper.StateSyncBuilder() + .withImeGone() + .waitForAndVerify() } - @JvmOverloads - open fun finishActivity(device: UiDevice, wmHelper: WindowManagerStateHelper? = null) { - val finishButton = device.wait( + open fun finishActivity(wmHelper: WindowManagerStateHelper) { + val finishButton = uiDevice.wait( Until.findObject(By.res(getPackage(), "finish_activity_btn")), FIND_TIMEOUT) - require(finishButton != null) { - "Finish activity button not found, probably IME activity is not on the screen ?" + requireNotNull(finishButton) { + "Finish activity button not found, probably IME activity is not on the screen?" } finishButton.click() - if (wmHelper == null) { - device.waitForIdle() - } else { - wmHelper.waitForActivityRemoved(component) - } + wmHelper.StateSyncBuilder() + .withActivityRemoved(component) + .waitForAndVerify() } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeEditorPopupDialogAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeEditorPopupDialogAppHelper.kt index 172c4330c3c6..438aeeb2cc74 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeEditorPopupDialogAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeEditorPopupDialogAppHelper.kt @@ -18,7 +18,6 @@ package com.android.server.wm.flicker.helpers import android.app.Instrumentation import androidx.test.uiautomator.By -import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.server.wm.traces.common.FlickerComponentName @@ -33,18 +32,15 @@ class ImeEditorPopupDialogAppHelper @JvmOverloads constructor( component: FlickerComponentName = ActivityOptions.EDITOR_POPUP_DIALOG_ACTIVITY_COMPONENT_NAME.toFlickerComponent() ) : ImeAppHelper(instr, launcherName, component) { - override fun openIME( - device: UiDevice, - wmHelper: WindowManagerStateHelper? - ) { - val editText = device.wait(Until.findObject(By.text("focused editText")), FIND_TIMEOUT) + override fun openIME(wmHelper: WindowManagerStateHelper) { + val editText = uiDevice.wait(Until.findObject(By.text("focused editText")), FIND_TIMEOUT) require(editText != null) { "Text field not found, this usually happens when the device " + "was left in an unknown state (e.g. in split screen)" } editText.click() - waitIMEShown(device, wmHelper) + waitIMEShown(wmHelper) } fun dismissDialog(wmHelper: WindowManagerStateHelper) { @@ -54,7 +50,9 @@ class ImeEditorPopupDialogAppHelper @JvmOverloads constructor( // Pressing back key to dismiss the dialog if (dismissButton != null) { dismissButton.click() - wmHelper.waitForAppTransitionIdle() + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .waitForAndVerify() } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt index be68704fc32d..1a2543cb8f97 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt @@ -41,12 +41,13 @@ class NewTasksAppHelper @JvmOverloads constructor( Until.findObject(By.res(getPackage(), "launch_new_task")), FIND_TIMEOUT) - require(button != null) { + requireNotNull(button) { "Button not found, this usually happens when the device " + "was left in an unknown state (e.g. in split screen)" } button.click() - wmHelper.waitForAppTransitionIdle() - wmHelper.waitForFullScreenApp(component) + wmHelper.StateSyncBuilder() + .withFullScreenApp(component) + .waitForAndVerify() } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt index 4e360f98723e..00b208bc8c23 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt @@ -20,7 +20,6 @@ import android.app.Instrumentation import android.support.test.launcherhelper.ILauncherStrategy import android.support.test.launcherhelper.LauncherStrategyFactory import androidx.test.uiautomator.By -import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.server.wm.traces.common.FlickerComponentName @@ -36,8 +35,8 @@ class NotificationAppHelper @JvmOverloads constructor( .getInstance(instr) .launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) { - fun postNotification(device: UiDevice, wmHelper: WindowManagerStateHelper) { - val button = device.wait( + fun postNotification(wmHelper: WindowManagerStateHelper) { + val button = uiDevice.wait( Until.findObject(By.res(getPackage(), "post_notification")), FIND_TIMEOUT) @@ -47,7 +46,7 @@ class NotificationAppHelper @JvmOverloads constructor( } button.click() - device.wait(Until.findObject(By.text("Flicker Test Notification")), FIND_TIMEOUT) + uiDevice.wait(Until.findObject(By.text("Flicker Test Notification")), FIND_TIMEOUT) ?: error("Flicker Notification not found") } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt index a135e0af067b..8519da14aa2d 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt @@ -19,13 +19,11 @@ package com.android.server.wm.flicker.helpers import android.app.Instrumentation import android.support.test.launcherhelper.ILauncherStrategy import android.support.test.launcherhelper.LauncherStrategyFactory -import android.view.Display import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.server.wm.traces.common.FlickerComponentName -import com.android.server.wm.traces.common.WindowManagerConditionsFactory import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper @@ -46,21 +44,19 @@ class TwoActivitiesAppHelper @JvmOverloads constructor( val launchActivityButton = By.res(getPackage(), LAUNCH_SECOND_ACTIVITY) val button = device.wait(Until.findObject(launchActivityButton), FIND_TIMEOUT) - require(button != null) { + requireNotNull(button) { "Button not found, this usually happens when the device " + "was left in an unknown state (e.g. in split screen)" } button.click() device.wait(Until.gone(launchActivityButton), FIND_TIMEOUT) - wmHelper.waitFor( - WindowManagerStateHelper.isAppFullScreen(secondActivityComponent), - WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY), - WindowManagerConditionsFactory.hasLayersAnimating().negate() - ) + wmHelper.StateSyncBuilder() + .withFullScreenApp(secondActivityComponent) + .waitForAndVerify() } companion object { private const val LAUNCH_SECOND_ACTIVITY = "launch_second_activity" } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt index 843ef232fdeb..67d2067d1b06 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt @@ -17,10 +17,10 @@ package com.android.server.wm.flicker.ime import android.app.Instrumentation +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface import android.view.WindowManagerPolicyConstants -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider @@ -71,7 +71,6 @@ class CloseImeAutoOpenWindowToAppTest(private val testSpec: FlickerTestParameter setup { eachRun { testApp.launchViaIntent(wmHelper) - testApp.openIME(device, wmHelper) } } teardown { @@ -80,7 +79,7 @@ class CloseImeAutoOpenWindowToAppTest(private val testSpec: FlickerTestParameter } } transitions { - testApp.closeIME(device, wmHelper) + testApp.closeIME(wmHelper) } } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt index 13a49a1cb47c..dcb5dad3e931 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt @@ -17,10 +17,10 @@ package com.android.server.wm.flicker.ime import android.app.Instrumentation +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface import android.view.WindowManagerPolicyConstants -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider @@ -29,11 +29,11 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible @@ -71,7 +71,6 @@ class CloseImeAutoOpenWindowToHomeTest(private val testSpec: FlickerTestParamete setup { eachRun { testApp.launchViaIntent(wmHelper) - testApp.openIME(device, wmHelper) } } teardown { @@ -81,8 +80,10 @@ class CloseImeAutoOpenWindowToHomeTest(private val testSpec: FlickerTestParamete } transitions { device.pressHome() - wmHelper.waitForHomeActivityVisible() - wmHelper.waitImeGone() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .withImeGone() + .waitForAndVerify() } } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt index 2e29b3e314ca..b979f3ec3442 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt @@ -23,9 +23,9 @@ import android.view.WindowManagerPolicyConstants import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider -import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.ImeEditorPopupDialogAppHelper @@ -54,18 +54,22 @@ class CloseImeEditorPopupDialogTest(private val testSpec: FlickerTestParameter) setup { eachRun { imeTestApp.launchViaIntent(wmHelper) - imeTestApp.openIME(device, wmHelper) + imeTestApp.openIME(wmHelper) } } transitions { imeTestApp.dismissDialog(wmHelper) - wmHelper.waitImeGone() + wmHelper.StateSyncBuilder() + .withImeGone() + .waitForAndVerify() } teardown { eachRun { device.pressHome() - wmHelper.waitForHomeActivityVisible() - imeTestApp.exit() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() + imeTestApp.exit(wmHelper) } } } @@ -133,4 +137,4 @@ class CloseImeEditorPopupDialogTest(private val testSpec: FlickerTestParameter) ) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt index 1ce1e1fb05f6..928bb5346671 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt @@ -17,8 +17,8 @@ package com.android.server.wm.flicker.ime import android.app.Instrumentation -import android.platform.test.annotations.Presubmit import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider @@ -27,11 +27,11 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.common.FlickerComponentName @@ -64,7 +64,7 @@ class CloseImeWindowToAppTest(private val testSpec: FlickerTestParameter) { testApp.launchViaIntent(wmHelper) } eachRun { - testApp.openIME(device, wmHelper) + testApp.openIME(wmHelper) } } teardown { @@ -73,7 +73,7 @@ class CloseImeWindowToAppTest(private val testSpec: FlickerTestParameter) { } } transitions { - testApp.closeIME(device, wmHelper) + testApp.closeIME(wmHelper) } } } @@ -163,4 +163,4 @@ class CloseImeWindowToAppTest(private val testSpec: FlickerTestParameter) { .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt index c77ea12a5850..3ff809d38330 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt @@ -17,10 +17,10 @@ package com.android.server.wm.flicker.ime import android.app.Instrumentation +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface import android.view.WindowManagerPolicyConstants -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider @@ -29,11 +29,11 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.common.FlickerComponentName @@ -42,6 +42,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized + /** * Test IME window closing to home transitions. * To run this test: `atest FlickerTests:CloseImeWindowToHomeTest` @@ -61,21 +62,19 @@ class CloseImeWindowToHomeTest(private val testSpec: FlickerTestParameter) { setup { eachRun { testApp.launchViaIntent(wmHelper) - testApp.openIME(device, wmHelper) + testApp.openIME(wmHelper) } } transitions { device.pressHome() - wmHelper.waitForHomeActivityVisible() - wmHelper.waitImeGone() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .withImeGone() + .waitForAndVerify() } teardown { - eachRun { - device.pressHome() - wmHelper.waitForHomeActivityVisible() - } test { - testApp.exit() + testApp.exit(wmHelper) } } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt index b298df2f8ffe..d1761fca899b 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt @@ -16,15 +16,14 @@ package com.android.server.wm.flicker.ime -import android.view.WindowInsets.Type.ime -import android.view.WindowInsets.Type.navigationBars -import android.view.WindowInsets.Type.statusBars - import android.app.Instrumentation +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface +import android.view.WindowInsets.Type.ime +import android.view.WindowInsets.Type.navigationBars +import android.view.WindowInsets.Type.statusBars import android.view.WindowManagerPolicyConstants -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider @@ -34,13 +33,13 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper import com.android.server.wm.traces.common.FlickerComponentName +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue /** * Test IME snapshot mechanism won't apply when transitioning from non-IME focused dialog activity. @@ -60,7 +59,9 @@ class LaunchAppShowImeAndDialogThemeAppTest(private val testSpec: FlickerTestPar setup { eachRun { testApp.launchViaIntent(wmHelper) - wmHelper.waitImeShown() + wmHelper.StateSyncBuilder() + .withImeShown() + .waitForAndVerify() testApp.startDialogThemedActivity(wmHelper) // Verify IME insets isn't visible on dialog since it's non-IME focusable window assertFalse(testApp.getInsetsVisibleFromDialog(ime())) @@ -70,7 +71,7 @@ class LaunchAppShowImeAndDialogThemeAppTest(private val testSpec: FlickerTestPar } teardown { eachRun { - testApp.exit() + testApp.exit(wmHelper) } } transitions { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt index b897ca2a9c15..d393ace3254d 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt @@ -79,19 +79,21 @@ class LaunchAppShowImeOnStartTest(private val testSpec: FlickerTestParameter) { return FlickerBuilder(instrumentation).apply { setup { eachRun { - initializeApp.launchViaIntent() + initializeApp.launchViaIntent(wmHelper) this.setRotation(testSpec.startRotation) } } teardown { eachRun { - initializeApp.exit() - testApp.exit() + initializeApp.exit(wmHelper) + testApp.exit(wmHelper) } } transitions { testApp.launchViaIntent(wmHelper) - wmHelper.waitImeShown() + wmHelper.StateSyncBuilder() + .withImeShown() + .waitForAndVerify() } } } @@ -153,4 +155,4 @@ class LaunchAppShowImeOnStartTest(private val testSpec: FlickerTestParameter) { ) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt index 972918e28fa7..1854d6e5506e 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt @@ -22,11 +22,19 @@ import android.view.Surface import android.view.WindowManagerPolicyConstants import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry -import com.android.server.wm.flicker.* +import com.android.server.wm.flicker.FlickerBuilderProvider +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.navBarLayerIsVisible +import com.android.server.wm.flicker.navBarWindowIsVisible +import com.android.server.wm.flicker.statusBarLayerIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -56,15 +64,15 @@ class OpenImeWindowAndCloseTest(private val testSpec: FlickerTestParameter) { eachRun { simpleApp.launchViaIntent(wmHelper) testApp.launchViaIntent(wmHelper) - testApp.openIME(device, wmHelper) + testApp.openIME(wmHelper) } } transitions { - testApp.finishActivity(device, wmHelper) + testApp.finishActivity(wmHelper) } teardown { test { - simpleApp.exit() + simpleApp.exit(wmHelper) } } } @@ -128,4 +136,4 @@ class OpenImeWindowAndCloseTest(private val testSpec: FlickerTestParameter) { ) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt index fdaeacb5e8a2..4f26fc284198 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt @@ -17,12 +17,12 @@ package com.android.server.wm.flicker.ime import android.app.Instrumentation +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice import android.view.Surface import android.view.WindowManagerPolicyConstants -import android.platform.test.annotations.FlakyTest import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerBuilderProvider @@ -70,7 +70,9 @@ class OpenImeWindowFromFixedOrientationAppTest(private val testSpec: FlickerTest eachRun { // Swiping out the IME activity to home. taplInstrumentation.goHome() - wmHelper.waitForHomeActivityVisible() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() } } transitions { @@ -135,4 +137,4 @@ class OpenImeWindowFromFixedOrientationAppTest(private val testSpec: FlickerTest ) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt index 94a6639e75ac..dbec3643c39f 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt @@ -17,10 +17,10 @@ package com.android.server.wm.flicker.ime import android.app.Instrumentation +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface import android.view.WindowManagerPolicyConstants -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider @@ -28,12 +28,12 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.entireScreenCovered -import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible @@ -65,14 +65,14 @@ class OpenImeWindowTest(private val testSpec: FlickerTestParameter) { } } transitions { - testApp.openIME(device, wmHelper) + testApp.openIME(wmHelper) } teardown { eachRun { - testApp.closeIME(device, wmHelper) + testApp.closeIME(wmHelper) } test { - testApp.exit() + testApp.exit(wmHelper) } } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt index e899a750a5df..eedbd5d3dba8 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt @@ -17,12 +17,12 @@ package com.android.server.wm.flicker.ime import android.app.Instrumentation +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice import android.view.Surface import android.view.WindowManagerPolicyConstants -import android.platform.test.annotations.FlakyTest import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerParametersRunnerFactory @@ -30,8 +30,8 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper +import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.statusBarLayerIsVisible @@ -39,8 +39,8 @@ import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.common.WindowManagerConditionsFactory import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import org.junit.Assume.assumeTrue import org.junit.Assume.assumeFalse +import org.junit.Assume.assumeTrue import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -72,12 +72,17 @@ class OpenImeWindowToOverViewTest(private val testSpec: FlickerTestParameter) { } transitions { device.pressRecentApps() - waitForRecentsActivityVisible(wmHelper) - waitNavStatusBarVisibility(wmHelper) + val builder = wmHelper.StateSyncBuilder() + .withRecentsActivityVisible() + waitNavStatusBarVisibility(builder) + builder.waitForAndVerify() } teardown { test { device.pressHome() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() imeTestApp.exit(wmHelper) } } @@ -97,12 +102,12 @@ class OpenImeWindowToOverViewTest(private val testSpec: FlickerTestParameter) { * * b/227189877 */ - private fun waitNavStatusBarVisibility(wmHelper: WindowManagerStateHelper) { + private fun waitNavStatusBarVisibility(stateSync: WindowManagerStateHelper.StateSyncBuilder) { when { - testSpec.isLandscapeOrSeascapeAtStart && !testSpec.isGesturalNavigation -> - wmHelper.waitFor(statusBarInvisible) testSpec.isLandscapeOrSeascapeAtStart -> - wmHelper.waitFor(statusBarInvisible, navBarInvisible) + stateSync.add(statusBarInvisible) + else -> + stateSync.withNavBarStatusBarVisible() } } @@ -195,25 +200,6 @@ class OpenImeWindowToOverViewTest(private val testSpec: FlickerTestParameter) { } } - private fun waitForRecentsActivityVisible( - wmHelper: WindowManagerStateHelper - ) { - val waitMsg = "state of Recents activity to be visible" - require( - wmHelper.waitFor(waitMsg) { - it.wmState.homeActivity?.let { act -> - it.wmState.isActivityVisible(act.name) - } == true || - it.wmState.recentsActivity?.let { act -> - it.wmState.isActivityVisible(act.name) - } == true - } - ) { "Recents activity should be visible" } - wmHelper.waitForAppTransitionIdle() - // Ensure WindowManagerService wait until all animations have completed - instrumentation.uiAutomation.syncInputTransactions() - } - companion object { /** * Creates the test configurations. @@ -235,4 +221,4 @@ class OpenImeWindowToOverViewTest(private val testSpec: FlickerTestParameter) { ) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt index 2b23d53e8a85..7d4724c48bbb 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt @@ -17,33 +17,30 @@ package com.android.server.wm.flicker.ime import android.app.Instrumentation +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit -import android.view.Display import android.view.Surface -import android.platform.test.annotations.FlakyTest import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group2 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper +import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.helpers.reopenAppFromOverview import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.entireScreenCovered -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.server.wm.flicker.statusBarWindowIsVisible -import com.android.server.wm.traces.common.ConditionList import com.android.server.wm.traces.common.FlickerComponentName -import com.android.server.wm.traces.common.WindowManagerConditionsFactory +import com.android.server.wm.traces.common.FlickerComponentName.Companion.LAUNCHER import org.junit.Assume.assumeFalse import org.junit.Assume.assumeTrue import org.junit.Before @@ -66,12 +63,6 @@ open class ReOpenImeWindowTest(private val testSpec: FlickerTestParameter) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation) - private val waitConditionSetup = ConditionList(listOf( - WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY), - WindowManagerConditionsFactory.hasLayersAnimating().negate(), - WindowManagerConditionsFactory.isHomeActivityVisible() - )) - @Before open fun before() { assumeFalse(isShellTransitionsEnabled) @@ -83,21 +74,25 @@ open class ReOpenImeWindowTest(private val testSpec: FlickerTestParameter) { setup { test { testApp.launchViaIntent(wmHelper) - testApp.openIME(device, wmHelper) + testApp.openIME(wmHelper) } eachRun { - device.pressRecentApps() - wmHelper.waitFor(waitConditionSetup) this.setRotation(testSpec.startRotation) + device.pressRecentApps() + wmHelper.StateSyncBuilder() + .withRecentsActivityVisible() + .waitForAndVerify() } } transitions { device.reopenAppFromOverview(wmHelper) - wmHelper.waitImeShown() + wmHelper.StateSyncBuilder() + .withImeShown() + .waitForAndVerify() } teardown { test { - testApp.exit() + testApp.exit(wmHelper) } } } @@ -128,9 +123,9 @@ open class ReOpenImeWindowTest(private val testSpec: FlickerTestParameter) { @Test fun launcherWindowBecomesInvisible() { testSpec.assertWm { - this.isAppWindowVisible(LAUNCHER_COMPONENT) + this.isAppWindowVisible(LAUNCHER) .then() - .isAppWindowInvisible(LAUNCHER_COMPONENT) + .isAppWindowInvisible(LAUNCHER) } } @@ -208,7 +203,7 @@ open class ReOpenImeWindowTest(private val testSpec: FlickerTestParameter) { @Test fun appLayerReplacesLauncher() { testSpec.assertLayers { - this.isVisible(LAUNCHER_COMPONENT) + this.isVisible(FlickerComponentName.LAUNCHER) .then() .isVisible(FlickerComponentName.SNAPSHOT, isOptional = true) .then() diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt index 4b268a871fa0..d1188467cc01 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt @@ -18,17 +18,16 @@ package com.android.server.wm.flicker.ime import android.app.Instrumentation import android.platform.test.annotations.Presubmit -import android.view.Display import android.view.Surface import android.view.WindowManagerPolicyConstants import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry -import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group4 +import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.helpers.WindowUtils @@ -37,11 +36,8 @@ import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.common.FlickerComponentName -import com.android.server.wm.traces.common.WindowManagerConditionsFactory -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import org.junit.Assume import org.junit.Before - import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -75,32 +71,26 @@ open class SwitchImeWindowsFromGestureNavTest(private val testSpec: FlickerTestP eachRun { this.setRotation(testSpec.startRotation) testApp.launchViaIntent(wmHelper) - val testAppVisible = wmHelper.waitFor( - WindowManagerStateHelper.isAppFullScreen(testApp.component), - WindowManagerConditionsFactory.isAppTransitionIdle( - Display.DEFAULT_DISPLAY)) - require(testAppVisible) { - "Expected ${testApp.component.toWindowName()} to be visible" - } + wmHelper.StateSyncBuilder() + .withFullScreenApp(testApp.component) + .waitForAndVerify() imeTestApp.launchViaIntent(wmHelper) - val imeAppVisible = wmHelper.waitFor( - WindowManagerStateHelper.isAppFullScreen(imeTestApp.component), - WindowManagerConditionsFactory.isAppTransitionIdle( - Display.DEFAULT_DISPLAY)) - require(imeAppVisible) { - "Expected ${imeTestApp.component.toWindowName()} to be visible" - } - - imeTestApp.openIME(device, wmHelper) + wmHelper.StateSyncBuilder() + .withFullScreenApp(imeTestApp.component) + .waitForAndVerify() + + imeTestApp.openIME(wmHelper) } } teardown { eachRun { device.pressHome() - wmHelper.waitForHomeActivityVisible() - testApp.exit() - imeTestApp.exit() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() + testApp.exit(wmHelper) + imeTestApp.exit(wmHelper) } } transitions { @@ -110,8 +100,9 @@ open class SwitchImeWindowsFromGestureNavTest(private val testSpec: FlickerTestP device.swipe(0, displayBounds.bounds.height, displayBounds.bounds.width, displayBounds.bounds.height, 50) - wmHelper.waitForFullScreenApp(testApp.component) - wmHelper.waitForAppTransitionIdle() + wmHelper.StateSyncBuilder() + .withFullScreenApp(testApp.component) + .waitForAndVerify() createTag(TAG_IME_INVISIBLE) } transitions { @@ -119,7 +110,9 @@ open class SwitchImeWindowsFromGestureNavTest(private val testSpec: FlickerTestP val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation) device.swipe(displayBounds.bounds.width, displayBounds.bounds.height, 0, displayBounds.bounds.height, 50) - wmHelper.waitForFullScreenApp(imeTestApp.component) + wmHelper.StateSyncBuilder() + .withFullScreenApp(imeTestApp.component) + .waitForAndVerify() } } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt index cc808a0ce871..a33f0ea5a074 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt @@ -18,22 +18,20 @@ package com.android.server.wm.flicker.launch import android.app.Instrumentation import android.platform.test.annotations.Presubmit -import android.view.Display import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry -import com.android.server.wm.flicker.entireScreenCovered +import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.TwoActivitiesAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions -import com.android.server.wm.traces.common.WindowManagerConditionsFactory +import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -64,6 +62,7 @@ import org.junit.runners.Parameterized class ActivitiesTransitionTest(val testSpec: FlickerTestParameter) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val testApp: TwoActivitiesAppHelper = TwoActivitiesAppHelper(instrumentation) + private val tapl = LauncherInstrumentation() /** * Entry point for the test runner. It will use this method to initialize and cache @@ -74,8 +73,8 @@ class ActivitiesTransitionTest(val testSpec: FlickerTestParameter) { return FlickerBuilder(instrumentation).apply { setup { test { + tapl.setExpectedRotation(testSpec.startRotation) testApp.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(testApp.component) } } teardown { @@ -85,11 +84,10 @@ class ActivitiesTransitionTest(val testSpec: FlickerTestParameter) { } transitions { testApp.openSecondActivity(device, wmHelper) - device.pressBack() - val firstActivityVisible = wmHelper.waitFor( - WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY), - WindowManagerStateHelper.isAppFullScreen(testApp.component)) - require(firstActivityVisible) { "Expected ${testApp.component} to be visible" } + tapl.pressBack() + wmHelper.StateSyncBuilder() + .withFullScreenApp(testApp.component) + .waitForAndVerify() } } } @@ -125,7 +123,7 @@ class ActivitiesTransitionTest(val testSpec: FlickerTestParameter) { fun entireScreenCovered() = testSpec.entireScreenCovered() /** - * Checks that the [LAUNCHER_COMPONENT] window is not on top. The launcher cannot be + * Checks that the [FlickerComponentName.LAUNCHER] window is not on top. The launcher cannot be * asserted with `isAppWindowVisible` because it contains 2 windows with the exact same name, * and both are never simultaneously visible */ @@ -133,17 +131,17 @@ class ActivitiesTransitionTest(val testSpec: FlickerTestParameter) { @Test fun launcherWindowNotOnTop() { testSpec.assertWm { - this.isAppWindowNotOnTop(LAUNCHER_COMPONENT) + this.isAppWindowNotOnTop(FlickerComponentName.LAUNCHER) } } /** - * Checks that the [LAUNCHER_COMPONENT] layer is never visible during the transition + * Checks that the [FlickerComponentName.LAUNCHER] layer is never visible during the transition */ @Presubmit @Test fun launcherLayerNotVisible() { - testSpec.assertLayers { this.isInvisible(LAUNCHER_COMPONENT) } + testSpec.assertLayers { this.isInvisible(FlickerComponentName.LAUNCHER) } } companion object { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt new file mode 100644 index 000000000000..c92704464817 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.launch + +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.RequiresDevice +import android.view.Surface +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test cold launching an app from launcher + * + * To run this test: `atest FlickerTests:OpenAppColdFromIcon` + * + * Actions: + * Make sure no apps are running on the device + * Launch an app [testApp] by clicking it's icon on all apps and wait animation to complete + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [OpenAppTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class OpenAppColdFromIcon(testSpec: FlickerTestParameter) : + OpenAppFromLauncherTransition(testSpec) { + /** + * Defines the transition used to run the test + */ + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + eachRun { + tapl.setExpectedRotation(Surface.ROTATION_0) + RemoveAllTasksButHomeRule.removeAllTasksButHome() + this.setRotation(testSpec.startRotation) + } + } + teardown { + eachRun { + testApp.exit(wmHelper) + } + } + transitions { + tapl.goHome() + .switchToAllApps() + .getAppIcon(testApp.launcherName) + .launch(testApp.`package`) + } + } + + @Postsubmit + @Test + override fun appWindowReplacesLauncherAsTopWindow() = + super.appWindowReplacesLauncherAsTopWindow() + + @Postsubmit + @Test + override fun appLayerBecomesVisible() = + super.appLayerBecomesVisible() + + @Postsubmit + @Test + override fun appLayerReplacesLauncher() = + super.appLayerReplacesLauncher() + + @Postsubmit + @Test + override fun appWindowBecomesTopWindow() = + super.appWindowBecomesTopWindow() + + @Postsubmit + @Test + override fun appWindowBecomesVisible() = + super.appWindowBecomesVisible() + + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + @Postsubmit + @Test + override fun focusChanges() = + super.focusChanges() + + @Postsubmit + @Test + override fun navBarLayerIsVisible() = + super.navBarLayerIsVisible() + + @Postsubmit + @Test + override fun navBarLayerRotatesAndScales() = + super.navBarLayerRotatesAndScales() + + @Postsubmit + @Test + override fun navBarWindowIsVisible() = + super.navBarWindowIsVisible() + + @Postsubmit + @Test + override fun statusBarLayerRotatesScales() = + super.statusBarLayerRotatesScales() + + @Postsubmit + @Test + override fun statusBarLayerIsVisible() = + super.statusBarLayerIsVisible() + + @Postsubmit + @Test + override fun statusBarWindowIsVisible() = + super.statusBarWindowIsVisible() + + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + @Postsubmit + @Test + override fun appWindowIsTopWindowAtEnd() = + super.appWindowIsTopWindowAtEnd() + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests() + } + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt index a25ccfb21948..e52b6c326b73 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt @@ -54,8 +54,8 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 -open class OpenAppColdTest(testSpec: FlickerTestParameter) - : OpenAppFromLauncherTransition(testSpec) { +open class OpenAppColdTest(testSpec: FlickerTestParameter) : + OpenAppFromLauncherTransition(testSpec) { /** * Defines the transition used to run the test */ @@ -75,7 +75,6 @@ open class OpenAppColdTest(testSpec: FlickerTestParameter) } transitions { testApp.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(testApp.component) } } @@ -85,7 +84,7 @@ open class OpenAppColdTest(testSpec: FlickerTestParameter) override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() /** {@inheritDoc} */ - @FlakyTest + @FlakyTest(bugId = 206753786) @Test override fun navBarLayerRotatesAndScales() { super.navBarLayerRotatesAndScales() diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt index c6e92adce8c7..269d82d7fa5a 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt @@ -18,7 +18,6 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.Presubmit import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.replacesLayer import com.android.server.wm.traces.common.FlickerComponentName import org.junit.Test @@ -26,8 +25,8 @@ import org.junit.Test /** * Base class for app launch tests */ -abstract class OpenAppFromLauncherTransition(testSpec: FlickerTestParameter) - : OpenAppTransition(testSpec) { +abstract class OpenAppFromLauncherTransition(testSpec: FlickerTestParameter) : + OpenAppTransition(testSpec) { /** * Checks that the focus changes from the launcher to [testApp] @@ -41,25 +40,25 @@ abstract class OpenAppFromLauncherTransition(testSpec: FlickerTestParameter) } /** - * Checks that [LAUNCHER_COMPONENT] layer is visible at the start of the transition, and - * is replaced by [testApp], which remains visible until the end + * Checks that [FlickerComponentName.LAUNCHER] layer is visible at the start of the transition, + * and is replaced by [testApp], which remains visible until the end */ open fun appLayerReplacesLauncher() { - testSpec.replacesLayer(LAUNCHER_COMPONENT, testApp.component, + testSpec.replacesLayer(FlickerComponentName.LAUNCHER, testApp.component, ignoreEntriesWithRotationLayer = true, ignoreSnapshot = true, ignoreSplashscreen = true) } /** - * Checks that [LAUNCHER_COMPONENT] window is visible at the start of the transition, and - * is replaced by a snapshot or splash screen (optional), and finally, is replaced by - * [testApp], which remains visible until the end + * Checks that [FlickerComponentName.LAUNCHER] window is visible at the start of the + * transition, and is replaced by a snapshot or splash screen (optional), and finally, is + * replaced by [testApp], which remains visible until the end */ @Presubmit @Test open fun appWindowReplacesLauncherAsTopWindow() { testSpec.assertWm { - this.isAppWindowOnTop(LAUNCHER_COMPONENT) + this.isAppWindowOnTop(FlickerComponentName.LAUNCHER) .then() .isAppWindowOnTop(FlickerComponentName.SNAPSHOT, isOptional = true) .then() @@ -68,4 +67,4 @@ abstract class OpenAppFromLauncherTransition(testSpec: FlickerTestParameter) .isAppWindowOnTop(testApp.component) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt index 23baac29b6c1..cea97ad0302a 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt @@ -16,14 +16,16 @@ package com.android.server.wm.flicker.launch +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.RequiresDevice -import android.platform.test.annotations.FlakyTest import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.navBarLayerPositionEnd +import com.android.server.wm.flicker.statusBarLayerPositionEnd import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -43,8 +45,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 @Postsubmit -open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) - : OpenAppFromNotificationCold(testSpec) { +open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) : + OpenAppFromNotificationCold(testSpec) { override val openingNotificationsFromLockScreen = true @@ -62,9 +64,9 @@ open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) setup { eachRun { device.sleep() - wmHelper.waitFor("noAppWindowsOnTop") { - it.wmState.topVisibleAppWindow.isEmpty() - } + wmHelper.StateSyncBuilder() + .withoutTopVisibleAppWindows() + .waitForAndVerify() } } } @@ -85,6 +87,62 @@ open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) @Test override fun appWindowBecomesTopWindow() = super.appWindowBecomesTopWindow() + /** + * Checks the position of the navigation bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerPositionEnd() + + /** + * Checks the position of the status bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerPositionEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appLayerBecomesVisible() = super.appLayerBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appWindowBecomesVisible() = super.appWindowBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + companion object { /** * Creates the test configurations. @@ -95,8 +153,8 @@ open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { - return com.android.server.wm.flicker.FlickerTestParameterFactory.getInstance() + return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt index 49d8ea628585..3d5f31709a66 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt @@ -16,14 +16,16 @@ package com.android.server.wm.flicker.launch +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.RequiresDevice -import android.platform.test.annotations.FlakyTest import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.navBarLayerPositionEnd +import com.android.server.wm.flicker.statusBarLayerPositionEnd import com.android.server.wm.traces.common.FlickerComponentName import org.junit.FixMethodOrder import org.junit.Test @@ -44,8 +46,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 @Postsubmit -open class OpenAppFromLockNotificationWarm(testSpec: FlickerTestParameter) - : OpenAppFromNotificationWarm(testSpec) { +open class OpenAppFromLockNotificationWarm(testSpec: FlickerTestParameter) : + OpenAppFromNotificationWarm(testSpec) { override val openingNotificationsFromLockScreen = true @@ -63,9 +65,9 @@ open class OpenAppFromLockNotificationWarm(testSpec: FlickerTestParameter) setup { eachRun { device.sleep() - wmHelper.waitFor("noAppWindowsOnTop") { - it.wmState.topVisibleAppWindow.isEmpty() - } + wmHelper.StateSyncBuilder() + .withoutTopVisibleAppWindows() + .waitForAndVerify() } } } @@ -111,6 +113,67 @@ open class OpenAppFromLockNotificationWarm(testSpec: FlickerTestParameter) override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() + /** + * Checks the position of the navigation bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerPositionEnd() + + /** + * Checks the position of the status bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerPositionEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appLayerBecomesVisible() = super.appLayerBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appWindowBecomesTopWindow() = super.appWindowBecomesTopWindow() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appWindowBecomesVisible() = super.appWindowBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + companion object { /** * Creates the test configurations. @@ -121,8 +184,8 @@ open class OpenAppFromLockNotificationWarm(testSpec: FlickerTestParameter) @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { - return com.android.server.wm.flicker.FlickerTestParameterFactory.getInstance() + return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt index 950f52ab57e1..446faa743450 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt @@ -16,9 +16,9 @@ package com.android.server.wm.flicker.launch +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.RequiresDevice -import android.platform.test.annotations.FlakyTest import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory @@ -26,6 +26,8 @@ import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.ShowWhenLockedAppHelper import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerPositionEnd +import com.android.server.wm.flicker.statusBarLayerPositionEnd import com.android.server.wm.traces.common.FlickerComponentName import org.junit.FixMethodOrder import org.junit.Test @@ -45,8 +47,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 @Postsubmit -class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParameter) - : OpenAppFromLockNotificationCold(testSpec) { +class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParameter) : + OpenAppFromLockNotificationCold(testSpec) { private val showWhenLockedApp: ShowWhenLockedAppHelper = ShowWhenLockedAppHelper(instrumentation) @@ -64,12 +66,14 @@ class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParamet // Launch an activity that is shown when the device is locked showWhenLockedApp.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(showWhenLockedApp.component) + wmHelper.StateSyncBuilder() + .withFullScreenApp(showWhenLockedApp.component) + .waitForAndVerify() device.sleep() - wmHelper.waitFor("noAppWindowsOnTop") { - it.wmState.topVisibleAppWindow.isEmpty() - } + wmHelper.StateSyncBuilder() + .withoutTopVisibleAppWindows() + .waitForAndVerify() } } @@ -109,6 +113,73 @@ class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParamet @Test override fun entireScreenCovered() = super.entireScreenCovered() + /** + * Checks the position of the navigation bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerPositionEnd() + + /** + * Checks the position of the status bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerPositionEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appLayerBecomesVisible() = super.appLayerBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appWindowBecomesTopWindow() = super.appWindowBecomesTopWindow() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appWindowBecomesVisible() = super.appWindowBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { /** * Creates the test configurations. @@ -123,4 +194,4 @@ class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParamet .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt index 9840520d5dbb..85024e744dea 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt @@ -16,19 +16,21 @@ package com.android.server.wm.flicker.launch -import android.platform.test.annotations.Presubmit import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.navBarLayerPositionEnd +import com.android.server.wm.flicker.statusBarLayerPositionEnd import com.android.server.wm.traces.common.FlickerComponentName import org.junit.Test /** * Base class for app launch tests from lock screen */ -abstract class OpenAppFromLockTransition(testSpec: FlickerTestParameter) - : OpenAppTransition(testSpec) { +abstract class OpenAppFromLockTransition(testSpec: FlickerTestParameter) : + OpenAppTransition(testSpec) { /** * Defines the transition used to run the test @@ -39,9 +41,9 @@ abstract class OpenAppFromLockTransition(testSpec: FlickerTestParameter) setup { eachRun { device.sleep() - wmHelper.waitFor("noAppWindowsOnTop") { - it.wmState.topVisibleAppWindow.isEmpty() - } + wmHelper.StateSyncBuilder() + .withoutTopVisibleAppWindows() + .waitForAndVerify() } } teardown { @@ -51,7 +53,6 @@ abstract class OpenAppFromLockTransition(testSpec: FlickerTestParameter) } transitions { testApp.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(testApp.component) } } @@ -112,6 +113,16 @@ abstract class OpenAppFromLockTransition(testSpec: FlickerTestParameter) override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerPositionEnd() /** + * Checks the position of the status bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerPositionEnd() + + /** * Checks that the status bar layer is visible at the end of the trace * * It is not possible to check at the start because the screen is off diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt index 5022dd8f9bff..68e8e467b950 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt @@ -42,8 +42,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 @Postsubmit -open class OpenAppFromNotificationCold(testSpec: FlickerTestParameter) - : OpenAppFromNotificationWarm(testSpec) { +open class OpenAppFromNotificationCold(testSpec: FlickerTestParameter) : + OpenAppFromNotificationWarm(testSpec) { override val transition: FlickerBuilder.() -> Unit get() = { super.transition(this) @@ -52,9 +52,9 @@ open class OpenAppFromNotificationCold(testSpec: FlickerTestParameter) eachRun { // Close the app that posted the notification to trigger a cold start next time // it is open - can't just kill it because that would remove the notification. - taplInstrumentation.goHome() - taplInstrumentation.workspace.switchToOverview() - taplInstrumentation.overview.dismissAllTasks() + tapl.goHome() + tapl.workspace.switchToOverview() + tapl.overview.dismissAllTasks() } } } @@ -77,8 +77,8 @@ open class OpenAppFromNotificationCold(testSpec: FlickerTestParameter) @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { - return com.android.server.wm.flicker.FlickerTestParameterFactory.getInstance() + return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt index 2d00f3f92dcc..6d5911152dc0 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt @@ -16,14 +16,13 @@ package com.android.server.wm.flicker.launch +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.RequiresDevice import android.view.WindowInsets import android.view.WindowManager -import android.platform.test.annotations.FlakyTest import androidx.test.uiautomator.By import androidx.test.uiautomator.Until -import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory @@ -53,10 +52,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 @Postsubmit -open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) - : OpenAppTransition(testSpec) { - protected val taplInstrumentation = LauncherInstrumentation() - +open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) : + OpenAppTransition(testSpec) { override val testApp: NotificationAppHelper = NotificationAppHelper(instrumentation) open val openingNotificationsFromLockScreen = false @@ -70,10 +67,14 @@ open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) } eachRun { testApp.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(testApp.component) - testApp.postNotification(device, wmHelper) - device.pressHome() - wmHelper.waitForAppTransitionIdle() + wmHelper.StateSyncBuilder() + .withFullScreenApp(testApp.component) + .waitForAndVerify() + testApp.postNotification(wmHelper) + tapl.goHome() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() } } @@ -106,7 +107,9 @@ open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) instrumentation.uiAutomation.syncInputTransactions() // Wait for the app to launch - wmHelper.waitForFullScreenApp(testApp.component) + wmHelper.StateSyncBuilder() + .withFullScreenApp(testApp.component) + .waitForAndVerify() } teardown { @@ -180,6 +183,32 @@ open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) super.appWindowBecomesTopWindow() } + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { /** * Creates the test configurations. @@ -194,4 +223,4 @@ open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt index f7532b201b75..c7883d65856d 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt @@ -19,17 +19,14 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice -import android.view.Display +import android.view.Surface import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled -import com.android.server.wm.flicker.helpers.reopenAppFromOverview import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.traces.common.WindowManagerConditionsFactory import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test @@ -61,8 +58,8 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 -open class OpenAppFromOverviewTest(testSpec: FlickerTestParameter) - : OpenAppFromLauncherTransition(testSpec) { +open class OpenAppFromOverviewTest(testSpec: FlickerTestParameter) : + OpenAppFromLauncherTransition(testSpec) { /** * Defines the transition used to run the test @@ -75,27 +72,25 @@ open class OpenAppFromOverviewTest(testSpec: FlickerTestParameter) testApp.launchViaIntent(wmHelper) } eachRun { + // Can't use tapl.goHome() because of b/235841947 device.pressHome() - wmHelper.waitForAppTransitionIdle() - device.pressRecentApps() - wmHelper.waitFor( - WindowManagerConditionsFactory - .isAppTransitionIdle(Display.DEFAULT_DISPLAY), - WindowManagerConditionsFactory.isActivityVisible(LAUNCHER_COMPONENT), - WindowManagerConditionsFactory.hasLayersAnimating().negate() - ) + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() + // Launcher is always ROTATION_0 + tapl.setExpectedRotation(Surface.ROTATION_0) + tapl.workspace.switchToOverview() + wmHelper.StateSyncBuilder() + .withRecentsActivityVisible() + .waitForAndVerify() this.setRotation(testSpec.startRotation) } } transitions { - device.reopenAppFromOverview(wmHelper) - wmHelper.waitFor( - WindowManagerConditionsFactory.hasLayersAnimating().negate(), - WindowManagerConditionsFactory.isWMStateComplete(), - WindowManagerConditionsFactory.isLayerVisible(LAUNCHER_COMPONENT).negate(), - WindowManagerConditionsFactory.isActivityVisible(LAUNCHER_COMPONENT).negate() - ) - wmHelper.waitForFullScreenApp(testApp.component) + tapl.overview.currentTask.open() + wmHelper.StateSyncBuilder() + .withFullScreenApp(testApp.component) + .waitForAndVerify() } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt index fd74ea5dd973..04523398046f 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt @@ -16,8 +16,8 @@ package com.android.server.wm.flicker.launch -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice import android.view.Surface @@ -60,10 +60,9 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 -open class OpenAppNonResizeableTest(testSpec: FlickerTestParameter) - : OpenAppFromLockTransition(testSpec) { +open class OpenAppNonResizeableTest(testSpec: FlickerTestParameter) : + OpenAppFromLockTransition(testSpec) { override val testApp = NonResizeableAppHelper(instrumentation) - private val colorFadComponent = FlickerComponentName("", "ColorFade BLAST#") /** * Checks that the nav bar layer starts invisible, becomes visible during unlocking animation diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt index 20e6d0222854..9c97970b4b91 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt @@ -19,14 +19,15 @@ package com.android.server.wm.flicker.launch import android.app.Instrumentation import android.platform.test.annotations.Presubmit import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.helpers.StandardAppHelper import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsVisible @@ -42,6 +43,7 @@ import org.junit.Test abstract class OpenAppTransition(protected val testSpec: FlickerTestParameter) { protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() protected open val testApp: StandardAppHelper = SimpleAppHelper(instrumentation) + protected val tapl = LauncherInstrumentation() /** * Defines the transition used to run the test @@ -49,6 +51,7 @@ abstract class OpenAppTransition(protected val testSpec: FlickerTestParameter) { protected open val transition: FlickerBuilder.() -> Unit = { setup { test { + tapl.setExpectedRotation(testSpec.startRotation) device.wakeUpAndGoToHomeScreen() this.setRotation(testSpec.startRotation) } @@ -225,11 +228,23 @@ abstract class OpenAppTransition(protected val testSpec: FlickerTestParameter) { testSpec.assertWm { this.isAppWindowNotOnTop(testApp.component) .then() - .isAppWindowOnTop(FlickerComponentName.SNAPSHOT, isOptional = true) - .then() - .isAppWindowOnTop(FlickerComponentName.SPLASH_SCREEN, isOptional = true) - .then() - .isAppWindowOnTop(testApp.component) + .isAppWindowOnTop( + testApp.component + .or(FlickerComponentName.SNAPSHOT) + .or(FlickerComponentName.SPLASH_SCREEN) + ) + } + } + + /** + * Checks that [testApp] window is not on top at the start of the transition, and then becomes + * the top visible window until the end of the transition. + */ + @Presubmit + @Test + open fun appWindowIsTopWindowAtEnd() { + testSpec.assertWmEnd { + this.isAppWindowOnTop(testApp.component) } } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt index 45f339979b15..88e2b2d8e7b7 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt @@ -16,9 +16,9 @@ package com.android.server.wm.flicker.launch +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice -import android.platform.test.annotations.FlakyTest import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory @@ -54,8 +54,8 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 -open class OpenAppWarmTest(testSpec: FlickerTestParameter) - : OpenAppFromLauncherTransition(testSpec) { +open class OpenAppWarmTest(testSpec: FlickerTestParameter) : + OpenAppFromLauncherTransition(testSpec) { /** * Defines the transition used to run the test */ @@ -68,18 +68,19 @@ open class OpenAppWarmTest(testSpec: FlickerTestParameter) } eachRun { device.pressHome() - wmHelper.waitForHomeActivityVisible() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() this.setRotation(testSpec.startRotation) } } teardown { - eachRun { + test { testApp.exit(wmHelper) } } transitions { testApp.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(testApp.component) } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt index 2f546b56f145..99df1f4b5798 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt @@ -21,11 +21,11 @@ import android.app.WallpaperManager import android.platform.test.annotations.Postsubmit import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.entireScreenCovered @@ -64,10 +64,10 @@ import org.junit.runners.Parameterized @Group4 class TaskTransitionTest(val testSpec: FlickerTestParameter) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() private val mTestApp: NewTasksAppHelper = NewTasksAppHelper(instrumentation) private val mWallpaper by lazy { - getWallpaperPackage(InstrumentationRegistry.getInstrumentation()) - ?: error("Unable to obtain wallpaper") + getWallpaperPackage(instrumentation) ?: error("Unable to obtain wallpaper") } @FlickerBuilderProvider @@ -76,19 +76,19 @@ class TaskTransitionTest(val testSpec: FlickerTestParameter) { setup { eachRun { mTestApp.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(mTestApp.component) } } teardown { test { - mTestApp.exit() + mTestApp.exit(wmHelper) } } transitions { mTestApp.openNewTask(device, wmHelper) - device.pressBack() - wmHelper.waitForAppTransitionIdle() - wmHelper.waitForFullScreenApp(mTestApp.component) + tapl.pressBack() + wmHelper.StateSyncBuilder() + .withFullScreenApp(mTestApp.component) + .waitForAndVerify() } } } @@ -126,7 +126,7 @@ class TaskTransitionTest(val testSpec: FlickerTestParameter) { @Test fun launcherWindowIsNeverVisible() { testSpec.assertWm { - this.isAppWindowInvisible(LAUNCHER_COMPONENT) + this.isAppWindowInvisible(FlickerComponentName.LAUNCHER) } } @@ -138,7 +138,7 @@ class TaskTransitionTest(val testSpec: FlickerTestParameter) { @Test fun launcherLayerIsNeverVisible() { testSpec.assertLayers { - this.isInvisible(LAUNCHER_COMPONENT) + this.isInvisible(FlickerComponentName.LAUNCHER) } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt index c89e6a44ab6c..1a712842fec0 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt @@ -17,6 +17,7 @@ package com.android.server.wm.flicker.quickswitch import android.app.Instrumentation +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice import android.view.Surface @@ -27,12 +28,10 @@ import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.NonResizeableAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper -import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales @@ -40,6 +39,7 @@ import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.Rect import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder @@ -66,13 +66,11 @@ import org.junit.runners.Parameterized @Group1 open class QuickSwitchBetweenTwoAppsBackTest(private val testSpec: FlickerTestParameter) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() - private val taplInstrumentation = LauncherInstrumentation() + private val tapl = LauncherInstrumentation() private val testApp1 = SimpleAppHelper(instrumentation) private val testApp2 = NonResizeableAppHelper(instrumentation) - private val startDisplayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation) - @Before open fun before() { Assume.assumeFalse(isShellTransitionsEnabled) @@ -83,28 +81,26 @@ open class QuickSwitchBetweenTwoAppsBackTest(private val testSpec: FlickerTestPa return FlickerBuilder(instrumentation).apply { setup { test { - taplInstrumentation.setExpectedRotation(testSpec.startRotation) + tapl.setExpectedRotation(testSpec.startRotation) } - eachRun { testApp1.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(testApp1.component) - testApp2.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(testApp2.component) + startDisplayBounds = wmHelper.currentState.layerState + .physicalDisplayBounds ?: error("Display not found") } } transitions { - taplInstrumentation.launchedAppState.quickSwitchToPreviousApp() - wmHelper.waitForFullScreenApp(testApp1.component) - wmHelper.waitForAppTransitionIdle() - wmHelper.waitForNavBarStatusBarVisible() + tapl.launchedAppState.quickSwitchToPreviousApp() + wmHelper.StateSyncBuilder() + .withNavBarStatusBarVisible() + .waitForAndVerify() } teardown { test { - testApp1.exit() - testApp2.exit() + testApp1.exit(wmHelper) + testApp2.exit(wmHelper) } } } @@ -251,7 +247,7 @@ open class QuickSwitchBetweenTwoAppsBackTest(private val testSpec: FlickerTestPa this.isAppWindowVisible(testApp2.component) .then() // TODO: Do we actually want to test this? Seems too implementation specific... - .isAppWindowVisible(LAUNCHER_COMPONENT, isOptional = true) + .isAppWindowVisible(FlickerComponentName.LAUNCHER, isOptional = true) .then() .isAppWindowVisible(FlickerComponentName.SNAPSHOT, isOptional = true) .then() @@ -270,7 +266,7 @@ open class QuickSwitchBetweenTwoAppsBackTest(private val testSpec: FlickerTestPa testSpec.assertLayers { this.isVisible(testApp2.component) .then() - .isVisible(LAUNCHER_COMPONENT, isOptional = true) + .isVisible(FlickerComponentName.LAUNCHER, isOptional = true) .then() .isVisible(FlickerComponentName.SNAPSHOT, isOptional = true) .then() @@ -297,7 +293,7 @@ open class QuickSwitchBetweenTwoAppsBackTest(private val testSpec: FlickerTestPa * * NOTE: This doesn't check that the navbar is visible or not. */ - @Presubmit + @FlakyTest @Test fun navbarIsAlwaysInRightPosition() = testSpec.navBarLayerRotatesAndScales() @@ -316,6 +312,8 @@ open class QuickSwitchBetweenTwoAppsBackTest(private val testSpec: FlickerTestPa fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsVisible() companion object { + private var startDisplayBounds = Rect.EMPTY + @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt index 725d2c3d818c..9e43a97a6242 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt @@ -17,6 +17,7 @@ package com.android.server.wm.flicker.quickswitch import android.app.Instrumentation +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice import android.view.Surface @@ -27,7 +28,6 @@ import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.NonResizeableAppHelper @@ -66,7 +66,7 @@ import org.junit.runners.Parameterized @Group1 open class QuickSwitchBetweenTwoAppsForwardTest(private val testSpec: FlickerTestParameter) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() - private val taplInstrumentation = LauncherInstrumentation() + private val tapl = LauncherInstrumentation() private val testApp1 = SimpleAppHelper(instrumentation) private val testApp2 = NonResizeableAppHelper(instrumentation) @@ -81,33 +81,24 @@ open class QuickSwitchBetweenTwoAppsForwardTest(private val testSpec: FlickerTes return FlickerBuilder(instrumentation).apply { setup { test { - taplInstrumentation.setExpectedRotation(testSpec.startRotation) + tapl.setExpectedRotation(testSpec.startRotation) } - eachRun { testApp1.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(testApp1.component) - testApp2.launchViaIntent(wmHelper) - wmHelper.waitForFullScreenApp(testApp2.component) - + tapl.launchedAppState.quickSwitchToPreviousApp() + wmHelper.StateSyncBuilder() + .withNavBarStatusBarVisible() + .waitForAndVerify() startDisplayBounds = wmHelper.currentState.layerState - .displays.firstOrNull { !it.isVirtual } - ?.layerStackSpace - ?: error("Display not found") - - taplInstrumentation.launchedAppState.quickSwitchToPreviousApp() - - wmHelper.waitForFullScreenApp(testApp1.component) - wmHelper.waitForAppTransitionIdle() + .physicalDisplayBounds ?: error("Display not found") } } transitions { - taplInstrumentation.launchedAppState.quickSwitchToPreviousAppSwipeLeft() - - wmHelper.waitForFullScreenApp(testApp2.component) - wmHelper.waitForAppTransitionIdle() - wmHelper.waitForNavBarStatusBarVisible() + tapl.launchedAppState.quickSwitchToPreviousAppSwipeLeft() + wmHelper.StateSyncBuilder() + .withNavBarStatusBarVisible() + .waitForAndVerify() } teardown { @@ -261,7 +252,7 @@ open class QuickSwitchBetweenTwoAppsForwardTest(private val testSpec: FlickerTes testSpec.assertWm { this.isAppWindowVisible(testApp1.component) .then() - .isAppWindowVisible(LAUNCHER_COMPONENT, isOptional = true) + .isAppWindowVisible(FlickerComponentName.LAUNCHER, isOptional = true) .then() .isAppWindowVisible(FlickerComponentName.SNAPSHOT, isOptional = true) .then() @@ -280,7 +271,7 @@ open class QuickSwitchBetweenTwoAppsForwardTest(private val testSpec: FlickerTes testSpec.assertLayers { this.isVisible(testApp1.component) .then() - .isVisible(LAUNCHER_COMPONENT, isOptional = true) + .isVisible(FlickerComponentName.LAUNCHER, isOptional = true) .then() .isVisible(FlickerComponentName.SNAPSHOT, isOptional = true) .then() @@ -311,7 +302,7 @@ open class QuickSwitchBetweenTwoAppsForwardTest(private val testSpec: FlickerTes * * NOTE: This doesn't check that the navbar is visible or not. */ - @Presubmit + @FlakyTest @Test open fun navbarIsAlwaysInRightPosition() { testSpec.navBarLayerRotatesAndScales() diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt index cc4a4b2d38aa..4ea1f1c18b52 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt @@ -27,18 +27,17 @@ import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.SimpleAppHelper -import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.Rect import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -67,8 +66,6 @@ class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) { private val testApp = SimpleAppHelper(instrumentation) - private val startDisplayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation) - @FlickerBuilderProvider fun buildFlicker(): FlickerBuilder { return FlickerBuilder(instrumentation).apply { @@ -80,20 +77,26 @@ class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) { eachRun { testApp.launchViaIntent(wmHelper) device.pressHome() - wmHelper.waitForHomeActivityVisible() - wmHelper.waitForWindowSurfaceDisappeared(testApp.component) + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .withWindowSurfaceDisappeared(testApp.component) + .waitForAndVerify() + + startDisplayBounds = wmHelper.currentState.layerState + .physicalDisplayBounds ?: error("Display not found") } } transitions { taplInstrumentation.workspace.quickSwitchToPreviousApp() - wmHelper.waitForFullScreenApp(testApp.component) - wmHelper.waitForAppTransitionIdle() - wmHelper.waitForNavBarStatusBarVisible() + wmHelper.StateSyncBuilder() + .withFullScreenApp(testApp.component) + .withNavBarStatusBarVisible() + .waitForAndVerify() } teardown { eachRun { - testApp.exit() + testApp.exit(wmHelper) } } } @@ -154,7 +157,7 @@ class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) { @Test fun startsWithLauncherWindowsCoverFullScreen() { testSpec.assertWmStart { - this.frameRegion(LAUNCHER_COMPONENT).coversExactly(startDisplayBounds) + this.frameRegion(FlickerComponentName.LAUNCHER).coversExactly(startDisplayBounds) } } @@ -166,7 +169,7 @@ class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) { @Test fun startsWithLauncherLayersCoverFullScreen() { testSpec.assertLayersStart { - this.visibleRegion(LAUNCHER_COMPONENT).coversExactly(startDisplayBounds) + this.visibleRegion(FlickerComponentName.LAUNCHER).coversExactly(startDisplayBounds) } } @@ -177,7 +180,7 @@ class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) { @Test fun startsWithLauncherBeingOnTop() { testSpec.assertWmStart { - this.isAppWindowOnTop(LAUNCHER_COMPONENT) + this.isAppWindowOnTop(FlickerComponentName.LAUNCHER) } } @@ -229,9 +232,9 @@ class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) { @Test fun launcherWindowBecomesAndStaysInvisible() { testSpec.assertWm { - this.isAppWindowOnTop(LAUNCHER_COMPONENT) + this.isAppWindowOnTop(FlickerComponentName.LAUNCHER) .then() - .isAppWindowNotOnTop(LAUNCHER_COMPONENT) + .isAppWindowNotOnTop(FlickerComponentName.LAUNCHER) } } @@ -243,9 +246,9 @@ class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) { @Test fun launcherLayerBecomesAndStaysInvisible() { testSpec.assertLayers { - this.isVisible(LAUNCHER_COMPONENT) + this.isVisible(FlickerComponentName.LAUNCHER) .then() - .isInvisible(LAUNCHER_COMPONENT) + .isInvisible(FlickerComponentName.LAUNCHER) } } @@ -257,7 +260,7 @@ class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) { @Test fun appWindowIsVisibleOnceLauncherWindowIsInvisible() { testSpec.assertWm { - this.isAppWindowOnTop(LAUNCHER_COMPONENT) + this.isAppWindowOnTop(FlickerComponentName.LAUNCHER) .then() .isAppWindowVisible(FlickerComponentName.SNAPSHOT, isOptional = true) .then() @@ -273,7 +276,7 @@ class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) { @Test fun appLayerIsVisibleOnceLauncherLayerIsInvisible() { testSpec.assertLayers { - this.isVisible(LAUNCHER_COMPONENT) + this.isVisible(FlickerComponentName.LAUNCHER) .then() .isVisible(FlickerComponentName.SNAPSHOT, isOptional = true) .then() @@ -326,6 +329,8 @@ class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) { fun screenIsAlwaysFilled() = testSpec.entireScreenCovered() companion object { + private var startDisplayBounds = Rect.EMPTY + @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt index 0becadf630e1..586be06d1a29 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt @@ -22,12 +22,12 @@ import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.StandardAppHelper import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales import com.android.server.wm.flicker.navBarWindowIsVisible -import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.traces.common.FlickerComponentName import org.junit.Test @@ -47,7 +47,7 @@ abstract class RotationTransition(protected val testSpec: FlickerTestParameter) } teardown { test { - testApp.exit() + testApp.exit(wmHelper) } } transitions { diff --git a/tests/RollbackTest/SampleRollbackApp/Android.bp b/tests/RollbackTest/SampleRollbackApp/Android.bp new file mode 100644 index 000000000000..a18488d8f57f --- /dev/null +++ b/tests/RollbackTest/SampleRollbackApp/Android.bp @@ -0,0 +1,32 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_app { + name: "SampleRollbackApp", + srcs: [ + "src/**/*.java", + ], + resource_dirs: ["res"], + certificate: "platform", + sdk_version: "system_current", +} diff --git a/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml b/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml new file mode 100644 index 000000000000..5a135c978343 --- /dev/null +++ b/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.sample.rollbackapp" > + <uses-permission android:name="android.permission.TEST_MANAGE_ROLLBACKS" /> + <application + android:label="@string/title_activity_main"> + <activity + android:name="com.android.sample.rollbackapp.MainActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/RollbackTest/SampleRollbackApp/res/layout/activity_main.xml b/tests/RollbackTest/SampleRollbackApp/res/layout/activity_main.xml new file mode 100644 index 000000000000..3fb987bb539c --- /dev/null +++ b/tests/RollbackTest/SampleRollbackApp/res/layout/activity_main.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <Button + android:id="@+id/trigger_rollback_button" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + style="?android:attr/buttonBarButtonStyle" + android:text="Rollback Selected" /> + + <ListView + android:id="@+id/listView" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:divider="?android:attr/dividerHorizontal" + android:dividerHeight="1dp" /> + +</LinearLayout>
\ No newline at end of file diff --git a/tests/RollbackTest/SampleRollbackApp/res/layout/listitem_rollbackinfo.xml b/tests/RollbackTest/SampleRollbackApp/res/layout/listitem_rollbackinfo.xml new file mode 100644 index 000000000000..f650dd5d2230 --- /dev/null +++ b/tests/RollbackTest/SampleRollbackApp/res/layout/listitem_rollbackinfo.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" +> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingTop="10dp" + android:paddingLeft="10dp" + android:paddingRight="10dp"> + <TextView android:id="@+id/rollback_id" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="20dp"/> + <TextView android:id="@+id/rollback_packages" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="16dp"/> + <CheckBox android:id="@+id/checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Roll Back"/> + </LinearLayout> +</RelativeLayout>
\ No newline at end of file diff --git a/tests/RollbackTest/SampleRollbackApp/res/values/strings.xml b/tests/RollbackTest/SampleRollbackApp/res/values/strings.xml new file mode 100644 index 000000000000..a85b6800a146 --- /dev/null +++ b/tests/RollbackTest/SampleRollbackApp/res/values/strings.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <string name="title_activity_main" description="Launcher title">Rollback Sample App</string> +</resources>
\ No newline at end of file diff --git a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java new file mode 100644 index 000000000000..916551a8ce6d --- /dev/null +++ b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sample.rollbackapp; + +import static android.app.PendingIntent.FLAG_MUTABLE; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.rollback.PackageRollbackInfo; +import android.content.rollback.RollbackInfo; +import android.content.rollback.RollbackManager; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MainActivity extends Activity { + + List<Integer> mIdsToRollback = new ArrayList<>(); + Button mTriggerRollbackButton; + RollbackManager mRollbackManager; + static final String ROLLBACK_ID_EXTRA = "rollbackId"; + static final String ACTION_NAME = MainActivity.class.getName(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + ListView rollbackListView = findViewById(R.id.listView); + mRollbackManager = getApplicationContext().getSystemService(RollbackManager.class); + initTriggerRollbackButton(); + + // Populate list of available rollbacks. + List<RollbackInfo> availableRollbacks = mRollbackManager.getAvailableRollbacks(); + CustomAdapter adapter = new CustomAdapter(availableRollbacks); + rollbackListView.setAdapter(adapter); + + // Register receiver for rollback status events. + getApplicationContext().registerReceiver( + new BroadcastReceiver() { + @Override + public void onReceive(Context context, + Intent intent) { + int rollbackId = intent.getIntExtra(ROLLBACK_ID_EXTRA, -1); + int rollbackStatusCode = intent.getIntExtra(RollbackManager.EXTRA_STATUS, + RollbackManager.STATUS_FAILURE); + String rollbackStatus = "FAILED"; + if (rollbackStatusCode == RollbackManager.STATUS_SUCCESS) { + rollbackStatus = "SUCCESS"; + } + makeToast("Status for rollback ID " + rollbackId + " is " + rollbackStatus); + }}, new IntentFilter(ACTION_NAME), Context.RECEIVER_NOT_EXPORTED); + } + + private void initTriggerRollbackButton() { + mTriggerRollbackButton = findViewById(R.id.trigger_rollback_button); + mTriggerRollbackButton.setClickable(false); + mTriggerRollbackButton.setOnClickListener(v -> { + // Commits all selected rollbacks. Rollback status events will be sent to our receiver. + for (int i = 0; i < mIdsToRollback.size(); i++) { + Intent intent = new Intent(ACTION_NAME); + intent.putExtra(ROLLBACK_ID_EXTRA, mIdsToRollback.get(i)); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + getApplicationContext(), 0, intent, FLAG_MUTABLE); + mRollbackManager.commitRollback(mIdsToRollback.get(i), + Collections.emptyList(), + pendingIntent.getIntentSender()); + } + }); + } + + + + private void makeToast(String message) { + runOnUiThread(() -> Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show()); + } + + public class CustomAdapter extends BaseAdapter { + List<RollbackInfo> mRollbackInfos; + LayoutInflater mInflater = LayoutInflater.from(getApplicationContext()); + + CustomAdapter(List<RollbackInfo> rollbackInfos) { + mRollbackInfos = rollbackInfos; + } + + @Override + public int getCount() { + return mRollbackInfos.size(); + } + + @Override + public Object getItem(int position) { + return mRollbackInfos.get(position); + } + + @Override + public long getItemId(int position) { + return mRollbackInfos.get(position).getRollbackId(); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + if (view == null) { + view = mInflater.inflate(R.layout.listitem_rollbackinfo, null); + } + RollbackInfo rollbackInfo = mRollbackInfos.get(position); + TextView rollbackIdView = view.findViewById(R.id.rollback_id); + rollbackIdView.setText("Rollback ID " + rollbackInfo.getRollbackId()); + TextView rollbackPackagesTextView = view.findViewById(R.id.rollback_packages); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < rollbackInfo.getPackages().size(); i++) { + PackageRollbackInfo pkgInfo = rollbackInfo.getPackages().get(i); + sb.append(pkgInfo.getPackageName() + ": " + + pkgInfo.getVersionRolledBackFrom().getLongVersionCode() + " -> " + + pkgInfo.getVersionRolledBackTo().getLongVersionCode() + ","); + } + sb.deleteCharAt(sb.length() - 1); + rollbackPackagesTextView.setText(sb.toString()); + CheckBox checkbox = view.findViewById(R.id.checkbox); + checkbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + mIdsToRollback.add(rollbackInfo.getRollbackId()); + } else { + mIdsToRollback.remove(Integer.valueOf(rollbackInfo.getRollbackId())); + } + mTriggerRollbackButton.setClickable(mIdsToRollback.size() > 0); + }); + return view; + } + } +} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt b/tools/lint/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt index cc2ab19c97f1..06c098df385d 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt @@ -19,6 +19,7 @@ package com.google.android.lint.parcel import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.LintFix import com.android.tools.lint.detector.api.Location +import com.intellij.psi.PsiArrayType import com.intellij.psi.PsiCallExpression import com.intellij.psi.PsiClassType import com.intellij.psi.PsiIntersectionType @@ -26,8 +27,8 @@ import com.intellij.psi.PsiMethod import com.intellij.psi.PsiType import com.intellij.psi.PsiTypeParameter import com.intellij.psi.PsiWildcardType -import org.jetbrains.kotlin.utils.addToStdlib.cast import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement import org.jetbrains.uast.UExpression import org.jetbrains.uast.UVariable @@ -42,11 +43,11 @@ abstract class CallMigrator( ) { open fun report(context: JavaContext, call: UCallExpression, method: PsiMethod) { val location = context.getLocation(call) - val itemType = getBoundingClass(context, call, method) + val itemType = filter(getBoundingClass(context, call, method)) val fix = (itemType as? PsiClassType)?.let { type -> getParcelFix(location, this.method.name, getArgumentSuffix(type)) } - val message = "Unsafe `Parcel.${this.method.name}()` API usage" + val message = "Unsafe `${this.method.className}.${this.method.name}()` API usage" context.report(SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, call, location, message, fix) } @@ -73,14 +74,14 @@ abstract class CallMigrator( } /** - * Tries to obtain the type expected by the "receiving" end given a certain {@link UExpression}. + * Tries to obtain the type expected by the "receiving" end given a certain {@link UElement}. * * This could be an assignment, an argument passed to a method call, to a constructor call, a * type cast, etc. If no receiving end is found, the type of the UExpression itself is returned. */ - protected fun getReceivingType(expression: UExpression): PsiType? { + protected fun getReceivingType(expression: UElement): PsiType? { val parent = expression.uastParent - val type = when (parent) { + var type = when (parent) { is UCallExpression -> { val i = parent.valueArguments.indexOf(expression) val psiCall = parent.sourcePsi as? PsiCallExpression ?: return null @@ -92,10 +93,13 @@ abstract class CallMigrator( is UExpression -> parent.getExpressionType() else -> null } - return filter(type ?: expression.getExpressionType()) + if (type == null && expression is UExpression) { + type = expression.getExpressionType() + } + return type } - private fun filter(type: PsiType?): PsiType? { + protected fun filter(type: PsiType?): PsiType? { // It's important that PsiIntersectionType case is above the one that check the type in // rejects, because for intersect types, the canonicalText is one of the terms. if (type is PsiIntersectionType) { @@ -169,7 +173,7 @@ class ContainerReturnMigrator( override fun getBoundingClass( context: JavaContext, call: UCallExpression, method: PsiMethod ): PsiType? { - val type = getReceivingType(call.uastParent as UExpression) ?: return null + val type = getReceivingType(call.uastParent!!) ?: return null return getItemType(type, container) } } @@ -184,7 +188,7 @@ class ReturnMigrator( override fun getBoundingClass( context: JavaContext, call: UCallExpression, method: PsiMethod ): PsiType? { - return getReceivingType(call.uastParent as UExpression) + return getReceivingType(call.uastParent!!) } } @@ -199,11 +203,27 @@ class ReturnMigratorWithClassLoader( override fun getBoundingClass( context: JavaContext, call: UCallExpression, method: PsiMethod ): PsiType? { - return getReceivingType(call.uastParent as UExpression) + return getReceivingType(call.uastParent!!) } override fun getArgumentSuffix(type: PsiClassType): String = "${type.rawType().canonicalText}.class.getClassLoader(), " + "${type.rawType().canonicalText}.class" -}
\ No newline at end of file +} + +/** + * This class derives the type to be appended by inferring the expected array type + * for the method result. + */ +class ArrayReturnMigrator( + method: Method, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + val type = getReceivingType(call.uastParent!!) + return (type as? PsiArrayType)?.componentType + } +} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/parcel/Method.kt b/tools/lint/checks/src/main/java/com/google/android/lint/parcel/Method.kt index c032fa29f254..0826e8e74431 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/parcel/Method.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/parcel/Method.kt @@ -35,4 +35,8 @@ data class Method( val prefix = if (params.isEmpty()) "" else "${params.joinToString(", ", "<", ">")} " return "$prefix$clazz.$name(${parameters.joinToString()})" } -}
\ No newline at end of file + + val className: String by lazy { + clazz.split(".").last() + } +} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt b/tools/lint/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt index 89dbcaeac3a7..bf99e8e669fc 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt @@ -24,6 +24,7 @@ import com.intellij.psi.PsiTypeParameter import org.jetbrains.uast.UCallExpression import java.util.* +@Suppress("UnstableApiUsage") class SaferParcelChecker : Detector(), SourceCodeScanner { override fun getApplicableMethodNames(): List<String> = MIGRATORS @@ -65,9 +66,9 @@ class SaferParcelChecker : Detector(), SourceCodeScanner { @JvmField val ISSUE_UNSAFE_API_USAGE: Issue = Issue.create( id = "UnsafeParcelApi", - briefDescription = "Use of unsafe Parcel API", + briefDescription = "Use of unsafe deserialization API", explanation = """ - You are using a deprecated Parcel API that doesn't accept the expected class as\ + You are using a deprecated deserialization API that doesn't accept the expected class as\ a parameter. This means that unexpected classes could be instantiated and\ unexpected code executed. @@ -83,25 +84,52 @@ class SaferParcelChecker : Detector(), SourceCodeScanner { ) ) - private val METHOD_READ_SERIALIZABLE = Method("android.os.Parcel", "readSerializable", listOf()) - private val METHOD_READ_ARRAY_LIST = Method("android.os.Parcel", "readArrayList", listOf("java.lang.ClassLoader")) - private val METHOD_READ_LIST = Method("android.os.Parcel", "readList", listOf("java.util.List", "java.lang.ClassLoader")) - private val METHOD_READ_PARCELABLE = Method(listOf("T"), "android.os.Parcel", "readParcelable", listOf("java.lang.ClassLoader")) - private val METHOD_READ_PARCELABLE_LIST = Method(listOf("T"), "android.os.Parcel", "readParcelableList", listOf("java.util.List<T>", "java.lang.ClassLoader")) - private val METHOD_READ_SPARSE_ARRAY = Method(listOf("T"), "android.os.Parcel", "readSparseArray", listOf("java.lang.ClassLoader")) + // Parcel + private val PARCEL_METHOD_READ_SERIALIZABLE = Method("android.os.Parcel", "readSerializable", listOf()) + private val PARCEL_METHOD_READ_ARRAY_LIST = Method("android.os.Parcel", "readArrayList", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_LIST = Method("android.os.Parcel", "readList", listOf("java.util.List", "java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_PARCELABLE = Method(listOf("T"), "android.os.Parcel", "readParcelable", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_PARCELABLE_LIST = Method(listOf("T"), "android.os.Parcel", "readParcelableList", listOf("java.util.List<T>", "java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_SPARSE_ARRAY = Method(listOf("T"), "android.os.Parcel", "readSparseArray", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_ARRAY = Method("android.os.Parcel", "readArray", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_PARCELABLE_ARRAY = Method("android.os.Parcel", "readParcelableArray", listOf("java.lang.ClassLoader")) + + // Bundle + private val BUNDLE_METHOD_GET_SERIALIZABLE = Method("android.os.Bundle", "getSerializable", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_PARCELABLE = Method(listOf("T"), "android.os.Bundle", "getParcelable", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST = Method(listOf("T"), "android.os.Bundle", "getParcelableArrayList", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY = Method("android.os.Bundle", "getParcelableArray", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY = Method(listOf("T"), "android.os.Bundle", "getSparseParcelableArray", listOf("java.lang.String")) + + // Intent + private val INTENT_METHOD_GET_SERIALIZABLE_EXTRA = Method("android.content.Intent", "getSerializableExtra", listOf("java.lang.String")) + private val INTENT_METHOD_GET_PARCELABLE_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableExtra", listOf("java.lang.String")) + private val INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA = Method("android.content.Intent", "getParcelableArrayExtra", listOf("java.lang.String")) + private val INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableArrayListExtra", listOf("java.lang.String")) // TODO: Write migrators for methods below - private val METHOD_READ_ARRAY = Method("android.os.Parcel", "readArray", listOf("java.lang.ClassLoader")) - private val METHOD_READ_PARCELABLE_ARRAY = Method("android.os.Parcel", "readParcelableArray", listOf("java.lang.ClassLoader")) - private val METHOD_READ_PARCELABLE_CREATOR = Method("android.os.Parcel", "readParcelableCreator", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_PARCELABLE_CREATOR = Method("android.os.Parcel", "readParcelableCreator", listOf("java.lang.ClassLoader")) private val MIGRATORS = listOf( - ReturnMigrator(METHOD_READ_PARCELABLE, setOf("android.os.Parcelable")), - ContainerArgumentMigrator(METHOD_READ_LIST, 0, "java.util.List"), - ContainerReturnMigrator(METHOD_READ_ARRAY_LIST, "java.util.Collection"), - ContainerReturnMigrator(METHOD_READ_SPARSE_ARRAY, "android.util.SparseArray"), - ContainerArgumentMigrator(METHOD_READ_PARCELABLE_LIST, 0, "java.util.List"), - ReturnMigratorWithClassLoader(METHOD_READ_SERIALIZABLE), + ReturnMigrator(PARCEL_METHOD_READ_PARCELABLE, setOf("android.os.Parcelable")), + ContainerArgumentMigrator(PARCEL_METHOD_READ_LIST, 0, "java.util.List"), + ContainerReturnMigrator(PARCEL_METHOD_READ_ARRAY_LIST, "java.util.Collection"), + ContainerReturnMigrator(PARCEL_METHOD_READ_SPARSE_ARRAY, "android.util.SparseArray"), + ContainerArgumentMigrator(PARCEL_METHOD_READ_PARCELABLE_LIST, 0, "java.util.List"), + ReturnMigratorWithClassLoader(PARCEL_METHOD_READ_SERIALIZABLE), + ArrayReturnMigrator(PARCEL_METHOD_READ_ARRAY, setOf("java.lang.Object")), + ArrayReturnMigrator(PARCEL_METHOD_READ_PARCELABLE_ARRAY, setOf("android.os.Parcelable")), + + ReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE, setOf("android.os.Parcelable")), + ContainerReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST, "java.util.Collection", setOf("android.os.Parcelable")), + ArrayReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY, setOf("android.os.Parcelable")), + ContainerReturnMigrator(BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY, "android.util.SparseArray", setOf("android.os.Parcelable")), + ReturnMigrator(BUNDLE_METHOD_GET_SERIALIZABLE, setOf("java.io.Serializable")), + + ReturnMigrator(INTENT_METHOD_GET_PARCELABLE_EXTRA, setOf("android.os.Parcelable")), + ContainerReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA, "java.util.Collection", setOf("android.os.Parcelable")), + ArrayReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA, setOf("android.os.Parcelable")), + ReturnMigrator(INTENT_METHOD_GET_SERIALIZABLE_EXTRA, setOf("java.io.Serializable")), ) } -}
\ No newline at end of file +} diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt index 05c7850c44c2..1286e519aae6 100644 --- a/tools/lint/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt +++ b/tools/lint/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt @@ -27,20 +27,22 @@ class SaferParcelCheckerTest : LintDetectorTest() { override fun getDetector(): Detector = SaferParcelChecker() override fun getIssues(): List<Issue> = listOf( - SaferParcelChecker.ISSUE_UNSAFE_API_USAGE + SaferParcelChecker.ISSUE_UNSAFE_API_USAGE ) override fun lint(): TestLintTask = - super.lint() - .allowMissingSdk(true) - // We don't do partial analysis in the platform - .skipTestModes(TestMode.PARTIAL) + super.lint() + .allowMissingSdk(true) + // We don't do partial analysis in the platform + .skipTestModes(TestMode.PARTIAL) - fun testDetectUnsafeReadSerializable() { + /** Parcel Tests */ + + fun testParcelDetectUnsafeReadSerializable() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.os.Parcel; import java.io.Serializable; @@ -51,27 +53,27 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .expectIdenticalTestModeOutput(false) - .run() - .expect( - """ + ).indented(), + *includes + ) + .expectIdenticalTestModeOutput(false) + .run() + .expect( + """ src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readSerializable() \ API usage [UnsafeParcelApi] Serializable ans = p.readSerializable(); ~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """.addLineContinuation() - ) + ) } - fun testDoesNotDetectSafeReadSerializable() { + fun testParcelDoesNotDetectSafeReadSerializable() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.os.Parcel; import java.io.Serializable; @@ -82,18 +84,18 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect("No warnings.") + ).indented(), + *includes + ) + .run() + .expect("No warnings.") } - fun testDetectUnsafeReadArrayList() { + fun testParcelDetectUnsafeReadArrayList() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.os.Parcel; @@ -103,26 +105,26 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect( - """ + ).indented(), + *includes + ) + .run() + .expect( + """ src/test/pkg/TestClass.java:6: Warning: Unsafe Parcel.readArrayList() API \ usage [UnsafeParcelApi] ArrayList ans = p.readArrayList(null); ~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """.addLineContinuation() - ) + ) } - fun testDoesNotDetectSafeReadArrayList() { + fun testParcelDoesNotDetectSafeReadArrayList() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.content.Intent; import android.os.Parcel; @@ -133,18 +135,18 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect("No warnings.") + ).indented(), + *includes + ) + .run() + .expect("No warnings.") } - fun testDetectUnsafeReadList() { + fun testParcelDetectUnsafeReadList() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.content.Intent; import android.os.Parcel; @@ -157,26 +159,26 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect( - """ + ).indented(), + *includes + ) + .run() + .expect( + """ src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readList() API usage \ [UnsafeParcelApi] p.readList(list, null); ~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """.addLineContinuation() - ) + ) } - fun testDoesNotDetectSafeReadList() { + fun testDParceloesNotDetectSafeReadList() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.content.Intent; import android.os.Parcel; @@ -189,18 +191,18 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect("No warnings.") + ).indented(), + *includes + ) + .run() + .expect("No warnings.") } - fun testDetectUnsafeReadParcelable() { + fun testParcelDetectUnsafeReadParcelable() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.content.Intent; import android.os.Parcel; @@ -211,26 +213,26 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect( - """ + ).indented(), + *includes + ) + .run() + .expect( + """ src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelable() API \ usage [UnsafeParcelApi] Intent ans = p.readParcelable(null); ~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """.addLineContinuation() - ) + ) } - fun testDoesNotDetectSafeReadParcelable() { + fun testParcelDoesNotDetectSafeReadParcelable() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.content.Intent; import android.os.Parcel; @@ -241,18 +243,18 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect("No warnings.") + ).indented(), + *includes + ) + .run() + .expect("No warnings.") } - fun testDetectUnsafeReadParcelableList() { + fun testParcelDetectUnsafeReadParcelableList() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.content.Intent; import android.os.Parcel; @@ -265,26 +267,26 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect( - """ + ).indented(), + *includes + ) + .run() + .expect( + """ src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readParcelableList() \ API usage [UnsafeParcelApi] List<Intent> ans = p.readParcelableList(list, null); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """.addLineContinuation() - ) + ) } - fun testDoesNotDetectSafeReadParcelableList() { + fun testParcelDoesNotDetectSafeReadParcelableList() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.content.Intent; import android.os.Parcel; @@ -298,18 +300,18 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect("No warnings.") + ).indented(), + *includes + ) + .run() + .expect("No warnings.") } - fun testDetectUnsafeReadSparseArray() { + fun testParcelDetectUnsafeReadSparseArray() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.content.Intent; import android.os.Parcel; @@ -321,26 +323,26 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect( - """ + ).indented(), + *includes + ) + .run() + .expect( + """ src/test/pkg/TestClass.java:8: Warning: Unsafe Parcel.readSparseArray() API\ usage [UnsafeParcelApi] SparseArray<Intent> ans = p.readSparseArray(null); ~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """.addLineContinuation() - ) + ) } - fun testDoesNotDetectSafeReadSparseArray() { + fun testParcelDoesNotDetectSafeReadSparseArray() { lint() - .files( - java( - """ + .files( + java( + """ package test.pkg; import android.content.Intent; import android.os.Parcel; @@ -353,21 +355,383 @@ class SaferParcelCheckerTest : LintDetectorTest() { } } """ - ).indented(), - *includes - ) - .run() - .expect("No warnings.") + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadSArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readArray(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readArray() API\ + usage [UnsafeParcelApi] + Intent[] ans = p.readArray(null); + ~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readArray(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadParcelableSArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readParcelableArray(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelableArray() API\ + usage [UnsafeParcelApi] + Intent[] ans = p.readParcelableArray(null); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readParcelableArray(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + /** Bundle Tests */ + + fun testBundleDetectUnsafeGetParcelable() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent ans = b.getParcelable("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelable() API usage [UnsafeParcelApi] + Intent ans = b.getParcelable("key"); + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetParcelable() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent ans = b.getParcelable("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testBundleDetectUnsafeGetParcelableArrayList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + ArrayList<Intent> ans = b.getParcelableArrayList("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArrayList() API usage [UnsafeParcelApi] + ArrayList<Intent> ans = b.getParcelableArrayList("key"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) } + fun testBundleDoesNotDetectSafeGetParcelableArrayList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + ArrayList<Intent> ans = b.getParcelableArrayList("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testBundleDetectUnsafeGetParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent[] ans = b.getParcelableArray("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArray() API usage [UnsafeParcelApi] + Intent[] ans = b.getParcelableArray("key"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent[] ans = b.getParcelableArray("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testBundleDetectUnsafeGetSparseParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + SparseArray<Intent> ans = b.getSparseParcelableArray("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getSparseParcelableArray() API usage [UnsafeParcelApi] + SparseArray<Intent> ans = b.getSparseParcelableArray("key"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetSparseParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + SparseArray<Intent> ans = b.getSparseParcelableArray("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + /** Intent Tests */ + + fun testIntentDetectUnsafeGetParcelableExtra() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + + public class TestClass { + private TestClass(Intent i) { + Intent ans = i.getParcelableExtra("name"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Unsafe Intent.getParcelableExtra() API usage [UnsafeParcelApi] + Intent ans = i.getParcelableExtra("name"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testIntentDoesNotDetectSafeGetParcelableExtra() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + + public class TestClass { + private TestClass(Intent i) { + Intent ans = i.getParcelableExtra("name", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + /** Stubs for classes used for testing */ private val includes = - arrayOf( - manifest().minSdk("Tiramisu"), - java( - """ + arrayOf( + manifest().minSdk("Tiramisu"), + java( + """ package android.os; import java.util.ArrayList; import java.util.List; @@ -375,7 +739,7 @@ class SaferParcelCheckerTest : LintDetectorTest() { import java.util.HashMap; public final class Parcel { - // Deprecateds + // Deprecated public Object[] readArray(ClassLoader loader) { return null; } public ArrayList readArrayList(ClassLoader loader) { return null; } public HashMap readHashMap(ClassLoader loader) { return null; } @@ -402,26 +766,57 @@ class SaferParcelCheckerTest : LintDetectorTest() { public <T> SparseArray<T> readSparseArray(ClassLoader loader, Class<? extends T> clazz) { return null; } } """ - ).indented(), - java( + ).indented(), + java( + """ + package android.os; + import java.util.ArrayList; + import java.util.List; + import java.util.Map; + import java.util.HashMap; + + public final class Bundle { + // Deprecated + public <T extends Parcelable> T getParcelable(String key) { return null; } + public <T extends Parcelable> ArrayList<T> getParcelableArrayList(String key) { return null; } + public Parcelable[] getParcelableArray(String key) { return null; } + public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(String key) { return null; } + + // Replacements + public <T> T getParcelable(String key, Class<T> clazz) { return null; } + public <T> ArrayList<T> getParcelableArrayList(String key, Class<? extends T> clazz) { return null; } + public <T> T[] getParcelableArray(String key, Class<T> clazz) { return null; } + public <T> SparseArray<T> getSparseParcelableArray(String key, Class<? extends T> clazz) { return null; } + + } """ + ).indented(), + java( + """ package android.os; public interface Parcelable {} """ - ).indented(), - java( - """ + ).indented(), + java( + """ package android.content; - public class Intent implements Parcelable, Cloneable {} - """ - ).indented(), - java( + public class Intent implements Parcelable, Cloneable { + // Deprecated + public <T extends Parcelable> T getParcelableExtra(String name) { return null; } + + // Replacements + public <T> T getParcelableExtra(String name, Class<T> clazz) { return null; } + + } """ + ).indented(), + java( + """ package android.util; public class SparseArray<E> implements Cloneable {} """ - ).indented(), - ) + ).indented(), + ) // Substitutes "backslash + new line" with an empty string to imitate line continuation private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "") |