diff options
350 files changed, 14864 insertions, 4057 deletions
diff --git a/apct-tests/perftests/core/AndroidManifest.xml b/apct-tests/perftests/core/AndroidManifest.xml index bb57161b4f6b..4e24909f5d3b 100644 --- a/apct-tests/perftests/core/AndroidManifest.xml +++ b/apct-tests/perftests/core/AndroidManifest.xml @@ -16,6 +16,7 @@ <application> <uses-library android:name="android.test.runner" /> + <profileable android:shell="true" /> <activity android:name="android.perftests.utils.PerfTestActivity" android:exported="true"> <intent-filter> diff --git a/apct-tests/perftests/packagemanager/src/android/os/PackageParsingPerfTest.kt b/apct-tests/perftests/packagemanager/src/android/os/PackageParsingPerfTest.kt index 3452f587e3ff..6d1e6d0cbd73 100644 --- a/apct-tests/perftests/packagemanager/src/android/os/PackageParsingPerfTest.kt +++ b/apct-tests/perftests/packagemanager/src/android/os/PackageParsingPerfTest.kt @@ -28,16 +28,16 @@ import androidx.test.filters.LargeTest import com.android.internal.util.ConcurrentUtils import com.android.server.pm.parsing.pkg.PackageImpl import com.android.server.pm.pkg.parsing.ParsingPackageUtils +import java.io.File +import java.io.FileOutputStream +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.TimeUnit import libcore.io.IoUtils import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.junit.runners.Parameterized -import java.io.File -import java.io.FileOutputStream -import java.util.concurrent.ArrayBlockingQueue -import java.util.concurrent.TimeUnit @LargeTest @RunWith(Parameterized::class) @@ -180,8 +180,8 @@ public class PackageParsingPerfTest { protected abstract fun parseImpl(file: File): PackageType } - class ParallelParser1(private val cacher: PackageCacher1? = null) - : ParallelParser<PackageParser.Package>(cacher) { + class ParallelParser1(private val cacher: PackageCacher1? = null) : + ParallelParser<PackageParser.Package>(cacher) { val parser = PackageParser().apply { setCallback { true } } @@ -189,8 +189,8 @@ public class PackageParsingPerfTest { override fun parseImpl(file: File) = parser.parsePackage(file, 0, cacher != null) } - class ParallelParser2(cacher: PackageCacher2? = null) - : ParallelParser<PackageImpl>(cacher) { + class ParallelParser2(cacher: PackageCacher2? = null) : + ParallelParser<PackageImpl>(cacher) { val input = ThreadLocal.withInitial { // For testing, just disable enforcement to avoid hooking up to compat framework ParseTypeImpl(ParseInput.Callback { _, _, _ -> false }) @@ -218,7 +218,7 @@ public class PackageParsingPerfTest { }) override fun parseImpl(file: File) = - parser.parsePackage(input.get()!!.reset(), file, 0, null).result + parser.parsePackage(input.get()!!.reset(), file, 0).result as PackageImpl } diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index 4d5eef2f65f5..1e13dbf9b057 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1404,8 +1404,31 @@ public class JobSchedulerService extends com.android.server.SystemService } mChangedJobList.remove(cancelled); // Cancel if running. - mConcurrencyManager.stopJobOnServiceContextLocked( + final boolean wasRunning = mConcurrencyManager.stopJobOnServiceContextLocked( cancelled, reason, internalReasonCode, debugReason); + // If the job was running, the JobServiceContext should log with state FINISHED. + if (!wasRunning) { + FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED, + cancelled.getSourceUid(), null, cancelled.getBatteryName(), + FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__CANCELLED, + internalReasonCode, cancelled.getStandbyBucket(), + cancelled.getJobId(), + cancelled.hasChargingConstraint(), + cancelled.hasBatteryNotLowConstraint(), + cancelled.hasStorageNotLowConstraint(), + cancelled.hasTimingDelayConstraint(), + cancelled.hasDeadlineConstraint(), + cancelled.hasIdleConstraint(), + cancelled.hasConnectivityConstraint(), + cancelled.hasContentTriggerConstraint(), + cancelled.isRequestedExpeditedJob(), + /* isRunningAsExpeditedJob */ false, + reason, + cancelled.getJob().isPrefetch(), + cancelled.getJob().getPriority(), + cancelled.getEffectivePriority(), + cancelled.getNumFailures()); + } // If this is a replacement, bring in the new version of the job if (incomingJob != null) { if (DEBUG) Slog.i(TAG, "Tracking replacement job " + incomingJob.toShortString()); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java index dfa1442a3192..fcfb45c5f1df 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -742,6 +742,10 @@ public final class JobStore { } } catch (XmlPullParserException | IOException e) { Slog.wtf(TAG, "Error jobstore xml.", e); + } catch (Exception e) { + // Crashing at this point would result in a boot loop, so live with a general + // Exception for system stability's sake. + Slog.wtf(TAG, "Unexpected exception", e); } finally { if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once. mPersistInfo.countAllJobsLoaded = numJobs; @@ -890,6 +894,9 @@ public final class JobStore { } catch (IOException e) { Slog.d(TAG, "Error I/O Exception.", e); return null; + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Constraints contained invalid data", e); + return null; } parser.next(); // Consume </constraints> @@ -986,8 +993,14 @@ public final class JobStore { return null; } - PersistableBundle extras = PersistableBundle.restoreFromXml(parser); - jobBuilder.setExtras(extras); + final PersistableBundle extras; + try { + extras = PersistableBundle.restoreFromXml(parser); + jobBuilder.setExtras(extras); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Persisted extras contained invalid data", e); + return null; + } parser.nextTag(); // Consume </extras> final JobInfo builtJob; diff --git a/api/Android.mk b/api/Android.mk new file mode 100644 index 000000000000..ce5f995033c5 --- /dev/null +++ b/api/Android.mk @@ -0,0 +1,2 @@ +.PHONY: checkapi +checkapi: frameworks-base-api-current-compat frameworks-base-api-system-current-compat frameworks-base-api-module-lib-current-compat diff --git a/cmds/idmap2/libidmap2/Idmap.cpp b/cmds/idmap2/libidmap2/Idmap.cpp index 4efaeea74274..444f91d144a2 100644 --- a/cmds/idmap2/libidmap2/Idmap.cpp +++ b/cmds/idmap2/libidmap2/Idmap.cpp @@ -222,7 +222,7 @@ std::unique_ptr<const IdmapData> IdmapData::FromBinaryStream(std::istream& strea || !Read32(stream, &entry_count)) { return nullptr; } - target_inline_entries.emplace_back(std::make_tuple(target_entry, entry_offset, entry_count)); + target_inline_entries.emplace_back(target_entry, entry_offset, entry_count); } // Read the inline overlay resource values @@ -241,7 +241,7 @@ std::unique_ptr<const IdmapData> IdmapData::FromBinaryStream(std::istream& strea || !Read32(stream, &value.data_value)) { return nullptr; } - target_values.emplace_back(std::make_pair(config_index, value)); + target_values.emplace_back(config_index, value); } // Read the configurations diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 7fd4283ca42e..7dcfab4b5503 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -10056,7 +10056,7 @@ package android.os.storage { method @WorkerThread public long getAllocatableBytes(@NonNull java.util.UUID, @RequiresPermission int) throws java.io.IOException; method @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public int getExternalStorageMountMode(int, @NonNull String); method public static boolean hasIsolatedStorage(); - method public void updateExternalStorageFileQuotaType(@NonNull java.io.File, int) throws java.io.IOException; + method @RequiresPermission(android.Manifest.permission.MANAGE_EXTERNAL_STORAGE) public void updateExternalStorageFileQuotaType(@NonNull java.io.File, int) throws java.io.IOException; field @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE) public static final int FLAG_ALLOCATE_AGGRESSIVE = 1; // 0x1 field public static final int MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE = 4; // 0x4 field public static final int MOUNT_MODE_EXTERNAL_DEFAULT = 1; // 0x1 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 887af1f4f350..99005a463f73 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1168,9 +1168,11 @@ package android.hardware.camera2 { package android.hardware.devicestate { public final class DeviceStateManager { + method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) public void cancelBaseStateOverride(); method @RequiresPermission(value=android.Manifest.permission.CONTROL_DEVICE_STATE, conditional=true) public void cancelStateRequest(); method @NonNull public int[] getSupportedStates(); method public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.devicestate.DeviceStateManager.DeviceStateCallback); + method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) public void requestBaseStateOverride(@NonNull android.hardware.devicestate.DeviceStateRequest, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.devicestate.DeviceStateRequest.Callback); method @RequiresPermission(value=android.Manifest.permission.CONTROL_DEVICE_STATE, conditional=true) public void requestState(@NonNull android.hardware.devicestate.DeviceStateRequest, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.devicestate.DeviceStateRequest.Callback); method public void unregisterCallback(@NonNull android.hardware.devicestate.DeviceStateManager.DeviceStateCallback); field public static final int MAXIMUM_DEVICE_STATE = 255; // 0xff @@ -1221,6 +1223,7 @@ package android.hardware.display { method @Nullable public android.view.Display.Mode getGlobalUserPreferredDisplayMode(); method @NonNull public int[] getUserDisabledHdrTypes(); method public boolean isMinimalPostProcessingRequested(int); + method @RequiresPermission(android.Manifest.permission.ACCESS_SURFACE_FLINGER) public void overrideHdrTypes(int, @NonNull int[]); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setAreUserDisabledHdrTypesAllowed(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE) public void setGlobalUserPreferredDisplayMode(@NonNull android.view.Display.Mode); method @RequiresPermission(android.Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE) public void setRefreshRateSwitchingType(int); @@ -2883,7 +2886,6 @@ package android.view { ctor public SurfaceControl(@NonNull android.view.SurfaceControl, @NonNull String); method @NonNull public static android.os.IBinder getInternalDisplayToken(); method public boolean isSameSurface(@NonNull android.view.SurfaceControl); - method public static void overrideHdrTypes(@NonNull android.os.IBinder, @NonNull int[]); } public class SurfaceControlViewHost { diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 80121b729779..0e1b47f65561 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -1094,7 +1094,7 @@ class ContextImpl extends Context { && (options == null || ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) { throw new AndroidRuntimeException( - "Calling startActivity() from outside of an Activity " + "Calling startActivity() from outside of an Activity" + " context requires the FLAG_ACTIVITY_NEW_TASK flag." + " Is this really what you want?"); } @@ -1128,7 +1128,7 @@ class ContextImpl extends Context { public int startActivitiesAsUser(Intent[] intents, Bundle options, UserHandle userHandle) { if ((intents[0].getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) { throw new AndroidRuntimeException( - "Calling startActivities() from outside of an Activity " + "Calling startActivities() from outside of an Activity" + " context requires the FLAG_ACTIVITY_NEW_TASK flag on first Intent." + " Is this really what you want?"); } @@ -1142,7 +1142,7 @@ class ContextImpl extends Context { warnIfCallingFromSystemProcess(); if ((intents[0].getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) { throw new AndroidRuntimeException( - "Calling startActivities() from outside of an Activity " + "Calling startActivities() from outside of an Activity" + " context requires the FLAG_ACTIVITY_NEW_TASK flag on first Intent." + " Is this really what you want?"); } diff --git a/core/java/android/app/UidObserver.java b/core/java/android/app/UidObserver.java new file mode 100644 index 000000000000..9e928073ac5c --- /dev/null +++ b/core/java/android/app/UidObserver.java @@ -0,0 +1,50 @@ +/* + * 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.app; + +/** + * Default implementation of {@link IUidObserver} for which all methods are + * no-op. Subclasses can override the select methods they're interested in + * handling. + * + * @hide + */ +public class UidObserver extends IUidObserver.Stub { + @Override + public void onUidActive(int uid) { + } + + @Override + public void onUidCachedChanged(int uid, boolean cached) { + } + + @Override + public void onUidGone(int uid, boolean disabled) { + } + + @Override + public void onUidIdle(int uid, boolean disabled) { + } + + @Override + public void onUidProcAdjChanged(int uid) { + } + + @Override + public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) { + } +} diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 10d6f2d6d04b..64fed63c7159 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -78,7 +78,6 @@ public class ApkLiteParseUtils { public static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml"; private static final int PARSE_IS_SYSTEM_DIR = 1 << 4; private static final int PARSE_COLLECT_CERTIFICATES = 1 << 5; - private static final int PARSE_FRAMEWORK_RES_SPLITS = 1 << 8; private static final String TAG_APPLICATION = "application"; private static final String TAG_PACKAGE_VERIFIER = "package-verifier"; private static final String TAG_PROFILEABLE = "profileable"; @@ -103,7 +102,7 @@ public class ApkLiteParseUtils { public static ParseResult<PackageLite> parsePackageLite(ParseInput input, File packageFile, int flags) { if (packageFile.isDirectory()) { - return parseClusterPackageLite(input, packageFile, /* frameworkSplits= */ null, flags); + return parseClusterPackageLite(input, packageFile, flags); } else { return parseMonolithicPackageLite(input, packageFile, flags); } @@ -137,38 +136,19 @@ public class ApkLiteParseUtils { /** * Parse lightweight details about a directory of APKs. * - * @param packageDirOrApk is the folder that contains split apks for a regular app or the - * framework-res.apk for framwork-res splits (in which case the - * splits come in the <code>frameworkSplits</code> parameter) + * @param packageDir is the folder that contains split apks for a regular app */ public static ParseResult<PackageLite> parseClusterPackageLite(ParseInput input, - File packageDirOrApk, List<File> frameworkSplits, int flags) { + File packageDir, int flags) { final File[] files; - final boolean parsingFrameworkSplits = (flags & PARSE_FRAMEWORK_RES_SPLITS) != 0; - if (parsingFrameworkSplits) { - if (ArrayUtils.isEmpty(frameworkSplits)) { - return input.error(PackageManager.INSTALL_PARSE_FAILED_NOT_APK, - "No packages found in split"); - } - files = frameworkSplits.toArray(new File[frameworkSplits.size() + 1]); - // we also want to process the base apk so add it to the array - files[files.length - 1] = packageDirOrApk; - } else { - files = packageDirOrApk.listFiles(); - if (ArrayUtils.isEmpty(files)) { - return input.error(PackageManager.INSTALL_PARSE_FAILED_NOT_APK, - "No packages found in split"); - } - // Apk directory is directly nested under the current directory - if (files.length == 1 && files[0].isDirectory()) { - return parseClusterPackageLite(input, files[0], frameworkSplits, flags); - } + files = packageDir.listFiles(); + if (ArrayUtils.isEmpty(files)) { + return input.error(PackageManager.INSTALL_PARSE_FAILED_NOT_APK, + "No packages found in split"); } - - if (parsingFrameworkSplits) { - // disable the flag for checking the certificates of the splits. We know they - // won't match, but we rely on the mainline apex to be safe if it was installed - flags = flags & ~PARSE_COLLECT_CERTIFICATES; + // Apk directory is directly nested under the current directory + if (files.length == 1 && files[0].isDirectory()) { + return parseClusterPackageLite(input, files[0], flags); } String packageName = null; @@ -186,10 +166,6 @@ public class ApkLiteParseUtils { } final ApkLite lite = result.getResult(); - if (parsingFrameworkSplits && file == files[files.length - 1]) { - baseApk = lite; - break; - } // Assert that all package names and version codes are // consistent with the first one we encounter. if (packageName == null) { @@ -201,8 +177,7 @@ public class ApkLiteParseUtils { "Inconsistent package " + lite.getPackageName() + " in " + file + "; expected " + packageName); } - // we allow version codes that do not match for framework splits - if (!parsingFrameworkSplits && versionCode != lite.getVersionCode()) { + if (versionCode != lite.getVersionCode()) { return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST, "Inconsistent version " + lite.getVersionCode() + " in " + file + "; expected " + versionCode); @@ -217,15 +192,11 @@ public class ApkLiteParseUtils { } } } - // baseApk is set in the last iteration of the for each loop when we are parsing - // frameworkRes splits or needs to be done now otherwise - if (!parsingFrameworkSplits) { - baseApk = apks.remove(null); - } + baseApk = apks.remove(null); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } - return composePackageLiteFromApks(input, packageDirOrApk, baseApk, apks); + return composePackageLiteFromApks(input, packageDir, baseApk, apks); } /** diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java index 30aa4db938da..bdd45e6df448 100644 --- a/core/java/android/hardware/devicestate/DeviceStateManager.java +++ b/core/java/android/hardware/devicestate/DeviceStateManager.java @@ -16,6 +16,7 @@ package android.hardware.devicestate; +import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; @@ -115,6 +116,52 @@ public final class DeviceStateManager { } /** + * Submits a {@link DeviceStateRequest request} to override the base state of the device. This + * should only be used for testing, where you want to simulate the physical change to the + * device state. + * <p> + * By default, the request is kept active until one of the following occurs: + * <ul> + * <li>The physical state of the device changes</li> + * <li>The system deems the request can no longer be honored, for example if the requested + * state becomes unsupported. + * <li>A call to {@link #cancelBaseStateOverride}. + * <li>Another processes submits a request succeeding this request in which case the request + * will be canceled. + * </ul> + * + * Submitting a base state override request may not cause any change in the presentation + * of the system if there is an emulated request made through {@link #requestState}, as the + * emulated override requests take priority. + * + * @throws IllegalArgumentException if the requested state is unsupported. + * @throws SecurityException if the caller does not hold the + * {@link android.Manifest.permission#CONTROL_DEVICE_STATE} permission. + * + * @see DeviceStateRequest + */ + @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) + public void requestBaseStateOverride(@NonNull DeviceStateRequest request, + @Nullable @CallbackExecutor Executor executor, + @Nullable DeviceStateRequest.Callback callback) { + mGlobal.requestBaseStateOverride(request, executor, callback); + } + + /** + * Cancels the active {@link DeviceStateRequest} previously submitted with a call to + * {@link #requestBaseStateOverride(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}. + * <p> + * This method is noop if there is no base state request currently active. + * + * @throws SecurityException if the caller does not hold the + * {@link android.Manifest.permission#CONTROL_DEVICE_STATE} permission. + */ + @RequiresPermission(Manifest.permission.CONTROL_DEVICE_STATE) + public void cancelBaseStateOverride() { + mGlobal.cancelBaseStateOverride(); + } + + /** * Registers a callback to receive notifications about changes in device state. * * @param executor the executor to process notifications. diff --git a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java index aba538f51043..738045dafdf1 100644 --- a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java +++ b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java @@ -18,6 +18,7 @@ package android.hardware.devicestate; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.content.Context; import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback; import android.os.Binder; @@ -81,6 +82,7 @@ public final class DeviceStateManagerGlobal { @VisibleForTesting public DeviceStateManagerGlobal(@NonNull IDeviceStateManager deviceStateManager) { mDeviceStateManager = deviceStateManager; + registerCallbackIfNeededLocked(); } /** @@ -116,27 +118,22 @@ public final class DeviceStateManagerGlobal { * DeviceStateRequest.Callback) * @see DeviceStateRequest */ + @RequiresPermission(value = android.Manifest.permission.CONTROL_DEVICE_STATE, + conditional = true) public void requestState(@NonNull DeviceStateRequest request, @Nullable Executor executor, @Nullable DeviceStateRequest.Callback callback) { - if (callback == null && executor != null) { - throw new IllegalArgumentException("Callback must be supplied with executor."); - } else if (executor == null && callback != null) { - throw new IllegalArgumentException("Executor must be supplied with callback."); - } - + DeviceStateRequestWrapper requestWrapper = new DeviceStateRequestWrapper(request, callback, + executor); synchronized (mLock) { - registerCallbackIfNeededLocked(); - if (findRequestTokenLocked(request) != null) { // This request has already been submitted. return; } - // Add the request wrapper to the mRequests array before requesting the state as the // callback could be triggered immediately if the mDeviceStateManager IBinder is in the // same process as this instance. IBinder token = new Binder(); - mRequests.put(token, new DeviceStateRequestWrapper(request, callback, executor)); + mRequests.put(token, requestWrapper); try { mDeviceStateManager.requestState(token, request.getState(), request.getFlags()); @@ -153,10 +150,10 @@ public final class DeviceStateManagerGlobal { * * @see DeviceStateManager#cancelStateRequest */ + @RequiresPermission(value = android.Manifest.permission.CONTROL_DEVICE_STATE, + conditional = true) public void cancelStateRequest() { synchronized (mLock) { - registerCallbackIfNeededLocked(); - try { mDeviceStateManager.cancelStateRequest(); } catch (RemoteException ex) { @@ -166,6 +163,56 @@ public final class DeviceStateManagerGlobal { } /** + * Submits a {@link DeviceStateRequest request} to modify the base state of the device. + * + * @see DeviceStateManager#requestBaseStateOverride(DeviceStateRequest, Executor, + * DeviceStateRequest.Callback) + * @see DeviceStateRequest + */ + @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) + public void requestBaseStateOverride(@NonNull DeviceStateRequest request, + @Nullable Executor executor, @Nullable DeviceStateRequest.Callback callback) { + DeviceStateRequestWrapper requestWrapper = new DeviceStateRequestWrapper(request, callback, + executor); + synchronized (mLock) { + if (findRequestTokenLocked(request) != null) { + // This request has already been submitted. + return; + } + // Add the request wrapper to the mRequests array before requesting the state as the + // callback could be triggered immediately if the mDeviceStateManager IBinder is in the + // same process as this instance. + IBinder token = new Binder(); + mRequests.put(token, requestWrapper); + + try { + mDeviceStateManager.requestBaseStateOverride(token, request.getState(), + request.getFlags()); + } catch (RemoteException ex) { + mRequests.remove(token); + throw ex.rethrowFromSystemServer(); + } + } + } + + /** + * Cancels a {@link DeviceStateRequest request} previously submitted with a call to + * {@link #requestBaseStateOverride(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}. + * + * @see DeviceStateManager#cancelBaseStateOverride + */ + @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) + public void cancelBaseStateOverride() { + synchronized (mLock) { + try { + mDeviceStateManager.cancelBaseStateOverride(); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + } + + /** * Registers a callback to receive notifications about changes in device state. * * @see DeviceStateManager#registerCallback(Executor, DeviceStateCallback) @@ -179,9 +226,6 @@ public final class DeviceStateManagerGlobal { // This callback is already registered. return; } - - registerCallbackIfNeededLocked(); - // Add the callback wrapper to the mCallbacks array after registering the callback as // the callback could be triggered immediately if the mDeviceStateManager IBinder is in // the same process as this instance. @@ -357,6 +401,8 @@ public final class DeviceStateManagerGlobal { DeviceStateRequestWrapper(@NonNull DeviceStateRequest request, @Nullable DeviceStateRequest.Callback callback, @Nullable Executor executor) { + validateRequestWrapperParameters(callback, executor); + mRequest = request; mCallback = callback; mExecutor = executor; @@ -377,5 +423,14 @@ public final class DeviceStateManagerGlobal { mExecutor.execute(() -> mCallback.onRequestCanceled(mRequest)); } + + private void validateRequestWrapperParameters( + @Nullable DeviceStateRequest.Callback callback, @Nullable Executor executor) { + if (callback == null && executor != null) { + throw new IllegalArgumentException("Callback must be supplied with executor."); + } else if (executor == null && callback != null) { + throw new IllegalArgumentException("Executor must be supplied with callback."); + } + } } } diff --git a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl index e450e42497a0..7175eae58a26 100644 --- a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl +++ b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl @@ -41,6 +41,10 @@ interface IDeviceStateManager { * previously registered with {@link #registerCallback(IDeviceStateManagerCallback)} before a * call to this method. * + * Requesting a state does not cancel a base state override made through + * {@link #requestBaseStateOverride}, but will still attempt to put the device into the + * supplied {@code state}. + * * @param token the request token provided * @param state the state of device the request is asking for * @param flags any flags that correspond to the request @@ -50,14 +54,53 @@ interface IDeviceStateManager { * @throws IllegalStateException if the supplied {@code token} has already been registered. * @throws IllegalArgumentException if the supplied {@code state} is not supported. */ + @JavaPassthrough(annotation= + "@android.annotation.RequiresPermission(value=android.Manifest.permission.CONTROL_DEVICE_STATE, conditional=true)") void requestState(IBinder token, int state, int flags); /** * Cancels the active request previously submitted with a call to - * {@link #requestState(IBinder, int, int)}. + * {@link #requestState(IBinder, int, int)}. Will have no effect on any base state override that + * was previously requested with {@link #requestBaseStateOverride}. * * @throws IllegalStateException if a callback has not yet been registered for the calling * process. */ + @JavaPassthrough(annotation= + "@android.annotation.RequiresPermission(value=android.Manifest.permission.CONTROL_DEVICE_STATE, conditional=true)") void cancelStateRequest(); + + /** + * Requests that the device's base state be overridden to the supplied {@code state}. A callback + * <b>MUST</b> have been previously registered with + * {@link #registerCallback(IDeviceStateManagerCallback)} before a call to this method. + * + * This method should only be used for testing, when you want to simulate the device physically + * changing states. If you are looking to change device state for a feature, where the system + * should still be aware that the physical state is different than the emulated state, use + * {@link #requestState}. + * + * @param token the request token provided + * @param state the state of device the request is asking for + * @param flags any flags that correspond to the request + * + * @throws IllegalStateException if a callback has not yet been registered for the calling + * process. + * @throws IllegalStateException if the supplied {@code token} has already been registered. + * @throws IllegalArgumentException if the supplied {@code state} is not supported. + */ + @JavaPassthrough(annotation= + "@android.annotation.RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)") + void requestBaseStateOverride(IBinder token, int state, int flags); + + /** + * Cancels the active base state request previously submitted with a call to + * {@link #overrideBaseState(IBinder, int, int)}. + * + * @throws IllegalStateException if a callback has not yet been registered for the calling + * process. + */ + @JavaPassthrough(annotation= + "@android.annotation.RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)") + void cancelBaseStateOverride(); } diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 8bc11cbc61de..dfb42365357d 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -855,6 +855,16 @@ public final class DisplayManager { return mGlobal.getUserDisabledHdrTypes(); } + /** + * Overrides HDR modes for a display device. + * + * @hide + */ + @RequiresPermission(Manifest.permission.ACCESS_SURFACE_FLINGER) + @TestApi + public void overrideHdrTypes(int displayId, @NonNull int[] modes) { + mGlobal.overrideHdrTypes(displayId, modes); + } /** * Creates a virtual display. diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index e2f8592ad329..9294dea50b0d 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -20,9 +20,11 @@ package android.hardware.display; import static android.hardware.display.DisplayManager.EventsMask; import static android.view.Display.HdrCapabilities.HdrType; +import android.Manifest; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.app.PropertyInvalidatedCache; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; @@ -577,6 +579,20 @@ public final class DisplayManagerGlobal { } } + /** + * Overrides HDR modes for a display device. + * + */ + @RequiresPermission(Manifest.permission.ACCESS_SURFACE_FLINGER) + public void overrideHdrTypes(int displayId, int[] modes) { + try { + mDm.overrideHdrTypes(displayId, modes); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + public void requestColorMode(int displayId, int colorMode) { try { mDm.requestColorMode(displayId, colorMode); diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index d57a272217e2..00bccc686919 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -28,6 +28,7 @@ import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; +import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.window.DisplayWindowPolicyController; import android.window.ScreenCapture; @@ -398,6 +399,11 @@ public abstract class DisplayManagerInternal { public abstract DisplayWindowPolicyController getDisplayWindowPolicyController(int displayId); /** + * Get DisplayPrimaries from SF for a particular display. + */ + public abstract SurfaceControl.DisplayPrimaries getDisplayNativePrimaries(int displayId); + + /** * Describes the requested power state of the display. * * This object is intended to describe the general characteristics of the diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl index fa3c4506d6e9..b166e215075a 100644 --- a/core/java/android/hardware/display/IDisplayManager.aidl +++ b/core/java/android/hardware/display/IDisplayManager.aidl @@ -83,6 +83,9 @@ interface IDisplayManager { // No permissions required. int[] getUserDisabledHdrTypes(); + // Requires ACCESS_SURFACE_FLINGER permission. + void overrideHdrTypes(int displayId, in int[] modes); + // Requires CONFIGURE_DISPLAY_COLOR_MODE void requestColorMode(int displayId, int colorMode); diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java index aa5480abafb4..4a18333aee9f 100644 --- a/core/java/android/hardware/radio/TunerAdapter.java +++ b/core/java/android/hardware/radio/TunerAdapter.java @@ -16,12 +16,13 @@ package android.hardware.radio; -import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Bitmap; import android.os.RemoteException; import android.util.Log; +import com.android.internal.annotations.GuardedBy; + import java.util.List; import java.util.Map; import java.util.Objects; @@ -29,28 +30,36 @@ import java.util.Objects; /** * Implements the RadioTuner interface by forwarding calls to radio service. */ -class TunerAdapter extends RadioTuner { +final class TunerAdapter extends RadioTuner { private static final String TAG = "BroadcastRadio.TunerAdapter"; - @NonNull private final ITuner mTuner; - @NonNull private final TunerCallbackAdapter mCallback; - private boolean mIsClosed = false; + private final ITuner mTuner; + private final TunerCallbackAdapter mCallback; + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private boolean mIsClosed; - private @RadioManager.Band int mBand; + @GuardedBy("mLock") + @RadioManager.Band + private int mBand; + @GuardedBy("mLock") private ProgramList mLegacyListProxy; + + @GuardedBy("mLock") private Map<String, String> mLegacyListFilter; - TunerAdapter(@NonNull ITuner tuner, @NonNull TunerCallbackAdapter callback, + TunerAdapter(ITuner tuner, TunerCallbackAdapter callback, @RadioManager.Band int band) { - mTuner = Objects.requireNonNull(tuner); - mCallback = Objects.requireNonNull(callback); + mTuner = Objects.requireNonNull(tuner, "Tuner cannot be null"); + mCallback = Objects.requireNonNull(callback, "Callback cannot be null"); mBand = band; } @Override public void close() { - synchronized (mTuner) { + synchronized (mLock) { if (mIsClosed) { Log.v(TAG, "Tuner is already closed"); return; @@ -60,8 +69,8 @@ class TunerAdapter extends RadioTuner { mLegacyListProxy.close(); mLegacyListProxy = null; } - mCallback.close(); } + mCallback.close(); try { mTuner.close(); } catch (RemoteException e) { @@ -71,16 +80,20 @@ class TunerAdapter extends RadioTuner { @Override public int setConfiguration(RadioManager.BandConfig config) { - if (config == null) return RadioManager.STATUS_BAD_VALUE; + if (config == null) { + return RadioManager.STATUS_BAD_VALUE; + } try { mTuner.setConfiguration(config); - mBand = config.getType(); + synchronized (mLock) { + mBand = config.getType(); + } return RadioManager.STATUS_OK; } catch (IllegalArgumentException e) { Log.e(TAG, "Can't set configuration", e); return RadioManager.STATUS_BAD_VALUE; } catch (RemoteException e) { - Log.e(TAG, "service died", e); + Log.e(TAG, "Service died", e); return RadioManager.STATUS_DEAD_OBJECT; } } @@ -94,7 +107,7 @@ class TunerAdapter extends RadioTuner { config[0] = mTuner.getConfiguration(); return RadioManager.STATUS_OK; } catch (RemoteException e) { - Log.e(TAG, "service died", e); + Log.e(TAG, "Service died", e); return RadioManager.STATUS_DEAD_OBJECT; } } @@ -107,7 +120,7 @@ class TunerAdapter extends RadioTuner { Log.e(TAG, "Can't set muted", e); return RadioManager.STATUS_ERROR; } catch (RemoteException e) { - Log.e(TAG, "service died", e); + Log.e(TAG, "Service died", e); return RadioManager.STATUS_DEAD_OBJECT; } return RadioManager.STATUS_OK; @@ -118,7 +131,7 @@ class TunerAdapter extends RadioTuner { try { return mTuner.isMuted(); } catch (RemoteException e) { - Log.e(TAG, "service died", e); + Log.e(TAG, "Service died", e); return true; } } @@ -126,12 +139,13 @@ class TunerAdapter extends RadioTuner { @Override public int step(int direction, boolean skipSubChannel) { try { - mTuner.step(direction == RadioTuner.DIRECTION_DOWN, skipSubChannel); + mTuner.step(/* directionDown= */ direction == RadioTuner.DIRECTION_DOWN, + skipSubChannel); } catch (IllegalStateException e) { Log.e(TAG, "Can't step", e); return RadioManager.STATUS_INVALID_OPERATION; } catch (RemoteException e) { - Log.e(TAG, "service died", e); + Log.e(TAG, "Service died", e); return RadioManager.STATUS_DEAD_OBJECT; } return RadioManager.STATUS_OK; @@ -140,12 +154,13 @@ class TunerAdapter extends RadioTuner { @Override public int scan(int direction, boolean skipSubChannel) { try { - mTuner.scan(direction == RadioTuner.DIRECTION_DOWN, skipSubChannel); + mTuner.scan(/* directionDown= */ direction == RadioTuner.DIRECTION_DOWN, + skipSubChannel); } catch (IllegalStateException e) { Log.e(TAG, "Can't scan", e); return RadioManager.STATUS_INVALID_OPERATION; } catch (RemoteException e) { - Log.e(TAG, "service died", e); + Log.e(TAG, "Service died", e); return RadioManager.STATUS_DEAD_OBJECT; } return RadioManager.STATUS_OK; @@ -154,7 +169,11 @@ class TunerAdapter extends RadioTuner { @Override public int tune(int channel, int subChannel) { try { - mTuner.tune(ProgramSelector.createAmFmSelector(mBand, channel, subChannel)); + int band; + synchronized (mLock) { + band = mBand; + } + mTuner.tune(ProgramSelector.createAmFmSelector(band, channel, subChannel)); } catch (IllegalStateException e) { Log.e(TAG, "Can't tune", e); return RadioManager.STATUS_INVALID_OPERATION; @@ -162,18 +181,18 @@ class TunerAdapter extends RadioTuner { Log.e(TAG, "Can't tune", e); return RadioManager.STATUS_BAD_VALUE; } catch (RemoteException e) { - Log.e(TAG, "service died", e); + Log.e(TAG, "Service died", e); return RadioManager.STATUS_DEAD_OBJECT; } return RadioManager.STATUS_OK; } @Override - public void tune(@NonNull ProgramSelector selector) { + public void tune(ProgramSelector selector) { try { mTuner.tune(selector); } catch (RemoteException e) { - throw new RuntimeException("service died", e); + throw new RuntimeException("Service died", e); } } @@ -185,7 +204,7 @@ class TunerAdapter extends RadioTuner { Log.e(TAG, "Can't cancel", e); return RadioManager.STATUS_INVALID_OPERATION; } catch (RemoteException e) { - Log.e(TAG, "service died", e); + Log.e(TAG, "Service died", e); return RadioManager.STATUS_DEAD_OBJECT; } return RadioManager.STATUS_OK; @@ -196,7 +215,7 @@ class TunerAdapter extends RadioTuner { try { mTuner.cancelAnnouncement(); } catch (RemoteException e) { - throw new RuntimeException("service died", e); + throw new RuntimeException("Service died", e); } } @@ -217,11 +236,12 @@ class TunerAdapter extends RadioTuner { } @Override - public @Nullable Bitmap getMetadataImage(int id) { + @Nullable + public Bitmap getMetadataImage(int id) { try { return mTuner.getImage(id); } catch (RemoteException e) { - throw new RuntimeException("service died", e); + throw new RuntimeException("Service died", e); } } @@ -230,66 +250,72 @@ class TunerAdapter extends RadioTuner { try { return mTuner.startBackgroundScan(); } catch (RemoteException e) { - throw new RuntimeException("service died", e); + throw new RuntimeException("Service died", e); } } @Override - public @NonNull List<RadioManager.ProgramInfo> + public List<RadioManager.ProgramInfo> getProgramList(@Nullable Map<String, String> vendorFilter) { - synchronized (mTuner) { + synchronized (mLock) { if (mLegacyListProxy == null || !Objects.equals(mLegacyListFilter, vendorFilter)) { Log.i(TAG, "Program list filter has changed, requesting new list"); mLegacyListProxy = new ProgramList(); mLegacyListFilter = vendorFilter; - mCallback.clearLastCompleteList(); - mCallback.setProgramListObserver(mLegacyListProxy, () -> { }); - try { - mTuner.startProgramListUpdates(new ProgramList.Filter(vendorFilter)); - } catch (RemoteException ex) { - throw new RuntimeException("service died", ex); - } + mCallback.setProgramListObserver(mLegacyListProxy, () -> { + Log.i(TAG, "Empty closeListener in programListObserver"); + }); } + } + try { + mTuner.startProgramListUpdates(new ProgramList.Filter(vendorFilter)); + } catch (RemoteException ex) { + throw new RuntimeException("Service died", ex); + } - List<RadioManager.ProgramInfo> list = mCallback.getLastCompleteList(); - if (list == null) throw new IllegalStateException("Program list is not ready yet"); - return list; + List<RadioManager.ProgramInfo> list = mCallback.getLastCompleteList(); + if (list == null) { + throw new IllegalStateException("Program list is not ready yet"); } + return list; } @Override - public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) { - synchronized (mTuner) { + @Nullable + public ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) { + synchronized (mLock) { if (mLegacyListProxy != null) { mLegacyListProxy.close(); mLegacyListProxy = null; } mLegacyListFilter = null; - - ProgramList list = new ProgramList(); - mCallback.setProgramListObserver(list, () -> { - try { - mTuner.stopProgramListUpdates(); - } catch (IllegalStateException ex) { - // it's fine to not stop updates if tuner is already closed - } catch (RemoteException ex) { - Log.e(TAG, "Couldn't stop program list updates", ex); - } - }); - + } + ProgramList list = new ProgramList(); + mCallback.setProgramListObserver(list, () -> { try { - mTuner.startProgramListUpdates(filter); - } catch (UnsupportedOperationException ex) { - Log.i(TAG, "Program list is not supported with this hardware"); - return null; + mTuner.stopProgramListUpdates(); + } catch (IllegalStateException ex) { + // it's fine to not stop updates if tuner is already closed + Log.e(TAG, "Tuner may already be closed", ex); } catch (RemoteException ex) { - mCallback.setProgramListObserver(null, () -> { }); - throw new RuntimeException("service died", ex); + Log.e(TAG, "Couldn't stop program list updates", ex); } + }); - return list; + try { + mTuner.startProgramListUpdates(filter); + } catch (UnsupportedOperationException ex) { + Log.i(TAG, "Program list is not supported with this hardware"); + return null; + } catch (RemoteException ex) { + mCallback.setProgramListObserver(null, () -> { + Log.i(TAG, "Empty closeListener in programListObserver"); + }); + throw new RuntimeException("Service died", ex); } + + return list; } @Override @@ -315,7 +341,7 @@ class TunerAdapter extends RadioTuner { try { return mTuner.isConfigFlagSupported(flag); } catch (RemoteException e) { - throw new RuntimeException("service died", e); + throw new RuntimeException("Service died", e); } } @@ -324,7 +350,7 @@ class TunerAdapter extends RadioTuner { try { return mTuner.isConfigFlagSet(flag); } catch (RemoteException e) { - throw new RuntimeException("service died", e); + throw new RuntimeException("Service died", e); } } @@ -333,25 +359,26 @@ class TunerAdapter extends RadioTuner { try { mTuner.setConfigFlag(flag, value); } catch (RemoteException e) { - throw new RuntimeException("service died", e); + throw new RuntimeException("Service died", e); } } @Override - public @NonNull Map<String, String> setParameters(@NonNull Map<String, String> parameters) { + public Map<String, String> setParameters(Map<String, String> parameters) { try { - return mTuner.setParameters(Objects.requireNonNull(parameters)); + return mTuner.setParameters(Objects.requireNonNull(parameters, + "Parameters cannot be null")); } catch (RemoteException e) { - throw new RuntimeException("service died", e); + throw new RuntimeException("Service died", e); } } @Override - public @NonNull Map<String, String> getParameters(@NonNull List<String> keys) { + public Map<String, String> getParameters(List<String> keys) { try { - return mTuner.getParameters(Objects.requireNonNull(keys)); + return mTuner.getParameters(Objects.requireNonNull(keys, "Keys cannot be null")); } catch (RemoteException e) { - throw new RuntimeException("service died", e); + throw new RuntimeException("Service died", e); } } diff --git a/core/java/android/hardware/radio/TunerCallbackAdapter.java b/core/java/android/hardware/radio/TunerCallbackAdapter.java index e3f7001d8740..b9782a87735d 100644 --- a/core/java/android/hardware/radio/TunerCallbackAdapter.java +++ b/core/java/android/hardware/radio/TunerCallbackAdapter.java @@ -16,12 +16,13 @@ package android.hardware.radio; -import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Handler; import android.os.Looper; import android.util.Log; +import com.android.internal.annotations.GuardedBy; + import java.util.List; import java.util.Map; import java.util.Objects; @@ -29,23 +30,31 @@ import java.util.Objects; /** * Implements the ITunerCallback interface by forwarding calls to RadioTuner.Callback. */ -class TunerCallbackAdapter extends ITunerCallback.Stub { +final class TunerCallbackAdapter extends ITunerCallback.Stub { private static final String TAG = "BroadcastRadio.TunerCallbackAdapter"; private final Object mLock = new Object(); - @NonNull private final RadioTuner.Callback mCallback; - @NonNull private final Handler mHandler; + private final RadioTuner.Callback mCallback; + private final Handler mHandler; + @GuardedBy("mLock") @Nullable ProgramList mProgramList; // cache for deprecated methods + @GuardedBy("mLock") boolean mIsAntennaConnected = true; + + @GuardedBy("mLock") @Nullable List<RadioManager.ProgramInfo> mLastCompleteList; - private boolean mDelayedCompleteCallback = false; + + @GuardedBy("mLock") + private boolean mDelayedCompleteCallback; + + @GuardedBy("mLock") @Nullable RadioManager.ProgramInfo mCurrentProgramInfo; - TunerCallbackAdapter(@NonNull RadioTuner.Callback callback, @Nullable Handler handler) { - mCallback = callback; + TunerCallbackAdapter(RadioTuner.Callback callback, @Nullable Handler handler) { + mCallback = Objects.requireNonNull(callback, "Callback cannot be null"); if (handler == null) { mHandler = new Handler(Looper.getMainLooper()); } else { @@ -55,31 +64,39 @@ class TunerCallbackAdapter extends ITunerCallback.Stub { void close() { synchronized (mLock) { - if (mProgramList != null) mProgramList.close(); + if (mProgramList != null) { + mProgramList.close(); + } } } void setProgramListObserver(@Nullable ProgramList programList, - @NonNull ProgramList.OnCloseListener closeListener) { - Objects.requireNonNull(closeListener); + ProgramList.OnCloseListener closeListener) { + Objects.requireNonNull(closeListener, "CloseListener cannot be null"); synchronized (mLock) { if (mProgramList != null) { Log.w(TAG, "Previous program list observer wasn't properly closed, closing it..."); mProgramList.close(); } mProgramList = programList; - if (programList == null) return; + if (programList == null) { + return; + } programList.setOnCloseListener(() -> { synchronized (mLock) { - if (mProgramList != programList) return; + if (mProgramList != programList) { + return; + } mProgramList = null; mLastCompleteList = null; - closeListener.onClose(); } + closeListener.onClose(); }); programList.addOnCompleteListener(() -> { synchronized (mLock) { - if (mProgramList != programList) return; + if (mProgramList != programList) { + return; + } mLastCompleteList = programList.toList(); if (mDelayedCompleteCallback) { Log.d(TAG, "Sending delayed onBackgroundScanComplete callback"); @@ -109,7 +126,11 @@ class TunerCallbackAdapter extends ITunerCallback.Stub { } boolean isAntennaConnected() { - return mIsAntennaConnected; + boolean isConnected; + synchronized (mLock) { + isConnected = mIsAntennaConnected; + } + return isConnected; } @Override @@ -177,7 +198,9 @@ class TunerCallbackAdapter extends ITunerCallback.Stub { @Override public void onAntennaState(boolean connected) { - mIsAntennaConnected = connected; + synchronized (mLock) { + mIsAntennaConnected = connected; + } mHandler.post(() -> mCallback.onAntennaState(connected)); } @@ -186,6 +209,7 @@ class TunerCallbackAdapter extends ITunerCallback.Stub { mHandler.post(() -> mCallback.onBackgroundScanAvailabilityChange(isAvailable)); } + @GuardedBy("mLock") private void sendBackgroundScanCompleteLocked() { mDelayedCompleteCallback = false; mHandler.post(() -> mCallback.onBackgroundScanComplete()); @@ -213,8 +237,10 @@ class TunerCallbackAdapter extends ITunerCallback.Stub { public void onProgramListUpdated(ProgramList.Chunk chunk) { mHandler.post(() -> { synchronized (mLock) { - if (mProgramList == null) return; - mProgramList.apply(Objects.requireNonNull(chunk)); + if (mProgramList == null) { + return; + } + mProgramList.apply(Objects.requireNonNull(chunk, "Chunk cannot be null")); } }); } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 7436601f5352..9ed55ffef11f 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -967,7 +967,7 @@ public class InputMethodService extends AbstractInputMethodService { Log.d(TAG, "Input should have started before starting Stylus handwriting."); return; } - maybeCreateInkWindow(); + maybeCreateAndInitInkWindow(); if (!mOnPreparedStylusHwCalled) { // prepare hasn't been called by Stylus HOVER. onPrepareStylusHandwriting(); @@ -1027,8 +1027,7 @@ public class InputMethodService extends AbstractInputMethodService { */ @Override public void initInkWindow() { - maybeCreateInkWindow(); - mInkWindow.initOnly(); + maybeCreateAndInitInkWindow(); onPrepareStylusHandwriting(); mOnPreparedStylusHwCalled = true; } @@ -1036,11 +1035,13 @@ public class InputMethodService extends AbstractInputMethodService { /** * Create and attach token to Ink window if it wasn't already created. */ - private void maybeCreateInkWindow() { + private void maybeCreateAndInitInkWindow() { if (mInkWindow == null) { mInkWindow = new InkWindow(mWindow.getContext()); mInkWindow.setToken(mToken); } + mInkWindow.initOnly(); + setInkViewVisibilityListener(); // TODO(b/243571274): set an idle-timeout after which InkWindow is removed. } @@ -2469,13 +2470,19 @@ public class InputMethodService extends AbstractInputMethodService { * @param motionEvent {@link MotionEvent} from stylus. */ public void onStylusHandwritingMotionEvent(@NonNull MotionEvent motionEvent) { - if (mInkWindow.isInkViewVisible()) { + if (mInkWindow != null && mInkWindow.isInkViewVisible()) { mInkWindow.getDecorView().dispatchTouchEvent(motionEvent); } else { if (mPendingEvents == null) { mPendingEvents = new RingBuffer(MotionEvent.class, MAX_EVENTS_BUFFER); } mPendingEvents.append(motionEvent); + setInkViewVisibilityListener(); + } + } + + private void setInkViewVisibilityListener() { + if (mInkWindow != null) { mInkWindow.setInkViewVisibilityListener(() -> { if (mPendingEvents != null && !mPendingEvents.isEmpty()) { for (MotionEvent event : mPendingEvents.toArray()) { @@ -2539,6 +2546,7 @@ public class InputMethodService extends AbstractInputMethodService { mHandler.removeCallbacks(mFinishHwRunnable); } mFinishHwRunnable = null; + mPendingEvents.clear(); final int requestId = mHandwritingRequestId.getAsInt(); mHandwritingRequestId = OptionalInt.empty(); diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index 88649cbf9e42..933769a352e9 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -76,7 +76,7 @@ interface IUserManager { String getUserAccount(int userId); void setUserAccount(int userId, String accountName); long getUserCreationTime(int userId); - boolean isUserSwitcherEnabled(int mUserId); + boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable, int mUserId); boolean isRestricted(int userId); boolean canHaveRestrictedProfile(int userId); int getUserSerialNumber(int userId); diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 1e91700fcf01..5c809a1bac1c 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -5265,36 +5265,10 @@ public class UserManager { public boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable) { try { - if (!mService.isUserSwitcherEnabled(mUserId)) { - return false; - } + return mService.isUserSwitcherEnabled(showEvenIfNotActionable, mUserId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } - - // The feature is enabled. But is it worth showing? - return showEvenIfNotActionable - || areThereUsersToWhichToSwitch() // There are switchable users. - || !hasUserRestrictionForUser(DISALLOW_ADD_USER, mUserId); // New users can be added - } - - /** Returns whether there are any users (other than the current user) to which to switch. */ - @RequiresPermission(anyOf = { - android.Manifest.permission.MANAGE_USERS, - android.Manifest.permission.CREATE_USERS - }) - private boolean areThereUsersToWhichToSwitch() { - final List<UserInfo> users = getAliveUsers(); - if (users == null) { - return false; - } - int switchableUserCount = 0; - for (UserInfo user : users) { - if (user.supportsSwitchToByUser()) { - ++switchableUserCount; - } - } - return switchableUserCount > 1; } /** diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 08de87ebe2e6..c1606e89645e 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -2549,7 +2549,7 @@ public class StorageManager { * called on first creation of a new file on external storage, and whenever the * media type of the file is updated later. * - * This API doesn't require any special permissions, though typical implementations + * This API requires MANAGE_EXTERNAL_STORAGE permission and typical implementations * will require being called from an SELinux domain that allows setting file attributes * related to quota (eg the GID or project ID). * @@ -2568,11 +2568,16 @@ public class StorageManager { * @hide */ @SystemApi + @RequiresPermission(android.Manifest.permission.MANAGE_EXTERNAL_STORAGE) public void updateExternalStorageFileQuotaType(@NonNull File path, @QuotaType int quotaType) throws IOException { long projectId; final String filePath = path.getCanonicalPath(); - final StorageVolume volume = getStorageVolume(path); + // MANAGE_EXTERNAL_STORAGE permission is required as FLAG_INCLUDE_SHARED_PROFILE is being + // set while querying getVolumeList. + final StorageVolume[] availableVolumes = getVolumeList(mContext.getUserId(), + FLAG_REAL_STATE | FLAG_INCLUDE_INVISIBLE | FLAG_INCLUDE_SHARED_PROFILE); + final StorageVolume volume = getStorageVolume(availableVolumes, path); if (volume == null) { Log.w(TAG, "Failed to update quota type for " + filePath); return; diff --git a/core/java/android/preference/GenericInflater.java b/core/java/android/preference/GenericInflater.java index 7edc987ee0c1..fe53d4eeff64 100644 --- a/core/java/android/preference/GenericInflater.java +++ b/core/java/android/preference/GenericInflater.java @@ -406,7 +406,7 @@ abstract class GenericInflater<T, P extends GenericInflater.Parent> { InflateException ie = new InflateException(attrs .getPositionDescription() + ": Error inflating class " - + constructor.getClass().getName()); + + constructor.getDeclaringClass().getName()); ie.initCause(e); throw ie; } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 3a9af7ef4ed2..cab6acbfe2b7 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7462,6 +7462,13 @@ public final class Settings { "trust_agents_initialized"; /** + * Set to 1 by the system after the list of known trust agents have been initialized. + * @hide + */ + public static final String KNOWN_TRUST_AGENTS_INITIALIZED = + "known_trust_agents_initialized"; + + /** * The Logging ID (a unique 64-bit value) as a hex string. * Used as a pseudonymous identifier for logging. * @deprecated This identifier is poorly initialized and has diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index 154fcab8827d..913efbd7b634 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -4820,5 +4820,12 @@ public final class Telephony { * @hide */ public static final String COLUMN_USAGE_SETTING = "usage_setting"; + + /** + * TelephonyProvider column name for user handle associated with this sim. + * + * @hide + */ + public static final String COLUMN_USER_HANDLE = "user_handle"; } } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 067946e90361..b2a26fa665f8 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -67,7 +67,6 @@ import android.view.SurfaceControl; import android.view.displayhash.DisplayHash; import android.view.displayhash.VerifiedDisplayHash; import android.window.ITaskFpsCallback; -import android.window.ScreenCapture; /** * System private interface to the window manager. @@ -969,11 +968,4 @@ interface IWindowManager * treatment. */ boolean isLetterboxBackgroundMultiColored(); - - /** - * Captures the entire display specified by the displayId using the args provided. If the args - * are null or if the sourceCrop is invalid or null, the entire display bounds will be captured. - */ - oneway void captureDisplay(int displayId, in @nullable ScreenCapture.CaptureArgs captureArgs, - in ScreenCapture.ScreenCaptureListener listener); } diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index fb0cac3ba3f4..2f7ae498b8b1 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -225,8 +225,6 @@ public final class SurfaceControl implements Parcelable { private static native void nativeSetDimmingEnabled(long transactionObj, long nativeObject, boolean dimmingEnabled); - private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes); - private static native void nativeSetInputWindowInfo(long transactionObj, long nativeObject, InputWindowHandle handle); @@ -449,6 +447,7 @@ public final class SurfaceControl implements Parcelable { private String mName; /** + * Note: do not rename, this field is used by native code. * @hide */ public long mNativeObject; @@ -2037,18 +2036,6 @@ public final class SurfaceControl implements Parcelable { } /** - * Overrides HDR modes for a display device. - * - * If the caller does not have ACCESS_SURFACE_FLINGER permission, this will throw a Security - * Exception. - * @hide - */ - @TestApi - public static void overrideHdrTypes(@NonNull IBinder displayToken, @NonNull int[] modes) { - nativeOverrideHdrTypes(displayToken, modes); - } - - /** * @hide */ public static long[] getPhysicalDisplayIds() { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index b6f775d3e536..ec1f73f648cf 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -286,7 +286,7 @@ public final class ViewRootImpl implements ViewParent, * @hide */ public static final boolean LOCAL_LAYOUT = - SystemProperties.getBoolean("persist.debug.local_layout", false); + SystemProperties.getBoolean("persist.debug.local_layout", true); /** * Set this system property to true to force the view hierarchy to render diff --git a/core/java/android/window/ScreenCapture.java b/core/java/android/window/ScreenCapture.java index b8f39a990cdd..887d027d26a8 100644 --- a/core/java/android/window/ScreenCapture.java +++ b/core/java/android/window/ScreenCapture.java @@ -24,16 +24,11 @@ import android.graphics.PixelFormat; import android.graphics.Rect; import android.hardware.HardwareBuffer; import android.os.IBinder; -import android.os.Parcel; -import android.os.Parcelable; import android.util.Log; import android.view.SurfaceControl; -import libcore.util.NativeAllocationRegistry; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; /** * Handles display and layer captures for the system. @@ -44,23 +39,18 @@ public class ScreenCapture { private static final String TAG = "ScreenCapture"; private static native int nativeCaptureDisplay(DisplayCaptureArgs captureArgs, - long captureListener); + ScreenCaptureListener captureListener); private static native int nativeCaptureLayers(LayerCaptureArgs captureArgs, - long captureListener); - private static native long nativeCreateScreenCaptureListener( - Consumer<ScreenshotHardwareBuffer> consumer); - private static native void nativeWriteListenerToParcel(long nativeObject, Parcel out); - private static native long nativeReadListenerFromParcel(Parcel in); - private static native long getNativeListenerFinalizer(); + ScreenCaptureListener captureListener); /** - * @param captureArgs Arguments about how to take the screenshot + * @param captureArgs Arguments about how to take the screenshot * @param captureListener A listener to receive the screenshot callback * @hide */ public static int captureDisplay(@NonNull DisplayCaptureArgs captureArgs, @NonNull ScreenCaptureListener captureListener) { - return nativeCaptureDisplay(captureArgs, captureListener.mNativeObject); + return nativeCaptureDisplay(captureArgs, captureListener); } /** @@ -71,8 +61,10 @@ public class ScreenCapture { */ public static ScreenshotHardwareBuffer captureDisplay( DisplayCaptureArgs captureArgs) { - SyncScreenCaptureListener screenCaptureListener = new SyncScreenCaptureListener(); - int status = captureDisplay(captureArgs, screenCaptureListener.getScreenCaptureListener()); + SyncScreenCaptureListener + screenCaptureListener = new SyncScreenCaptureListener(); + + int status = captureDisplay(captureArgs, screenCaptureListener); if (status != 0) { return null; } @@ -83,13 +75,14 @@ public class ScreenCapture { /** * Captures a layer and its children and returns a {@link HardwareBuffer} with the content. * - * @param layer The root layer to capture. - * @param sourceCrop The portion of the root surface to capture; caller may pass in 'new - * Rect()' or null if no cropping is desired. If the root layer does not - * have a buffer or a crop set, then a non-empty source crop must be - * specified. - * @param frameScale The desired scale of the returned buffer; the raw screen will be scaled - * up/down. + * @param layer The root layer to capture. + * @param sourceCrop The portion of the root surface to capture; caller may pass in 'new + * Rect()' or null if no cropping is desired. If the root layer does not + * have a buffer or a crop set, then a non-empty source crop must be + * specified. + * @param frameScale The desired scale of the returned buffer; the raw + * screen will be scaled up/down. + * * @return Returns a HardwareBuffer that contains the layer capture. * @hide */ @@ -101,14 +94,15 @@ public class ScreenCapture { /** * Captures a layer and its children and returns a {@link HardwareBuffer} with the content. * - * @param layer The root layer to capture. - * @param sourceCrop The portion of the root surface to capture; caller may pass in 'new - * Rect()' or null if no cropping is desired. If the root layer does not - * have a buffer or a crop set, then a non-empty source crop must be - * specified. - * @param frameScale The desired scale of the returned buffer; the raw screen will be scaled - * up/down. - * @param format The desired pixel format of the returned buffer. + * @param layer The root layer to capture. + * @param sourceCrop The portion of the root surface to capture; caller may pass in 'new + * Rect()' or null if no cropping is desired. If the root layer does not + * have a buffer or a crop set, then a non-empty source crop must be + * specified. + * @param frameScale The desired scale of the returned buffer; the raw + * screen will be scaled up/down. + * @param format The desired pixel format of the returned buffer. + * * @return Returns a HardwareBuffer that contains the layer capture. * @hide */ @@ -130,7 +124,7 @@ public class ScreenCapture { LayerCaptureArgs captureArgs) { SyncScreenCaptureListener screenCaptureListener = new SyncScreenCaptureListener(); - int status = captureLayers(captureArgs, screenCaptureListener.getScreenCaptureListener()); + int status = captureLayers(captureArgs, screenCaptureListener); if (status != 0) { return null; } @@ -141,7 +135,6 @@ public class ScreenCapture { /** * Like {@link #captureLayers(SurfaceControl, Rect, float, int)} but with an array of layer * handles to exclude. - * * @hide */ public static ScreenshotHardwareBuffer captureLayersExcluding(SurfaceControl layer, @@ -157,13 +150,24 @@ public class ScreenCapture { } /** - * @param captureArgs Arguments about how to take the screenshot + * @param captureArgs Arguments about how to take the screenshot * @param captureListener A listener to receive the screenshot callback * @hide */ public static int captureLayers(@NonNull LayerCaptureArgs captureArgs, @NonNull ScreenCaptureListener captureListener) { - return nativeCaptureLayers(captureArgs, captureListener.mNativeObject); + return nativeCaptureLayers(captureArgs, captureListener); + } + + /** + * @hide + */ + public interface ScreenCaptureListener { + /** + * The callback invoked when the screen capture is complete. + * @param hardwareBuffer Data containing info about the screen capture. + */ + void onScreenCaptureComplete(ScreenshotHardwareBuffer hardwareBuffer); } /** @@ -186,16 +190,15 @@ public class ScreenCapture { mContainsHdrLayers = containsHdrLayers; } - /** - * Create ScreenshotHardwareBuffer from an existing HardwareBuffer object. - * - * @param hardwareBuffer The existing HardwareBuffer object - * @param namedColorSpace Integer value of a named color space {@link ColorSpace.Named} - * @param containsSecureLayers Indicates whether this graphic buffer contains captured - * contents of secure layers, in which case the screenshot - * should not be persisted. - * @param containsHdrLayers Indicates whether this graphic buffer contains HDR content. - */ + /** + * Create ScreenshotHardwareBuffer from an existing HardwareBuffer object. + * @param hardwareBuffer The existing HardwareBuffer object + * @param namedColorSpace Integer value of a named color space {@link ColorSpace.Named} + * @param containsSecureLayers Indicates whether this graphic buffer contains captured + * contents of secure layers, in which case the screenshot + * should not be persisted. + * @param containsHdrLayers Indicates whether this graphic buffer contains HDR content. + */ private static ScreenshotHardwareBuffer createFromNative(HardwareBuffer hardwareBuffer, int namedColorSpace, boolean containsSecureLayers, boolean containsHdrLayers) { ColorSpace colorSpace = ColorSpace.get(ColorSpace.Named.values()[namedColorSpace]); @@ -217,7 +220,6 @@ public class ScreenCapture { public boolean containsSecureLayers() { return mContainsSecureLayers; } - /** * Returns whether the screenshot contains at least one HDR layer. * This information may be useful for informing the display whether this screenshot @@ -232,7 +234,7 @@ public class ScreenCapture { * Note: If you want to modify the Bitmap in software, you will need to copy the Bitmap * into * a software Bitmap using {@link Bitmap#copy(Bitmap.Config, boolean)} - * <p> + * * CAVEAT: This can be extremely slow; avoid use unless absolutely necessary; prefer to * directly * use the {@link HardwareBuffer} directly. @@ -248,23 +250,44 @@ public class ScreenCapture { } } + private static class SyncScreenCaptureListener implements ScreenCaptureListener { + private static final int SCREENSHOT_WAIT_TIME_S = 1; + private ScreenshotHardwareBuffer mScreenshotHardwareBuffer; + private final CountDownLatch mCountDownLatch = new CountDownLatch(1); + + @Override + public void onScreenCaptureComplete(ScreenshotHardwareBuffer hardwareBuffer) { + mScreenshotHardwareBuffer = hardwareBuffer; + mCountDownLatch.countDown(); + } + + private ScreenshotHardwareBuffer waitForScreenshot() { + try { + mCountDownLatch.await(SCREENSHOT_WAIT_TIME_S, TimeUnit.SECONDS); + } catch (Exception e) { + Log.e(TAG, "Failed to wait for screen capture result", e); + } + + return mScreenshotHardwareBuffer; + } + } + /** * A common arguments class used for various screenshot requests. This contains arguments that * are shared between {@link DisplayCaptureArgs} and {@link LayerCaptureArgs} - * * @hide */ - public static class CaptureArgs implements Parcelable { - public final int mPixelFormat; - public final Rect mSourceCrop = new Rect(); - public final float mFrameScaleX; - public final float mFrameScaleY; - public final boolean mCaptureSecureLayers; - public final boolean mAllowProtected; - public final long mUid; - public final boolean mGrayscale; - - private CaptureArgs(CaptureArgs.Builder<? extends CaptureArgs.Builder<?>> builder) { + private abstract static class CaptureArgs { + private final int mPixelFormat; + private final Rect mSourceCrop = new Rect(); + private final float mFrameScaleX; + private final float mFrameScaleY; + private final boolean mCaptureSecureLayers; + private final boolean mAllowProtected; + private final long mUid; + private final boolean mGrayscale; + + private CaptureArgs(Builder<? extends Builder<?>> builder) { mPixelFormat = builder.mPixelFormat; mSourceCrop.set(builder.mSourceCrop); mFrameScaleX = builder.mFrameScaleX; @@ -275,23 +298,12 @@ public class ScreenCapture { mGrayscale = builder.mGrayscale; } - private CaptureArgs(Parcel in) { - mPixelFormat = in.readInt(); - mSourceCrop.readFromParcel(in); - mFrameScaleX = in.readFloat(); - mFrameScaleY = in.readFloat(); - mCaptureSecureLayers = in.readBoolean(); - mAllowProtected = in.readBoolean(); - mUid = in.readLong(); - mGrayscale = in.readBoolean(); - } - /** * The Builder class used to construct {@link CaptureArgs} * - * @param <T> A builder that extends {@link CaptureArgs.Builder} + * @param <T> A builder that extends {@link Builder} */ - public static class Builder<T extends CaptureArgs.Builder<T>> { + abstract static class Builder<T extends Builder<T>> { private int mPixelFormat = PixelFormat.RGBA_8888; private final Rect mSourceCrop = new Rect(); private float mFrameScaleX = 1; @@ -302,14 +314,6 @@ public class ScreenCapture { private boolean mGrayscale; /** - * Construct a new {@link CaptureArgs} with the set parameters. The builder remains - * valid. - */ - public CaptureArgs build() { - return new CaptureArgs(this); - } - - /** * The desired pixel format of the returned buffer. */ public T setPixelFormat(int pixelFormat) { @@ -391,47 +395,15 @@ public class ScreenCapture { /** * Each sub class should return itself to allow the builder to chain properly */ - T getThis() { - return (T) this; - } - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeInt(mPixelFormat); - mSourceCrop.writeToParcel(dest, flags); - dest.writeFloat(mFrameScaleX); - dest.writeFloat(mFrameScaleY); - dest.writeBoolean(mCaptureSecureLayers); - dest.writeBoolean(mAllowProtected); - dest.writeLong(mUid); - dest.writeBoolean(mGrayscale); + abstract T getThis(); } - - public static final Parcelable.Creator<CaptureArgs> CREATOR = - new Parcelable.Creator<CaptureArgs>() { - @Override - public CaptureArgs createFromParcel(Parcel in) { - return new CaptureArgs(in); - } - - @Override - public CaptureArgs[] newArray(int size) { - return new CaptureArgs[size]; - } - }; } /** * The arguments class used to make display capture requests. * + * @see #nativeCaptureDisplay(DisplayCaptureArgs, ScreenCaptureListener) * @hide - * @see #nativeCaptureDisplay(DisplayCaptureArgs, long) */ public static class DisplayCaptureArgs extends CaptureArgs { private final IBinder mDisplayToken; @@ -516,8 +488,8 @@ public class ScreenCapture { /** * The arguments class used to make layer capture requests. * + * @see #nativeCaptureLayers(LayerCaptureArgs, ScreenCaptureListener) * @hide - * @see #nativeCaptureLayers(LayerCaptureArgs, long) */ public static class LayerCaptureArgs extends CaptureArgs { private final long mNativeLayer; @@ -558,17 +530,6 @@ public class ScreenCapture { return new LayerCaptureArgs(this); } - public Builder(SurfaceControl layer, CaptureArgs args) { - setLayer(layer); - setPixelFormat(args.mPixelFormat); - setSourceCrop(args.mSourceCrop); - setFrameScale(args.mFrameScaleX, args.mFrameScaleY); - setCaptureSecureLayers(args.mCaptureSecureLayers); - setAllowProtected(args.mAllowProtected); - setUid(args.mUid); - setGrayscale(args.mGrayscale); - } - public Builder(SurfaceControl layer) { setLayer(layer); } @@ -581,6 +542,7 @@ public class ScreenCapture { return this; } + /** * An array of layer handles to exclude. */ @@ -602,106 +564,8 @@ public class ScreenCapture { Builder getThis() { return this; } - } - } - /** - * The object used to receive the results when invoking screen capture requests via - * {@link #captureDisplay(DisplayCaptureArgs, ScreenCaptureListener)} or - * {@link #captureLayers(LayerCaptureArgs, ScreenCaptureListener)} - */ - public static class ScreenCaptureListener implements Parcelable { - private final long mNativeObject; - private static final NativeAllocationRegistry sRegistry = - NativeAllocationRegistry.createMalloced( - ScreenCaptureListener.class.getClassLoader(), getNativeListenerFinalizer()); - - /** - * @param consumer The callback invoked when the screen capture is complete. - */ - public ScreenCaptureListener(Consumer<ScreenshotHardwareBuffer> consumer) { - mNativeObject = nativeCreateScreenCaptureListener(consumer); - sRegistry.registerNativeAllocation(this, mNativeObject); } - - private ScreenCaptureListener(Parcel in) { - if (in.readBoolean()) { - mNativeObject = nativeReadListenerFromParcel(in); - sRegistry.registerNativeAllocation(this, mNativeObject); - } else { - mNativeObject = 0; - } - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - if (mNativeObject == 0) { - dest.writeBoolean(false); - } else { - dest.writeBoolean(true); - nativeWriteListenerToParcel(mNativeObject, dest); - } - } - - public static final Parcelable.Creator<ScreenCaptureListener> CREATOR = - new Parcelable.Creator<ScreenCaptureListener>() { - @Override - public ScreenCaptureListener createFromParcel(Parcel in) { - return new ScreenCaptureListener(in); - } - - @Override - public ScreenCaptureListener[] newArray(int size) { - return new ScreenCaptureListener[0]; - } - }; } - /** - * A helper class to handle the async screencapture callbacks synchronously. This should only - * be used if the screencapture caller doesn't care that it blocks waiting for a screenshot. - */ - public static class SyncScreenCaptureListener { - private static final int SCREENSHOT_WAIT_TIME_S = 1; - private ScreenshotHardwareBuffer mScreenshotHardwareBuffer; - private final CountDownLatch mCountDownLatch = new CountDownLatch(1); - private final ScreenCaptureListener mScreenCaptureListener; - - public SyncScreenCaptureListener() { - mScreenCaptureListener = new ScreenCaptureListener(screenshotHardwareBuffer -> { - mScreenshotHardwareBuffer = screenshotHardwareBuffer; - mCountDownLatch.countDown(); - }); - } - - /** - * @return The underlying {@link ScreenCaptureListener} - */ - public ScreenCaptureListener getScreenCaptureListener() { - return mScreenCaptureListener; - } - - /** - * Waits until the screenshot callback has been invoked and the screenshot is ready. This - * can return {@code null} if the screenshot callback wasn't invoked after - * {@link #SCREENSHOT_WAIT_TIME_S} or the screencapture request resulted in an error - * - * @return A ScreenshotHardwareBuffer for the content that was captured. - */ - @Nullable - public ScreenshotHardwareBuffer waitForScreenshot() { - try { - mCountDownLatch.await(SCREENSHOT_WAIT_TIME_S, TimeUnit.SECONDS); - } catch (Exception e) { - Log.e(TAG, "Failed to wait for screen capture result", e); - } - - return mScreenshotHardwareBuffer; - } - } } diff --git a/core/java/android/window/TaskFragmentInfo.java b/core/java/android/window/TaskFragmentInfo.java index 56e910769cb5..e2c8a31cc987 100644 --- a/core/java/android/window/TaskFragmentInfo.java +++ b/core/java/android/window/TaskFragmentInfo.java @@ -188,6 +188,10 @@ public final class TaskFragmentInfo implements Parcelable { /** * Returns {@code true} if the parameters that are important for task fragment organizers are * equal between this {@link TaskFragmentInfo} and {@param that}. + * Note that this method is usually called with + * {@link com.android.server.wm.WindowOrganizerController#configurationsAreEqualForOrganizer( + * Configuration, Configuration)} to determine if this {@link TaskFragmentInfo} should + * be dispatched to the client. */ public boolean equalsForTaskFragmentOrganizer(@Nullable TaskFragmentInfo that) { if (that == null) { diff --git a/core/java/android/window/ScreenCapture.aidl b/core/java/android/window/TaskFragmentParentInfo.aidl index 267a7c60b60d..79d2209ab244 100644 --- a/core/java/android/window/ScreenCapture.aidl +++ b/core/java/android/window/TaskFragmentParentInfo.aidl @@ -16,11 +16,8 @@ package android.window; -/** @hide */ -parcelable ScreenCapture.CaptureArgs; - -/** @hide */ -parcelable ScreenCapture.ScreenshotHardwareBuffer; - -/** @hide */ -parcelable ScreenCapture.ScreenCaptureListener;
\ No newline at end of file +/** + * The information about the parent Task of a particular TaskFragment + * @hide + */ +parcelable TaskFragmentParentInfo;
\ No newline at end of file diff --git a/core/java/android/window/TaskFragmentParentInfo.java b/core/java/android/window/TaskFragmentParentInfo.java new file mode 100644 index 000000000000..64b2638407df --- /dev/null +++ b/core/java/android/window/TaskFragmentParentInfo.java @@ -0,0 +1,158 @@ +/* + * 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.window; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.WindowConfiguration; +import android.content.res.Configuration; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * The information about the parent Task of a particular TaskFragment + * @hide + */ +public class TaskFragmentParentInfo implements Parcelable { + @NonNull + private final Configuration mConfiguration = new Configuration(); + + private final int mDisplayId; + + private final boolean mVisibleRequested; + + public TaskFragmentParentInfo(@NonNull Configuration configuration, int displayId, + boolean visibleRequested) { + mConfiguration.setTo(configuration); + mDisplayId = displayId; + mVisibleRequested = visibleRequested; + } + + public TaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { + mConfiguration.setTo(info.getConfiguration()); + mDisplayId = info.mDisplayId; + mVisibleRequested = info.mVisibleRequested; + } + + /** The {@link Configuration} of the parent Task */ + @NonNull + public Configuration getConfiguration() { + return mConfiguration; + } + + /** + * The display ID of the parent Task. {@link android.view.Display#INVALID_DISPLAY} means the + * Task is detached from previously associated display. + */ + public int getDisplayId() { + return mDisplayId; + } + + /** Whether the parent Task is requested to be visible or not */ + public boolean isVisibleRequested() { + return mVisibleRequested; + } + + /** + * Returns {@code true} if the parameters which are important for task fragment + * organizers are equal between this {@link TaskFragmentParentInfo} and {@code that}. + * Note that this method is usually called with + * {@link com.android.server.wm.WindowOrganizerController#configurationsAreEqualForOrganizer( + * Configuration, Configuration)} to determine if this {@link TaskFragmentParentInfo} should + * be dispatched to the client. + */ + public boolean equalsForTaskFragmentOrganizer(@Nullable TaskFragmentParentInfo that) { + if (that == null) { + return false; + } + return getWindowingMode() == that.getWindowingMode() && mDisplayId == that.mDisplayId + && mVisibleRequested == that.mVisibleRequested; + } + + @WindowConfiguration.WindowingMode + private int getWindowingMode() { + return mConfiguration.windowConfiguration.getWindowingMode(); + } + + @Override + public String toString() { + return TaskFragmentParentInfo.class.getSimpleName() + ":{" + + "config=" + mConfiguration + + ", displayId=" + mDisplayId + + ", visibleRequested=" + mVisibleRequested + + "}"; + } + + /** + * Indicates that whether this {@link TaskFragmentParentInfo} equals to {@code obj}. + * Note that {@link #equalsForTaskFragmentOrganizer(TaskFragmentParentInfo)} should be used + * for most cases because not all {@link Configuration} properties are interested for + * {@link TaskFragmentOrganizer}. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof TaskFragmentParentInfo)) { + return false; + } + final TaskFragmentParentInfo that = (TaskFragmentParentInfo) obj; + return mConfiguration.equals(that.mConfiguration) + && mDisplayId == that.mDisplayId + && mVisibleRequested == that.mVisibleRequested; + } + + @Override + public int hashCode() { + int result = mConfiguration.hashCode(); + result = 31 * result + mDisplayId; + result = 31 * result + (mVisibleRequested ? 1 : 0); + return result; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + mConfiguration.writeToParcel(dest, flags); + dest.writeInt(mDisplayId); + dest.writeBoolean(mVisibleRequested); + } + + private TaskFragmentParentInfo(Parcel in) { + mConfiguration.readFromParcel(in); + mDisplayId = in.readInt(); + mVisibleRequested = in.readBoolean(); + } + + public static final Creator<TaskFragmentParentInfo> CREATOR = + new Creator<TaskFragmentParentInfo>() { + @Override + public TaskFragmentParentInfo createFromParcel(Parcel in) { + return new TaskFragmentParentInfo(in); + } + + @Override + public TaskFragmentParentInfo[] newArray(int size) { + return new TaskFragmentParentInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/window/TaskFragmentTransaction.java b/core/java/android/window/TaskFragmentTransaction.java index 04fcd3afcf1d..76677431d6fa 100644 --- a/core/java/android/window/TaskFragmentTransaction.java +++ b/core/java/android/window/TaskFragmentTransaction.java @@ -173,10 +173,6 @@ public final class TaskFragmentTransaction implements Parcelable { /** @see #setTaskId(int) */ private int mTaskId; - /** @see #setTaskConfiguration(Configuration) */ - @Nullable - private Configuration mTaskConfiguration; - /** @see #setErrorCallbackToken(IBinder) */ @Nullable private IBinder mErrorCallbackToken; @@ -193,6 +189,9 @@ public final class TaskFragmentTransaction implements Parcelable { @Nullable private IBinder mActivityToken; + @Nullable + private TaskFragmentParentInfo mTaskFragmentParentInfo; + public Change(@ChangeType int type) { mType = type; } @@ -202,11 +201,11 @@ public final class TaskFragmentTransaction implements Parcelable { mTaskFragmentToken = in.readStrongBinder(); mTaskFragmentInfo = in.readTypedObject(TaskFragmentInfo.CREATOR); mTaskId = in.readInt(); - mTaskConfiguration = in.readTypedObject(Configuration.CREATOR); mErrorCallbackToken = in.readStrongBinder(); mErrorBundle = in.readBundle(TaskFragmentTransaction.class.getClassLoader()); mActivityIntent = in.readTypedObject(Intent.CREATOR); mActivityToken = in.readStrongBinder(); + mTaskFragmentParentInfo = in.readTypedObject(TaskFragmentParentInfo.CREATOR); } @Override @@ -215,11 +214,11 @@ public final class TaskFragmentTransaction implements Parcelable { dest.writeStrongBinder(mTaskFragmentToken); dest.writeTypedObject(mTaskFragmentInfo, flags); dest.writeInt(mTaskId); - dest.writeTypedObject(mTaskConfiguration, flags); dest.writeStrongBinder(mErrorCallbackToken); dest.writeBundle(mErrorBundle); dest.writeTypedObject(mActivityIntent, flags); dest.writeStrongBinder(mActivityToken); + dest.writeTypedObject(mTaskFragmentParentInfo, flags); } /** The change is related to the TaskFragment created with this unique token. */ @@ -243,10 +242,10 @@ public final class TaskFragmentTransaction implements Parcelable { return this; } + // TODO(b/241043377): Keep this API to prevent @TestApi changes. Remove in the next release. /** Configuration of the parent Task. */ @NonNull public Change setTaskConfiguration(@NonNull Configuration configuration) { - mTaskConfiguration = requireNonNull(configuration); return this; } @@ -294,6 +293,19 @@ public final class TaskFragmentTransaction implements Parcelable { return this; } + // TODO(b/241043377): Hide this API to prevent @TestApi changes. Remove in the next release. + /** + * Sets info of the parent Task of the embedded TaskFragment. + * @see TaskFragmentParentInfo + * + * @hide pending unhide + */ + @NonNull + public Change setTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { + mTaskFragmentParentInfo = requireNonNull(info); + return this; + } + @ChangeType public int getType() { return mType; @@ -313,9 +325,10 @@ public final class TaskFragmentTransaction implements Parcelable { return mTaskId; } + // TODO(b/241043377): Keep this API to prevent @TestApi changes. Remove in the next release. @Nullable public Configuration getTaskConfiguration() { - return mTaskConfiguration; + return mTaskFragmentParentInfo.getConfiguration(); } @Nullable @@ -339,6 +352,13 @@ public final class TaskFragmentTransaction implements Parcelable { return mActivityToken; } + // TODO(b/241043377): Hide this API to prevent @TestApi changes. Remove in the next release. + /** @hide pending unhide */ + @Nullable + public TaskFragmentParentInfo getTaskFragmentParentInfo() { + return mTaskFragmentParentInfo; + } + @Override public String toString() { return "Change{ type=" + mType + " }"; diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 33ea2e4af473..641d1a189711 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -125,8 +125,15 @@ public final class TransitionInfo implements Parcelable { /** The container attaches work profile thumbnail for cross profile animation. */ public static final int FLAG_CROSS_PROFILE_WORK_THUMBNAIL = 1 << 13; + /** + * Whether the window is covered by an app starting window. This is different from + * {@link #FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT} which is only set on the Activity window + * that contains the starting window. + */ + public static final int FLAG_IS_BEHIND_STARTING_WINDOW = 1 << 14; + /** The first unused bit. This can be used by remotes to attach custom flags to this change. */ - public static final int FLAG_FIRST_CUSTOM = 1 << 14; + public static final int FLAG_FIRST_CUSTOM = 1 << 15; /** @hide */ @IntDef(prefix = { "FLAG_" }, value = { @@ -145,6 +152,7 @@ public final class TransitionInfo implements Parcelable { FLAG_WILL_IME_SHOWN, FLAG_CROSS_PROFILE_OWNER_THUMBNAIL, FLAG_CROSS_PROFILE_WORK_THUMBNAIL, + FLAG_IS_BEHIND_STARTING_WINDOW, FLAG_FIRST_CUSTOM }) public @interface ChangeFlags {} @@ -351,6 +359,9 @@ public final class TransitionInfo implements Parcelable { if ((flags & FLAG_FILLS_TASK) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("FILLS_TASK"); } + if ((flags & FLAG_IS_BEHIND_STARTING_WINDOW) != 0) { + sb.append(sb.length() == 0 ? "" : "|").append("IS_BEHIND_STARTING_WINDOW"); + } if ((flags & FLAG_FIRST_CUSTOM) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM"); } diff --git a/core/java/com/android/internal/app/SimpleIconFactory.java b/core/java/com/android/internal/app/SimpleIconFactory.java index 354eb62ba045..be0b729aedfe 100644 --- a/core/java/com/android/internal/app/SimpleIconFactory.java +++ b/core/java/com/android/internal/app/SimpleIconFactory.java @@ -51,6 +51,7 @@ import android.util.Pools.SynchronizedPool; import android.util.TypedValue; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import org.xmlpull.v1.XmlPullParser; @@ -69,6 +70,7 @@ public class SimpleIconFactory { private static final SynchronizedPool<SimpleIconFactory> sPool = new SynchronizedPool<>(Runtime.getRuntime().availableProcessors()); + private static boolean sPoolEnabled = true; private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE; private static final float BLUR_FACTOR = 1.5f / 48; @@ -92,7 +94,7 @@ public class SimpleIconFactory { */ @Deprecated public static SimpleIconFactory obtain(Context ctx) { - SimpleIconFactory instance = sPool.acquire(); + SimpleIconFactory instance = sPoolEnabled ? sPool.acquire() : null; if (instance == null) { final ActivityManager am = (ActivityManager) ctx.getSystemService(ACTIVITY_SERVICE); final int iconDpi = (am == null) ? 0 : am.getLauncherLargeIconDensity(); @@ -106,6 +108,17 @@ public class SimpleIconFactory { return instance; } + /** + * Enables or disables SimpleIconFactory objects pooling. It is enabled in production, you + * could use this method in tests and disable the pooling to make the icon rendering more + * deterministic because some sizing parameters will not be cached. Please ensure that you + * reset this value back after finishing the test. + */ + @VisibleForTesting + public static void setPoolEnabled(boolean poolEnabled) { + sPoolEnabled = poolEnabled; + } + private static int getAttrDimFromContext(Context ctx, @AttrRes int attrId, String errorMsg) { final Resources res = ctx.getResources(); TypedValue outVal = new TypedValue(); diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index b1e7d15cbf4a..deafd19e866f 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -1001,16 +1001,24 @@ public final class Zygote { } /** + * This will enable jdwp by default for all apps. It is OK to cache this property + * because we expect to reboot the system whenever this property changes + */ + private static final boolean ENABLE_JDWP = SystemProperties.get( + "persist.debug.dalvik.vm.jdwp.enabled").equals("1"); + + /** * Applies debugger system properties to the zygote arguments. * - * If "ro.debuggable" is "1", all apps are debuggable. Otherwise, - * the debugger state is specified via the "--enable-jdwp" flag - * in the spawn request. + * For eng builds all apps are debuggable. On userdebug and user builds + * if persist.debuggable.dalvik.vm.jdwp.enabled is 1 all apps are + * debuggable. Otherwise, the debugger state is specified via the + * "--enable-jdwp" flag in the spawn request. * * @param args non-null; zygote spawner args */ static void applyDebuggerSystemProperty(ZygoteArguments args) { - if (RoSystemProperties.DEBUGGABLE) { + if (Build.IS_ENG || ENABLE_JDWP) { args.mRuntimeFlags |= Zygote.DEBUG_ENABLE_JDWP; } } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 3e0a6cb1cae0..65c2d00fda06 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -164,6 +164,7 @@ public class LockPatternUtils { private static final String LOCK_SCREEN_DEVICE_OWNER_INFO = "lockscreen.device_owner_info"; private static final String ENABLED_TRUST_AGENTS = "lockscreen.enabledtrustagents"; + private static final String KNOWN_TRUST_AGENTS = "lockscreen.knowntrustagents"; private static final String IS_TRUST_USUALLY_MANAGED = "lockscreen.istrustusuallymanaged"; public static final String CURRENT_LSKF_BASED_PROTECTOR_ID_KEY = "sp-handle"; @@ -1089,31 +1090,50 @@ public class LockPatternUtils { return getString(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, userId) != null; } + /** Updates the list of enabled trust agent in LockSettings storage for the given user. */ public void setEnabledTrustAgents(Collection<ComponentName> activeTrustAgents, int userId) { + setString(ENABLED_TRUST_AGENTS, serializeTrustAgents(activeTrustAgents), userId); + getTrustManager().reportEnabledTrustAgentsChanged(userId); + } + + /** Returns the list of enabled trust agent in LockSettings storage for the given user. */ + public List<ComponentName> getEnabledTrustAgents(int userId) { + return deserializeTrustAgents(getString(ENABLED_TRUST_AGENTS, userId)); + } + + /** Updates the list of known trust agent in LockSettings storage for the given user. */ + public void setKnownTrustAgents(Collection<ComponentName> knownTrustAgents, int userId) { + setString(KNOWN_TRUST_AGENTS, serializeTrustAgents(knownTrustAgents), userId); + } + + /** Returns the list of known trust agent in LockSettings storage for the given user. */ + public List<ComponentName> getKnownTrustAgents(int userId) { + return deserializeTrustAgents(getString(KNOWN_TRUST_AGENTS, userId)); + } + + private String serializeTrustAgents(Collection<ComponentName> trustAgents) { StringBuilder sb = new StringBuilder(); - for (ComponentName cn : activeTrustAgents) { + for (ComponentName cn : trustAgents) { if (sb.length() > 0) { sb.append(','); } sb.append(cn.flattenToShortString()); } - setString(ENABLED_TRUST_AGENTS, sb.toString(), userId); - getTrustManager().reportEnabledTrustAgentsChanged(userId); + return sb.toString(); } - public List<ComponentName> getEnabledTrustAgents(int userId) { - String serialized = getString(ENABLED_TRUST_AGENTS, userId); - if (TextUtils.isEmpty(serialized)) { - return new ArrayList<ComponentName>(); + private List<ComponentName> deserializeTrustAgents(String serializedTrustAgents) { + if (TextUtils.isEmpty(serializedTrustAgents)) { + return new ArrayList<>(); } - String[] split = serialized.split(","); - ArrayList<ComponentName> activeTrustAgents = new ArrayList<ComponentName>(split.length); + String[] split = serializedTrustAgents.split(","); + ArrayList<ComponentName> trustAgents = new ArrayList<>(split.length); for (String s : split) { if (!TextUtils.isEmpty(s)) { - activeTrustAgents.add(ComponentName.unflattenFromString(s)); + trustAgents.add(ComponentName.unflattenFromString(s)); } } - return activeTrustAgents; + return trustAgents; } /** @@ -1487,7 +1507,8 @@ public class LockPatternUtils { STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, STRONG_AUTH_REQUIRED_AFTER_TIMEOUT, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, - STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT}) + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT, + SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED}) @Retention(RetentionPolicy.SOURCE) public @interface StrongAuthFlags {} @@ -1540,6 +1561,12 @@ public class LockPatternUtils { public static final int STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT = 0x80; /** + * Some authentication is required because the trustagent either timed out or was disabled + * manually. + */ + public static final int SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED = 0x100; + + /** * Strong auth flags that do not prevent biometric methods from being accepted as auth. * If any other flags are set, biometric authentication is disabled. */ diff --git a/core/jni/android_os_MessageQueue.cpp b/core/jni/android_os_MessageQueue.cpp index f7a98d16f2e3..30d9ea19be39 100644 --- a/core/jni/android_os_MessageQueue.cpp +++ b/core/jni/android_os_MessageQueue.cpp @@ -51,6 +51,21 @@ public: virtual int handleEvent(int fd, int events, void* data); + /** + * A simple proxy that holds a weak reference to a looper callback. + */ + class WeakLooperCallback : public LooperCallback { + protected: + virtual ~WeakLooperCallback(); + + public: + WeakLooperCallback(const wp<LooperCallback>& callback); + virtual int handleEvent(int fd, int events, void* data); + + private: + wp<LooperCallback> mCallback; + }; + private: JNIEnv* mPollEnv; jobject mPollObj; @@ -131,7 +146,8 @@ void NativeMessageQueue::setFileDescriptorEvents(int fd, int events) { if (events & CALLBACK_EVENT_OUTPUT) { looperEvents |= Looper::EVENT_OUTPUT; } - mLooper->addFd(fd, Looper::POLL_CALLBACK, looperEvents, this, + mLooper->addFd(fd, Looper::POLL_CALLBACK, looperEvents, + sp<WeakLooperCallback>::make(this), reinterpret_cast<void*>(events)); } else { mLooper->removeFd(fd); @@ -162,6 +178,24 @@ int NativeMessageQueue::handleEvent(int fd, int looperEvents, void* data) { } +// --- NativeMessageQueue::WeakLooperCallback --- + +NativeMessageQueue::WeakLooperCallback::WeakLooperCallback(const wp<LooperCallback>& callback) : + mCallback(callback) { +} + +NativeMessageQueue::WeakLooperCallback::~WeakLooperCallback() { +} + +int NativeMessageQueue::WeakLooperCallback::handleEvent(int fd, int events, void* data) { + sp<LooperCallback> callback = mCallback.promote(); + if (callback != nullptr) { + return callback->handleEvent(fd, events, data); + } + return 0; +} + + // ---------------------------------------------------------------------------- sp<MessageQueue> android_os_MessageQueue_getMessageQueue(JNIEnv* env, jobject messageQueueObj) { diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp index 8c23b214b8fe..764338faef64 100644 --- a/core/jni/android_util_AssetManager.cpp +++ b/core/jni/android_util_AssetManager.cpp @@ -39,6 +39,7 @@ #include "androidfw/AssetManager2.h" #include "androidfw/AttributeResolution.h" #include "androidfw/MutexGuard.h" +#include <androidfw/ResourceTimer.h> #include "androidfw/ResourceTypes.h" #include "androidfw/ResourceUtils.h" @@ -630,6 +631,7 @@ static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jin jshort density, jobject typed_value, jboolean resolve_references) { ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr)); + ResourceTimer _tag(ResourceTimer::Counter::GetResourceValue); auto value = assetmanager->GetResource(static_cast<uint32_t>(resid), false /*may_be_bag*/, static_cast<uint16_t>(density)); if (!value.has_value()) { @@ -1232,6 +1234,7 @@ static jboolean NativeRetrieveAttributes(JNIEnv* env, jclass /*clazz*/, jlong pt } ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr)); + ResourceTimer _tag(ResourceTimer::Counter::RetrieveAttributes); ResXMLParser* xml_parser = reinterpret_cast<ResXMLParser*>(xml_parser_ptr); auto result = RetrieveAttributes(assetmanager.get(), xml_parser, reinterpret_cast<uint32_t*>(attrs), diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index aefec6cbc2cb..b11f22a030d9 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -28,6 +28,7 @@ #include <android_runtime/android_graphics_GraphicBuffer.h> #include <android_runtime/android_hardware_HardwareBuffer.h> #include <android_runtime/android_view_Surface.h> +#include <android_runtime/android_view_SurfaceControl.h> #include <android_runtime/android_view_SurfaceSession.h> #include <gui/ISurfaceComposer.h> #include <gui/Surface.h> @@ -221,8 +222,14 @@ static struct { static struct { jclass clazz; + jfieldID mNativeObject; +} gTransactionClassInfo; + +static struct { + jclass clazz; + jfieldID mNativeObject; jmethodID invokeReleaseCallback; -} gInvokeReleaseCallback; +} gSurfaceControlClassInfo; constexpr ui::Dataspace pickDataspaceFromColorMode(const ui::ColorMode colorMode) { switch (colorMode) { @@ -511,10 +518,11 @@ static ReleaseBufferCallback genReleaseCallback(JNIEnv* env, jobject releaseCall if (fenceCopy) { fenceCopy->incStrong(0); } - globalCallbackRef->env()->CallStaticVoidMethod(gInvokeReleaseCallback.clazz, - gInvokeReleaseCallback.invokeReleaseCallback, - globalCallbackRef->object(), - reinterpret_cast<jlong>(fenceCopy)); + globalCallbackRef->env() + ->CallStaticVoidMethod(gSurfaceControlClassInfo.clazz, + gSurfaceControlClassInfo.invokeReleaseCallback, + globalCallbackRef->object(), + reinterpret_cast<jlong>(fenceCopy)); }; } @@ -1520,27 +1528,6 @@ static void nativeReparent(JNIEnv* env, jclass clazz, jlong transactionObj, transaction->reparent(ctrl, newParent); } -static void nativeOverrideHdrTypes(JNIEnv* env, jclass clazz, jobject tokenObject, - jintArray jHdrTypes) { - sp<IBinder> token(ibinderForJavaObject(env, tokenObject)); - if (token == nullptr || jHdrTypes == nullptr) return; - - int* hdrTypes = env->GetIntArrayElements(jHdrTypes, 0); - int numHdrTypes = env->GetArrayLength(jHdrTypes); - - std::vector<ui::Hdr> hdrTypesVector; - for (int i = 0; i < numHdrTypes; i++) { - hdrTypesVector.push_back(static_cast<ui::Hdr>(hdrTypes[i])); - } - env->ReleaseIntArrayElements(jHdrTypes, hdrTypes, 0); - - status_t error = SurfaceComposerClient::overrideHdrTypes(token, hdrTypesVector); - if (error != NO_ERROR) { - jniThrowExceptionFmt(env, "java/lang/SecurityException", - "ACCESS_SURFACE_FLINGER is missing"); - } -} - static jboolean nativeGetBootDisplayModeSupport(JNIEnv* env, jclass clazz) { bool isBootDisplayModeSupported = false; SurfaceComposerClient::getBootDisplayModeSupport(&isBootDisplayModeSupported); @@ -1906,6 +1893,28 @@ static jobject nativeGetDefaultApplyToken(JNIEnv* env, jclass clazz) { // ---------------------------------------------------------------------------- +SurfaceControl* android_view_SurfaceControl_getNativeSurfaceControl(JNIEnv* env, + jobject surfaceControlObj) { + if (!!surfaceControlObj && + env->IsInstanceOf(surfaceControlObj, gSurfaceControlClassInfo.clazz)) { + return reinterpret_cast<SurfaceControl*>( + env->GetLongField(surfaceControlObj, gSurfaceControlClassInfo.mNativeObject)); + } else { + return nullptr; + } +} + +SurfaceComposerClient::Transaction* android_view_SurfaceTransaction_getNativeSurfaceTransaction( + JNIEnv* env, jobject surfaceTransactionObj) { + if (!!surfaceTransactionObj && + env->IsInstanceOf(surfaceTransactionObj, gTransactionClassInfo.clazz)) { + return reinterpret_cast<SurfaceComposerClient::Transaction*>( + env->GetLongField(surfaceTransactionObj, gTransactionClassInfo.mNativeObject)); + } else { + return nullptr; + } +} + static const JNINativeMethod sSurfaceControlMethods[] = { // clang-format off {"nativeCreate", "(Landroid/view/SurfaceSession;Ljava/lang/String;IIIIJLandroid/os/Parcel;)J", @@ -2026,8 +2035,6 @@ static const JNINativeMethod sSurfaceControlMethods[] = { (void*)nativeSetGameContentType }, {"nativeGetCompositionDataspaces", "()[I", (void*)nativeGetCompositionDataspaces}, - {"nativeOverrideHdrTypes", "(Landroid/os/IBinder;[I)V", - (void*)nativeOverrideHdrTypes }, {"nativeClearContentFrameStats", "(J)Z", (void*)nativeClearContentFrameStats }, {"nativeGetContentFrameStats", "(JLandroid/view/WindowContentFrameStats;)Z", @@ -2306,11 +2313,18 @@ int register_android_view_SurfaceControl(JNIEnv* env) GetFieldIDOrDie(env, displayDecorationSupportClazz, "alphaInterpretation", "I"); jclass surfaceControlClazz = FindClassOrDie(env, "android/view/SurfaceControl"); - gInvokeReleaseCallback.clazz = MakeGlobalRefOrDie(env, surfaceControlClazz); - gInvokeReleaseCallback.invokeReleaseCallback = + gSurfaceControlClassInfo.clazz = MakeGlobalRefOrDie(env, surfaceControlClazz); + gSurfaceControlClassInfo.mNativeObject = + GetFieldIDOrDie(env, gSurfaceControlClassInfo.clazz, "mNativeObject", "J"); + gSurfaceControlClassInfo.invokeReleaseCallback = GetStaticMethodIDOrDie(env, surfaceControlClazz, "invokeReleaseCallback", "(Ljava/util/function/Consumer;J)V"); + jclass surfaceTransactionClazz = FindClassOrDie(env, "android/view/SurfaceControl$Transaction"); + gTransactionClassInfo.clazz = MakeGlobalRefOrDie(env, surfaceTransactionClazz); + gTransactionClassInfo.mNativeObject = + GetFieldIDOrDie(env, gTransactionClassInfo.clazz, "mNativeObject", "J"); + return err; } diff --git a/core/jni/android_window_ScreenCapture.cpp b/core/jni/android_window_ScreenCapture.cpp index c1929c6535fb..3bada15aa834 100644 --- a/core/jni/android_window_ScreenCapture.cpp +++ b/core/jni/android_window_ScreenCapture.cpp @@ -61,8 +61,9 @@ static struct { } gLayerCaptureArgsClassInfo; static struct { - jmethodID accept; -} gConsumerClassInfo; + jclass clazz; + jmethodID onScreenCaptureComplete; +} gScreenCaptureListenerClassInfo; static struct { jclass clazz; @@ -97,14 +98,14 @@ class ScreenCaptureListenerWrapper : public gui::BnScreenCaptureListener { public: explicit ScreenCaptureListenerWrapper(JNIEnv* env, jobject jobject) { env->GetJavaVM(&mVm); - mConsumerObject = env->NewGlobalRef(jobject); - LOG_ALWAYS_FATAL_IF(!mConsumerObject, "Failed to make global ref"); + mScreenCaptureListenerObject = env->NewGlobalRef(jobject); + LOG_ALWAYS_FATAL_IF(!mScreenCaptureListenerObject, "Failed to make global ref"); } ~ScreenCaptureListenerWrapper() { - if (mConsumerObject) { - getenv()->DeleteGlobalRef(mConsumerObject); - mConsumerObject = nullptr; + if (mScreenCaptureListenerObject) { + getenv()->DeleteGlobalRef(mScreenCaptureListenerObject); + mScreenCaptureListenerObject = nullptr; } } @@ -112,8 +113,9 @@ public: const gui::ScreenCaptureResults& captureResults) override { JNIEnv* env = getenv(); if (!captureResults.fenceResult.ok() || captureResults.buffer == nullptr) { - env->CallVoidMethod(mConsumerObject, gConsumerClassInfo.accept, nullptr); - checkAndClearException(env, "accept"); + env->CallVoidMethod(mScreenCaptureListenerObject, + gScreenCaptureListenerClassInfo.onScreenCaptureComplete, nullptr); + checkAndClearException(env, "onScreenCaptureComplete"); return binder::Status::ok(); } captureResults.fenceResult.value()->waitForever(LOG_TAG); @@ -128,15 +130,17 @@ public: captureResults.capturedSecureLayers, captureResults.capturedHdrLayers); checkAndClearException(env, "builder"); - env->CallVoidMethod(mConsumerObject, gConsumerClassInfo.accept, screenshotHardwareBuffer); - checkAndClearException(env, "accept"); + env->CallVoidMethod(mScreenCaptureListenerObject, + gScreenCaptureListenerClassInfo.onScreenCaptureComplete, + screenshotHardwareBuffer); + checkAndClearException(env, "onScreenCaptureComplete"); env->DeleteLocalRef(jhardwareBuffer); env->DeleteLocalRef(screenshotHardwareBuffer); return binder::Status::ok(); } private: - jobject mConsumerObject; + jobject mScreenCaptureListenerObject; JavaVM* mVm; JNIEnv* getenv() { @@ -190,7 +194,7 @@ static DisplayCaptureArgs displayCaptureArgsFromObject(JNIEnv* env, } static jint nativeCaptureDisplay(JNIEnv* env, jclass clazz, jobject displayCaptureArgsObject, - jlong screenCaptureListenerObject) { + jobject screenCaptureListenerObject) { const DisplayCaptureArgs captureArgs = displayCaptureArgsFromObject(env, displayCaptureArgsObject); @@ -198,13 +202,13 @@ static jint nativeCaptureDisplay(JNIEnv* env, jclass clazz, jobject displayCaptu return BAD_VALUE; } - sp<gui::IScreenCaptureListener> captureListener = - reinterpret_cast<gui::IScreenCaptureListener*>(screenCaptureListenerObject); + sp<IScreenCaptureListener> captureListener = + sp<ScreenCaptureListenerWrapper>::make(env, screenCaptureListenerObject); return ScreenshotClient::captureDisplay(captureArgs, captureListener); } static jint nativeCaptureLayers(JNIEnv* env, jclass clazz, jobject layerCaptureArgsObject, - jlong screenCaptureListenerObject) { + jobject screenCaptureListenerObject) { LayerCaptureArgs captureArgs; getCaptureArgs(env, layerCaptureArgsObject, captureArgs); SurfaceControl* layer = reinterpret_cast<SurfaceControl*>( @@ -234,70 +238,21 @@ static jint nativeCaptureLayers(JNIEnv* env, jclass clazz, jobject layerCaptureA } } - sp<gui::IScreenCaptureListener> captureListener = - reinterpret_cast<gui::IScreenCaptureListener*>(screenCaptureListenerObject); + sp<IScreenCaptureListener> captureListener = + sp<ScreenCaptureListenerWrapper>::make(env, screenCaptureListenerObject); return ScreenshotClient::captureLayers(captureArgs, captureListener); } -static jlong nativeCreateScreenCaptureListener(JNIEnv* env, jclass clazz, jobject consumerObj) { - sp<gui::IScreenCaptureListener> listener = - sp<ScreenCaptureListenerWrapper>::make(env, consumerObj); - listener->incStrong((void*)nativeCreateScreenCaptureListener); - return reinterpret_cast<jlong>(listener.get()); -} - -static void nativeWriteListenerToParcel(JNIEnv* env, jclass clazz, jlong nativeObject, - jobject parcelObj) { - Parcel* parcel = parcelForJavaObject(env, parcelObj); - if (parcel == NULL) { - jniThrowNullPointerException(env, NULL); - return; - } - ScreenCaptureListenerWrapper* const self = - reinterpret_cast<ScreenCaptureListenerWrapper*>(nativeObject); - if (self != nullptr) { - parcel->writeStrongBinder(IInterface::asBinder(self)); - } -} - -static jlong nativeReadListenerFromParcel(JNIEnv* env, jclass clazz, jobject parcelObj) { - Parcel* parcel = parcelForJavaObject(env, parcelObj); - if (parcel == NULL) { - jniThrowNullPointerException(env, NULL); - return 0; - } - sp<gui::IScreenCaptureListener> listener = - interface_cast<gui::IScreenCaptureListener>(parcel->readStrongBinder()); - if (listener == nullptr) { - return 0; - } - listener->incStrong((void*)nativeCreateScreenCaptureListener); - return reinterpret_cast<jlong>(listener.get()); -} - -void destroyNativeListener(void* ptr) { - ScreenCaptureListenerWrapper* listener = reinterpret_cast<ScreenCaptureListenerWrapper*>(ptr); - listener->decStrong((void*)nativeCreateScreenCaptureListener); -} - -static jlong getNativeListenerFinalizer(JNIEnv* env, jclass clazz) { - return static_cast<jlong>(reinterpret_cast<uintptr_t>(&destroyNativeListener)); -} - // ---------------------------------------------------------------------------- static const JNINativeMethod sScreenCaptureMethods[] = { // clang-format off - {"nativeCaptureDisplay", "(Landroid/window/ScreenCapture$DisplayCaptureArgs;J)I", + {"nativeCaptureDisplay", + "(Landroid/window/ScreenCapture$DisplayCaptureArgs;Landroid/window/ScreenCapture$ScreenCaptureListener;)I", (void*)nativeCaptureDisplay }, - {"nativeCaptureLayers", "(Landroid/window/ScreenCapture$LayerCaptureArgs;J)I", + {"nativeCaptureLayers", + "(Landroid/window/ScreenCapture$LayerCaptureArgs;Landroid/window/ScreenCapture$ScreenCaptureListener;)I", (void*)nativeCaptureLayers }, - {"nativeCreateScreenCaptureListener", "(Ljava/util/function/Consumer;)J", - (void*)nativeCreateScreenCaptureListener }, - {"nativeWriteListenerToParcel", "(JLandroid/os/Parcel;)V", (void*)nativeWriteListenerToParcel }, - {"nativeReadListenerFromParcel", "(Landroid/os/Parcel;)J", - (void*)nativeReadListenerFromParcel }, - {"getNativeListenerFinalizer", "()J", (void*)getNativeListenerFinalizer }, // clang-format on }; @@ -338,8 +293,12 @@ int register_android_window_ScreenCapture(JNIEnv* env) { gLayerCaptureArgsClassInfo.childrenOnly = GetFieldIDOrDie(env, layerCaptureArgsClazz, "mChildrenOnly", "Z"); - jclass consumer = FindClassOrDie(env, "java/util/function/Consumer"); - gConsumerClassInfo.accept = GetMethodIDOrDie(env, consumer, "accept", "(Ljava/lang/Object;)V"); + jclass screenCaptureListenerClazz = + FindClassOrDie(env, "android/window/ScreenCapture$ScreenCaptureListener"); + gScreenCaptureListenerClassInfo.clazz = MakeGlobalRefOrDie(env, screenCaptureListenerClazz); + gScreenCaptureListenerClassInfo.onScreenCaptureComplete = + GetMethodIDOrDie(env, screenCaptureListenerClazz, "onScreenCaptureComplete", + "(Landroid/window/ScreenCapture$ScreenshotHardwareBuffer;)V"); jclass screenshotGraphicsBufferClazz = FindClassOrDie(env, "android/window/ScreenCapture$ScreenshotHardwareBuffer"); diff --git a/core/jni/include/android_runtime/android_view_SurfaceControl.h b/core/jni/include/android_runtime/android_view_SurfaceControl.h new file mode 100644 index 000000000000..10a754903208 --- /dev/null +++ b/core/jni/include/android_runtime/android_view_SurfaceControl.h @@ -0,0 +1,38 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _ANDROID_VIEW_SURFACECONTROL_H +#define _ANDROID_VIEW_SURFACECONTROL_H + +#include <gui/SurfaceComposerClient.h> +#include <gui/SurfaceControl.h> + +#include "jni.h" + +namespace android { + +/* Gets the underlying native SurfaceControl for a java SurfaceControl. */ +extern SurfaceControl* android_view_SurfaceControl_getNativeSurfaceControl( + JNIEnv* env, jobject surfaceControlObj); + +/* Gets the underlying native SurfaceControl for a java SurfaceControl. */ +extern SurfaceComposerClient::Transaction* +android_view_SurfaceTransaction_getNativeSurfaceTransaction(JNIEnv* env, + jobject surfaceTransactionObj); + +} // namespace android + +#endif // _ANDROID_VIEW_SURFACECONTROL_H diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1d2ce7eb37c3..060f440c9c4f 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1662,13 +1662,18 @@ darkening hysteresis constraint value is the n-th element of config_screenDarkeningThresholds. + Historically, it has been assumed that this will be an integer array with values in the + range of [0, 255]. However, it is now assumed to be a float array with values in the + range of [0, 1]. To accommodate both the possibilities, we internally check the scale on + which the thresholds are defined, and calibrate it accordingly. + The (zero-based) index is calculated as follows: (MAX is the largest index of the array) condition calculated index value < level[0] 0 level[n] <= value < level[n+1] n+1 level[MAX] <= value MAX+1 --> - <integer-array name="config_screenThresholdLevels"> - </integer-array> + <array name="config_screenThresholdLevels"> + </array> <!-- Array of hysteresis constraint values for brightening, represented as tenths of a percent. The length of this array is assumed to be one greater than diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java index 4c247427ef8f..9e39e13265bd 100644 --- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java +++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java @@ -16,11 +16,12 @@ package android.hardware.devicestate; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -36,7 +37,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.mockito.Mockito; import java.util.HashSet; import java.util.Set; @@ -59,6 +59,7 @@ public final class DeviceStateManagerGlobalTest { public void setUp() { mService = new TestDeviceStateManagerService(); mDeviceStateManagerGlobal = new DeviceStateManagerGlobal(mService); + assertFalse(mService.mCallbacks.isEmpty()); } @Test @@ -79,8 +80,8 @@ public final class DeviceStateManagerGlobalTest { verify(callback2).onBaseStateChanged(eq(mService.getBaseState())); verify(callback2).onStateChanged(eq(mService.getMergedState())); - Mockito.reset(callback1); - Mockito.reset(callback2); + reset(callback1); + reset(callback2); // Change the supported states and verify callback mService.setSupportedStates(new int[]{ DEFAULT_DEVICE_STATE }); @@ -88,8 +89,8 @@ public final class DeviceStateManagerGlobalTest { verify(callback2).onSupportedStatesChanged(eq(mService.getSupportedStates())); mService.setSupportedStates(new int[]{ DEFAULT_DEVICE_STATE, OTHER_DEVICE_STATE }); - Mockito.reset(callback1); - Mockito.reset(callback2); + reset(callback1); + reset(callback2); // Change the base state and verify callback mService.setBaseState(OTHER_DEVICE_STATE); @@ -98,8 +99,8 @@ public final class DeviceStateManagerGlobalTest { verify(callback2).onBaseStateChanged(eq(mService.getBaseState())); verify(callback2).onStateChanged(eq(mService.getMergedState())); - Mockito.reset(callback1); - Mockito.reset(callback2); + reset(callback1); + reset(callback2); // Change the requested state and verify callback DeviceStateRequest request = DeviceStateRequest.newBuilder(DEFAULT_DEVICE_STATE).build(); @@ -120,7 +121,7 @@ public final class DeviceStateManagerGlobalTest { verify(callback).onSupportedStatesChanged(eq(mService.getSupportedStates())); verify(callback).onBaseStateChanged(eq(mService.getBaseState())); verify(callback).onStateChanged(eq(mService.getMergedState())); - Mockito.reset(callback); + reset(callback); mDeviceStateManagerGlobal.unregisterDeviceStateCallback(callback); @@ -130,33 +131,86 @@ public final class DeviceStateManagerGlobalTest { } @Test - public void submittingRequestRegistersCallback() { - assertTrue(mService.mCallbacks.isEmpty()); + public void submitRequest() { + DeviceStateCallback callback = mock(DeviceStateCallback.class); + mDeviceStateManagerGlobal.registerDeviceStateCallback(callback, + ConcurrentUtils.DIRECT_EXECUTOR); - DeviceStateRequest request = DeviceStateRequest.newBuilder(DEFAULT_DEVICE_STATE).build(); + verify(callback).onStateChanged(eq(mService.getBaseState())); + reset(callback); + + DeviceStateRequest request = DeviceStateRequest.newBuilder(OTHER_DEVICE_STATE).build(); mDeviceStateManagerGlobal.requestState(request, null /* executor */, null /* callback */); - assertFalse(mService.mCallbacks.isEmpty()); + verify(callback).onStateChanged(eq(OTHER_DEVICE_STATE)); + reset(callback); + + mDeviceStateManagerGlobal.cancelStateRequest(); + + verify(callback).onStateChanged(eq(mService.getBaseState())); } @Test - public void submitRequest() { + public void submitBaseStateOverrideRequest() { DeviceStateCallback callback = mock(DeviceStateCallback.class); mDeviceStateManagerGlobal.registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR); + verify(callback).onBaseStateChanged(eq(mService.getBaseState())); verify(callback).onStateChanged(eq(mService.getBaseState())); - Mockito.reset(callback); + reset(callback); DeviceStateRequest request = DeviceStateRequest.newBuilder(OTHER_DEVICE_STATE).build(); - mDeviceStateManagerGlobal.requestState(request, null /* executor */, null /* callback */); + mDeviceStateManagerGlobal.requestBaseStateOverride(request, null /* executor */, + null /* callback */); + verify(callback).onBaseStateChanged(eq(OTHER_DEVICE_STATE)); verify(callback).onStateChanged(eq(OTHER_DEVICE_STATE)); - Mockito.reset(callback); + reset(callback); - mDeviceStateManagerGlobal.cancelStateRequest(); + mDeviceStateManagerGlobal.cancelBaseStateOverride(); + + verify(callback).onBaseStateChanged(eq(mService.getBaseState())); + verify(callback).onStateChanged(eq(mService.getBaseState())); + } + + @Test + public void submitBaseAndEmulatedStateOverride() { + DeviceStateCallback callback = mock(DeviceStateCallback.class); + mDeviceStateManagerGlobal.registerDeviceStateCallback(callback, + ConcurrentUtils.DIRECT_EXECUTOR); + verify(callback).onBaseStateChanged(eq(mService.getBaseState())); verify(callback).onStateChanged(eq(mService.getBaseState())); + reset(callback); + + DeviceStateRequest request = DeviceStateRequest.newBuilder(OTHER_DEVICE_STATE).build(); + mDeviceStateManagerGlobal.requestBaseStateOverride(request, null /* executor */, + null /* callback */); + + verify(callback).onBaseStateChanged(eq(OTHER_DEVICE_STATE)); + verify(callback).onStateChanged(eq(OTHER_DEVICE_STATE)); + assertEquals(OTHER_DEVICE_STATE, mService.getBaseState()); + reset(callback); + + DeviceStateRequest secondRequest = DeviceStateRequest.newBuilder( + DEFAULT_DEVICE_STATE).build(); + + mDeviceStateManagerGlobal.requestState(secondRequest, null, null); + + assertEquals(OTHER_DEVICE_STATE, mService.getBaseState()); + verify(callback).onStateChanged(eq(DEFAULT_DEVICE_STATE)); + reset(callback); + + mDeviceStateManagerGlobal.cancelStateRequest(); + + verify(callback).onStateChanged(OTHER_DEVICE_STATE); + reset(callback); + + mDeviceStateManagerGlobal.cancelBaseStateOverride(); + + verify(callback).onBaseStateChanged(DEFAULT_DEVICE_STATE); + verify(callback).onStateChanged(DEFAULT_DEVICE_STATE); } @Test @@ -169,7 +223,7 @@ public final class DeviceStateManagerGlobalTest { callback /* callback */); verify(callback).onRequestActivated(eq(request)); - Mockito.reset(callback); + reset(callback); mDeviceStateManagerGlobal.cancelStateRequest(); @@ -203,13 +257,16 @@ public final class DeviceStateManagerGlobalTest { private int[] mSupportedStates = new int[] { DEFAULT_DEVICE_STATE, OTHER_DEVICE_STATE }; private int mBaseState = DEFAULT_DEVICE_STATE; private Request mRequest; + private Request mBaseStateRequest; private Set<IDeviceStateManagerCallback> mCallbacks = new HashSet<>(); private DeviceStateInfo getInfo() { + final int mergedBaseState = mBaseStateRequest == null + ? mBaseState : mBaseStateRequest.state; final int mergedState = mRequest == null - ? mBaseState : mRequest.state; - return new DeviceStateInfo(mSupportedStates, mBaseState, mergedState); + ? mergedBaseState : mRequest.state; + return new DeviceStateInfo(mSupportedStates, mergedBaseState, mergedState); } private void notifyDeviceStateInfoChanged() { @@ -238,7 +295,7 @@ public final class DeviceStateManagerGlobalTest { try { callback.onDeviceStateInfoChanged(getInfo()); } catch (RemoteException e) { - // Do nothing. Should never happen. + e.rethrowFromSystemServer(); } } @@ -249,7 +306,7 @@ public final class DeviceStateManagerGlobalTest { try { callback.onRequestCanceled(mRequest.token); } catch (RemoteException e) { - // Do nothing. Should never happen. + e.rethrowFromSystemServer(); } } } @@ -262,7 +319,7 @@ public final class DeviceStateManagerGlobalTest { try { callback.onRequestActive(token); } catch (RemoteException e) { - // Do nothing. Should never happen. + e.rethrowFromSystemServer(); } } } @@ -275,7 +332,46 @@ public final class DeviceStateManagerGlobalTest { try { callback.onRequestCanceled(token); } catch (RemoteException e) { - // Do nothing. Should never happen. + e.rethrowFromSystemServer(); + } + } + notifyDeviceStateInfoChanged(); + } + + @Override + public void requestBaseStateOverride(IBinder token, int state, int flags) { + if (mBaseStateRequest != null) { + for (IDeviceStateManagerCallback callback : mCallbacks) { + try { + callback.onRequestCanceled(mBaseStateRequest.token); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + } + + final Request request = new Request(token, state, flags); + mBaseStateRequest = request; + notifyDeviceStateInfoChanged(); + + for (IDeviceStateManagerCallback callback : mCallbacks) { + try { + callback.onRequestActive(token); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + } + + @Override + public void cancelBaseStateOverride() throws RemoteException { + IBinder token = mBaseStateRequest.token; + mBaseStateRequest = null; + for (IDeviceStateManagerCallback callback : mCallbacks) { + try { + callback.onRequestCanceled(token); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); } } notifyDeviceStateInfoChanged(); @@ -296,7 +392,7 @@ public final class DeviceStateManagerGlobalTest { } public int getBaseState() { - return mBaseState; + return getInfo().baseState; } public int getMergedState() { diff --git a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java index 940ca96fac4f..4679a9ea6f66 100644 --- a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java +++ b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java @@ -19,14 +19,15 @@ package com.android.internal.util; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; -import static org.junit.Assert.assertEquals; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -51,8 +52,11 @@ import com.android.internal.widget.IWeakEscrowTokenActivatedListener; import com.android.internal.widget.IWeakEscrowTokenRemovedListener; import com.android.internal.widget.LockPatternUtils; +import com.google.android.collect.Lists; + import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import java.nio.charset.StandardCharsets; @@ -172,13 +176,61 @@ public class LockPatternUtilsTest { } @Test - public void testGetEnabledTrustAgentsNotNull() throws RemoteException { + public void testSetEnabledTrustAgents() throws RemoteException { int testUserId = 10; ILockSettings ils = createTestLockSettings(); - when(ils.getString(anyString(), any(), anyInt())).thenReturn(""); + ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class); + doNothing().when(ils).setString(anyString(), valueCaptor.capture(), anyInt()); + List<ComponentName> enabledTrustAgents = Lists.newArrayList( + ComponentName.unflattenFromString("com.android/.TrustAgent"), + ComponentName.unflattenFromString("com.test/.TestAgent")); + + mLockPatternUtils.setEnabledTrustAgents(enabledTrustAgents, testUserId); + + assertThat(valueCaptor.getValue()).isEqualTo("com.android/.TrustAgent,com.test/.TestAgent"); + } + + @Test + public void testGetEnabledTrustAgents() throws RemoteException { + int testUserId = 10; + ILockSettings ils = createTestLockSettings(); + when(ils.getString(anyString(), any(), anyInt())).thenReturn( + "com.android/.TrustAgent,com.test/.TestAgent"); + List<ComponentName> trustAgents = mLockPatternUtils.getEnabledTrustAgents(testUserId); - assertNotNull(trustAgents); - assertEquals(0, trustAgents.size()); + + assertThat(trustAgents).containsExactly( + ComponentName.unflattenFromString("com.android/.TrustAgent"), + ComponentName.unflattenFromString("com.test/.TestAgent")); + } + + @Test + public void testSetKnownTrustAgents() throws RemoteException { + int testUserId = 10; + ILockSettings ils = createTestLockSettings(); + ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class); + doNothing().when(ils).setString(anyString(), valueCaptor.capture(), anyInt()); + List<ComponentName> knownTrustAgents = Lists.newArrayList( + ComponentName.unflattenFromString("com.android/.TrustAgent"), + ComponentName.unflattenFromString("com.test/.TestAgent")); + + mLockPatternUtils.setKnownTrustAgents(knownTrustAgents, testUserId); + + assertThat(valueCaptor.getValue()).isEqualTo("com.android/.TrustAgent,com.test/.TestAgent"); + } + + @Test + public void testGetKnownTrustAgents() throws RemoteException { + int testUserId = 10; + ILockSettings ils = createTestLockSettings(); + when(ils.getString(anyString(), any(), anyInt())).thenReturn( + "com.android/.TrustAgent,com.test/.TestAgent"); + + List<ComponentName> trustAgents = mLockPatternUtils.getKnownTrustAgents(testUserId); + + assertThat(trustAgents).containsExactly( + ComponentName.unflattenFromString("com.android/.TrustAgent"), + ComponentName.unflattenFromString("com.test/.TestAgent")); } private ILockSettings createTestLockSettings() { diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 682483d9ce63..4f9af1290767 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -373,12 +373,6 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/TransitionController.java" }, - "-1750384749": { - "message": "Launch on display check: allow launch on public display", - "level": "DEBUG", - "group": "WM_DEBUG_TASKS", - "at": "com\/android\/server\/wm\/ActivityTaskSupervisor.java" - }, "-1750206390": { "message": "Exception thrown when creating surface for client %s (%s). %s", "level": "WARN", @@ -907,6 +901,12 @@ "group": "WM_DEBUG_BOOT", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-1253056469": { + "message": "Launch on display check: %s launch for userId=%d on displayId=%d", + "level": "DEBUG", + "group": "WM_DEBUG_TASKS", + "at": "com\/android\/server\/wm\/ActivityTaskSupervisor.java" + }, "-1248645819": { "message": "\tAdd container=%s", "level": "DEBUG", diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index 7e9c4189dabb..fb0a9db6a20b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -41,7 +41,7 @@ public class WindowExtensionsImpl implements WindowExtensions { // TODO(b/241126279) Introduce constants to better version functionality @Override public int getVendorApiLevel() { - return 2; + return 1; } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index febd7917dff9..74303e2fab7c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -31,6 +31,7 @@ import android.window.TaskFragmentOrganizer; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -93,6 +94,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { } /** No longer overrides the animation if the transition is on the given Task. */ + @GuardedBy("mLock") void stopOverrideSplitAnimation(int taskId) { if (mAnimationController != null) { mAnimationController.unregisterRemoteAnimations(taskId); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java index c8ac0fc73ff9..00be5a6e3416 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -17,8 +17,10 @@ package androidx.window.extensions.embedding; import android.app.Activity; +import android.content.res.Configuration; import android.util.Pair; import android.util.Size; +import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; @@ -32,14 +34,18 @@ class SplitContainer { private final TaskFragmentContainer mSecondaryContainer; @NonNull private final SplitRule mSplitRule; + @NonNull + private SplitAttributes mSplitAttributes; SplitContainer(@NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, - @NonNull SplitRule splitRule) { + @NonNull SplitRule splitRule, + @NonNull SplitAttributes splitAttributes) { mPrimaryContainer = primaryContainer; mSecondaryContainer = secondaryContainer; mSplitRule = splitRule; + mSplitAttributes = splitAttributes; if (shouldFinishPrimaryWithSecondary(splitRule)) { if (mPrimaryContainer.getRunningActivityCount() == 1 @@ -72,6 +78,26 @@ class SplitContainer { return mSplitRule; } + @NonNull + SplitAttributes getSplitAttributes() { + return mSplitAttributes; + } + + /** + * Updates the {@link SplitAttributes} to this container. + * It is usually used when there's a folding state change or + * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction, int, + * Configuration)}. + */ + void setSplitAttributes(@NonNull SplitAttributes splitAttributes) { + mSplitAttributes = splitAttributes; + } + + @NonNull + TaskContainer getTaskContainer() { + return getPrimaryContainer().getTaskContainer(); + } + /** Returns the minimum dimension pair of primary container and secondary container. */ @NonNull Pair<Size, Size> getMinDimensionsPair() { @@ -141,6 +167,7 @@ class SplitContainer { + " primaryContainer=" + mPrimaryContainer + " secondaryContainer=" + mSecondaryContainer + " splitRule=" + mSplitRule + + " splitAttributes" + mSplitAttributes + "}"; } } 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 126f8350839c..203ece091e46 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -19,6 +19,7 @@ package androidx.window.extensions.embedding; import static android.app.ActivityManager.START_SUCCESS; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; @@ -40,12 +41,13 @@ import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAs 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.getNonEmbeddedActivityBounds; -import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; +import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit; import android.app.Activity; import android.app.ActivityClient; import android.app.ActivityOptions; import android.app.ActivityThread; +import android.app.Application; import android.app.Instrumentation; import android.content.ComponentName; import android.content.Context; @@ -62,19 +64,25 @@ import android.util.Log; import android.util.Pair; import android.util.Size; import android.util.SparseArray; +import android.view.WindowMetrics; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.window.common.CommonFoldingFeature; import androidx.window.common.EmptyLifecycleCallbacksAdapter; +import androidx.window.extensions.WindowExtensionsProvider; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -96,6 +104,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); /** + * A developer-defined {@link SplitAttributes} calculator to compute the current + * {@link SplitAttributes} with the current device and window states. + * It is registered via {@link #setSplitAttributesCalculator(SplitAttributesCalculator)} + * and unregistered via {@link #clearSplitAttributesCalculator()}. + * This is called when: + * <ul> + * <li>{@link SplitPresenter#updateSplitContainer(SplitContainer, TaskFragmentContainer, + * WindowContainerTransaction)}</li> + * <li>There's a started Activity which matches {@link SplitPairRule} </li> + * <li>Checking whether the place holder should be launched if there's a Activity matches + * {@link SplitPlaceholderRule} </li> + * </ul> + */ + @GuardedBy("mLock") + @Nullable + private SplitAttributesCalculator mSplitAttributesCalculator; + /** * Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info * below it. * When the app is host of multiple Tasks, there can be multiple splits controlled by the same @@ -105,26 +130,65 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>(); - // Callback to Jetpack to notify about changes to split states. - @NonNull + /** Callback to Jetpack to notify about changes to split states. */ + @Nullable private Consumer<List<SplitInfo>> mEmbeddingCallback; private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); private final Handler mHandler; final Object mLock = new Object(); private final ActivityStartMonitor mActivityStartMonitor; + @NonNull + final WindowLayoutComponentImpl mWindowLayoutComponent; public SplitController() { + this((WindowLayoutComponentImpl) Objects.requireNonNull(WindowExtensionsProvider + .getWindowExtensions().getWindowLayoutComponent())); + } + + @VisibleForTesting + SplitController(@NonNull WindowLayoutComponentImpl windowLayoutComponent) { final MainThreadExecutor executor = new MainThreadExecutor(); mHandler = executor.mHandler; mPresenter = new SplitPresenter(executor, this); - ActivityThread activityThread = ActivityThread.currentActivityThread(); + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + final Application application = activityThread.getApplication(); // Register a callback to be notified about activities being created. - activityThread.getApplication().registerActivityLifecycleCallbacks( - new LifecycleCallbacks()); + application.registerActivityLifecycleCallbacks(new LifecycleCallbacks()); // Intercept activity starts to route activities to new containers if necessary. Instrumentation instrumentation = activityThread.getInstrumentation(); + mActivityStartMonitor = new ActivityStartMonitor(); instrumentation.addMonitor(mActivityStartMonitor); + mWindowLayoutComponent = windowLayoutComponent; + mWindowLayoutComponent.addFoldingStateChangedCallback(new FoldingFeatureListener()); + } + + private class FoldingFeatureListener implements Consumer<List<CommonFoldingFeature>> { + @Override + public void accept(List<CommonFoldingFeature> foldingFeatures) { + synchronized (mLock) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + for (int i = 0; i < mTaskContainers.size(); i++) { + final TaskContainer taskContainer = mTaskContainers.valueAt(i); + if (!taskContainer.isVisible()) { + continue; + } + if (taskContainer.getDisplayId() != DEFAULT_DISPLAY) { + continue; + } + // TODO(b/238948678): Support reporting display features in all windowing modes. + if (taskContainer.isInMultiWindow()) { + continue; + } + if (taskContainer.isEmpty()) { + continue; + } + updateContainersInTask(wct, taskContainer); + updateAnimationOverride(taskContainer); + } + mPresenter.applyTransaction(wct); + } + } } /** Updates the embedding rules applied to future activity launches. */ @@ -141,12 +205,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator) { - // TODO: Implement this method + synchronized (mLock) { + mSplitAttributesCalculator = calculator; + } } @Override public void clearSplitAttributesCalculator() { - // TODO: Implement this method + synchronized (mLock) { + mSplitAttributesCalculator = null; + } + } + + @GuardedBy("mLock") + @Nullable + SplitAttributesCalculator getSplitAttributesCalculator() { + return mSplitAttributesCalculator; } @NonNull @@ -191,7 +265,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen onTaskFragmentVanished(wct, info); break; case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED: - onTaskFragmentParentInfoChanged(wct, taskId, change.getTaskConfiguration()); + onTaskFragmentParentInfoChanged(wct, taskId, + change.getTaskFragmentParentInfo()); break; case TYPE_TASK_FRAGMENT_ERROR: final Bundle errorBundle = change.getErrorBundle(); @@ -346,22 +421,33 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * * @param wct The {@link WindowContainerTransaction} to make any changes with if needed. * @param taskId Id of the parent Task that is changed. - * @param parentConfig Config of the parent Task. + * @param parentInfo {@link TaskFragmentParentInfo} of the parent Task. */ @VisibleForTesting @GuardedBy("mLock") void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct, - int taskId, @NonNull Configuration parentConfig) { - onTaskConfigurationChanged(taskId, parentConfig); - if (isInPictureInPicture(parentConfig)) { - // No need to update presentation in PIP until the Task exit PIP. - return; - } + int taskId, @NonNull TaskFragmentParentInfo parentInfo) { final TaskContainer taskContainer = getTaskContainer(taskId); if (taskContainer == null || taskContainer.isEmpty()) { Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId); return; } + taskContainer.updateTaskFragmentParentInfo(parentInfo); + if (!taskContainer.isVisible()) { + // Don't update containers if the task is not visible. We only update containers when + // parentInfo#isVisibleRequested is true. + return; + } + onTaskContainerInfoChanged(taskContainer, parentInfo.getConfiguration()); + if (isInPictureInPicture(parentInfo.getConfiguration())) { + // No need to update presentation in PIP until the Task exit PIP. + return; + } + updateContainersInTask(wct, taskContainer); + } + + private void updateContainersInTask(@NonNull WindowContainerTransaction wct, + @NonNull TaskContainer taskContainer) { // Update all TaskFragments in the Task. Make a copy of the list since some may be // removed on updating. final List<TaskFragmentContainer> containers = @@ -486,6 +572,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** Called on receiving {@link #onTaskFragmentVanished} for cleanup. */ + @GuardedBy("mLock") private void cleanupTaskFragment(@NonNull IBinder taskFragmentToken) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final TaskContainer taskContainer = mTaskContainers.valueAt(i); @@ -501,14 +588,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } - private void onTaskConfigurationChanged(int taskId, @NonNull Configuration config) { - final TaskContainer taskContainer = mTaskContainers.get(taskId); - if (taskContainer == null) { - return; - } + @GuardedBy("mLock") + private void onTaskContainerInfoChanged(@NonNull TaskContainer taskContainer, + @NonNull Configuration config) { final boolean wasInPip = taskContainer.isInPictureInPicture(); final boolean isInPIp = isInPictureInPicture(config); - taskContainer.setWindowingMode(config.windowConfiguration.getWindowingMode()); // We need to check the animation override when enter/exit PIP or has bounds changed. boolean shouldUpdateAnimationOverride = wasInPip != isInPIp; @@ -526,37 +610,49 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Updates if we should override transition animation. We only want to override if the Task * bounds is large enough for at least one split rule. */ + @GuardedBy("mLock") private void updateAnimationOverride(@NonNull TaskContainer taskContainer) { if (ENABLE_SHELL_TRANSITIONS) { // TODO(b/207070762): cleanup with legacy app transition // Animation will be handled by WM Shell with Shell transition enabled. return; } - if (!taskContainer.isTaskBoundsInitialized() - || !taskContainer.isWindowingModeInitialized()) { + if (!taskContainer.isTaskBoundsInitialized()) { // We don't know about the Task bounds/windowingMode yet. return; } - // We only want to override if it supports split. - if (supportSplit(taskContainer)) { + // We only want to override if the TaskContainer may show split. + if (mayShowSplit(taskContainer)) { mPresenter.startOverrideSplitAnimation(taskContainer.getTaskId()); } else { mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId()); } } - private boolean supportSplit(@NonNull TaskContainer taskContainer) { + /** Returns whether the given {@link TaskContainer} may show in split. */ + // Suppress GuardedBy warning because lint asks to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + private boolean mayShowSplit(@NonNull TaskContainer taskContainer) { // No split inside PIP. if (taskContainer.isInPictureInPicture()) { return false; } + // Always assume the TaskContainer if SplitAttributesCalculator is set + if (mSplitAttributesCalculator != null) { + return true; + } // Check if the parent container bounds can support any split rule. for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitRule)) { continue; } - if (shouldShowSideBySide(taskContainer.getTaskBounds(), (SplitRule) rule)) { + final SplitRule splitRule = (SplitRule) rule; + final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes( + taskContainer.getTaskProperties(), splitRule, null /* minDimensionsPair */); + if (shouldShowSplit(splitAttributes)) { return true; } } @@ -700,14 +796,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Starts an activity to side of the launchingActivity with the provided split config. */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(container.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") @GuardedBy("mLock") private void startActivityToSide(@NonNull WindowContainerTransaction wct, @NonNull Activity launchingActivity, @NonNull Intent intent, @Nullable Bundle options, @NonNull SplitRule sideRule, - @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) { + @NonNull SplitAttributes splitAttributes, @Nullable Consumer<Exception> failureCallback, + boolean isPlaceholder) { try { mPresenter.startActivityToSide(wct, launchingActivity, intent, options, sideRule, - isPlaceholder); + splitAttributes, isPlaceholder); } catch (Exception e) { if (failureCallback != null) { failureCallback.accept(e); @@ -734,6 +834,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** Whether the given new launched activity is in a split with a rule matched. */ + // Suppress GuardedBy warning because lint asks to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") private boolean isNewActivityInSplitWithRuleMatched(@NonNull Activity launchedActivity) { final TaskFragmentContainer container = getContainerWithActivity(launchedActivity); final SplitContainer splitContainer = getActiveSplitForContainer(container); @@ -827,8 +931,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TaskFragmentContainer primaryContainer = getContainerWithActivity( primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); + final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity); if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() - && canReuseContainer(splitRule, splitContainer.getSplitRule())) { + && canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics)) { // Can launch in the existing secondary container if the rules share the same // presentation. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); @@ -958,6 +1063,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @VisibleForTesting @Nullable + @GuardedBy("mLock") TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { /* @@ -1020,6 +1126,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Returns an empty expanded {@link TaskFragmentContainer} that we can launch an activity into. */ + @GuardedBy("mLock") @Nullable private TaskFragmentContainer createEmptyExpandedContainer( @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId, @@ -1061,8 +1168,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer); + final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity); if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() - && (canReuseContainer(splitRule, splitContainer.getSplitRule()) + && (canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics) // TODO(b/231845476) we should always respect clearTop. || !respectClearTop) && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, @@ -1101,12 +1209,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return newContainer(pendingAppearedActivity, pendingAppearedActivity, taskId); } + @GuardedBy("mLock") TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, @NonNull Activity activityInTask, int taskId) { return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, activityInTask, taskId); } + @GuardedBy("mLock") TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, @@ -1130,7 +1240,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen throw new IllegalArgumentException("activityInTask must not be null,"); } if (!mTaskContainers.contains(taskId)) { - mTaskContainers.put(taskId, new TaskContainer(taskId)); + mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask)); } final TaskContainer taskContainer = mTaskContainers.get(taskId); final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, @@ -1142,10 +1252,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen Log.w(TAG, "Can't find bounds from activity=" + activityInTask); } } - if (!taskContainer.isWindowingModeInitialized()) { - taskContainer.setWindowingMode(activityInTask.getResources().getConfiguration() - .windowConfiguration.getWindowingMode()); - } updateAnimationOverride(taskContainer); return container; } @@ -1154,12 +1260,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Creates and registers a new split with the provided containers and configuration. Finishes * existing secondary containers if found for the given primary container. */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") void registerSplit(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, - @NonNull SplitRule splitRule) { + @NonNull SplitRule splitRule, @NonNull SplitAttributes splitAttributes) { final SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity, - secondaryContainer, splitRule); + secondaryContainer, splitRule, splitAttributes); // Remove container later to prevent pinning escaping toast showing in lock task mode. if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) { removeExistingSecondaryContainers(wct, primaryContainer); @@ -1310,6 +1420,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Skip position update - one or both containers are finished. return; } + final TaskContainer taskContainer = splitContainer.getTaskContainer(); + final SplitRule splitRule = splitContainer.getSplitRule(); + final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair(); + final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes( + taskContainer.getTaskProperties(), splitRule, minDimensionsPair); + splitContainer.setSplitAttributes(splitAttributes); if (dismissPlaceholderIfNecessary(wct, splitContainer)) { // Placeholder was finished, the positions will be updated when its container is emptied return; @@ -1383,6 +1499,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return launchPlaceholderIfNecessary(wct, topActivity, false /* isOnCreated */); } + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") @GuardedBy("mLock") boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, @NonNull Activity activity, boolean isOnCreated) { @@ -1409,18 +1528,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } + final TaskContainer.TaskProperties taskProperties = mPresenter.getTaskProperties(activity); final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity, placeholderRule.getPlaceholderIntent()); - if (!shouldShowSideBySide( - mPresenter.getParentContainerBounds(activity), placeholderRule, - minDimensionsPair)) { + final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(taskProperties, + placeholderRule, minDimensionsPair); + if (!SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } // TODO(b/190433398): Handle failed request final Bundle options = getPlaceholderOptions(activity, isOnCreated); startActivityToSide(wct, activity, placeholderRule.getPlaceholderIntent(), options, - placeholderRule, null /* failureCallback */, true /* isPlaceholder */); + placeholderRule, splitAttributes, null /* failureCallback */, + true /* isPlaceholder */); return true; } @@ -1445,6 +1566,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return options.toBundle(); } + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") @VisibleForTesting @GuardedBy("mLock") boolean dismissPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct, @@ -1457,11 +1581,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // The placeholder should remain after it was first shown. return false; } - - if (shouldShowSideBySide(splitContainer)) { + final SplitAttributes splitAttributes = splitContainer.getSplitAttributes(); + if (SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } - mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(), false /* shouldFinishDependent */); return true; @@ -1471,6 +1594,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns the rule to launch a placeholder for the activity with the provided component name * if it is configured in the split config. */ + @GuardedBy("mLock") private SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) { for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPlaceholderRule)) { @@ -1487,6 +1611,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Notifies listeners about changes to split states if necessary. */ + @GuardedBy("mLock") private void updateCallbackIfNecessary() { if (mEmbeddingCallback == null) { return; @@ -1508,6 +1633,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * null, that indicates that the active split states are in an intermediate state and should * not be reported. */ + @GuardedBy("mLock") @Nullable private List<SplitInfo> getActiveSplitStates() { List<SplitInfo> splitStates = new ArrayList<>(); @@ -1526,20 +1652,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen .toActivityStack(); final ActivityStack secondaryContainer = container.getSecondaryContainer() .toActivityStack(); - final SplitAttributes.SplitType splitType = shouldShowSideBySide(container) - ? new SplitAttributes.SplitType.RatioSplitType( - container.getSplitRule().getSplitRatio()) - : new SplitAttributes.SplitType.ExpandContainersSplitType(); final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer, - // Splits that are not showing side-by-side are reported as having 0 split - // ratio, since by definition in the API the primary container occupies no - // width of the split when covered by the secondary. - // TODO(b/241042437): use v2 APIs for splitAttributes - new SplitAttributes.Builder() - .setSplitType(splitType) - .setLayoutDirection(container.getSplitRule().getLayoutDirection()) - .build() - ); + container.getSplitAttributes()); splitStates.add(splitState); } } @@ -1577,6 +1691,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a split rule for the provided pair of primary activity and secondary activity intent * if available. */ + @GuardedBy("mLock") @Nullable private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryActivityIntent) { @@ -1595,6 +1710,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Returns a split rule for the provided pair of primary and secondary activities if available. */ + @GuardedBy("mLock") @Nullable private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { @@ -1669,6 +1785,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns {@code true} if an Activity with the provided component name should always be * expanded to occupy full task bounds. Such activity must not be put in a split. */ + @GuardedBy("mLock") private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) { for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof ActivityRule)) { @@ -1694,6 +1811,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * 'sticky' and the placeholder was finished when fully overlapping the primary container. * @return {@code true} if the associated container should be retained (and not be finished). */ + // Suppress GuardedBy warning because lint ask to mark this method as + // @GuardedBy(mPresenter.mController.mLock), which is mLock itself + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") boolean shouldRetainAssociatedContainer(@NonNull TaskFragmentContainer finishingContainer, @NonNull TaskFragmentContainer associatedContainer) { SplitContainer splitContainer = getActiveSplitForContainers(associatedContainer, @@ -1712,7 +1833,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Decide whether the associated container should be retained based on the current // presentation mode. - if (shouldShowSideBySide(splitContainer)) { + if (shouldShowSplit(splitContainer)) { return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior); } else { return !shouldFinishAssociatedContainerWhenStacked(finishBehavior); @@ -1905,23 +2026,33 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * If the two rules have the same presentation, we can reuse the same {@link SplitContainer} if * there is any. */ - private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2) { + private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2, + @NonNull WindowMetrics parentWindowMetrics) { if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } - return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2); + return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2, + parentWindowMetrics); } /** Whether the two rules have the same presentation. */ - private static boolean haveSamePresentation(@NonNull SplitPairRule rule1, - @NonNull SplitPairRule rule2) { + @VisibleForTesting + static boolean haveSamePresentation(@NonNull SplitPairRule rule1, + @NonNull SplitPairRule rule2, @NonNull WindowMetrics parentWindowMetrics) { + if (rule1.getTag() != null || rule2.getTag() != null) { + // Tag must be unique if it is set. We don't want to reuse the container if the rules + // have different tags because they can have different SplitAttributes later through + // SplitAttributesCalculator. + return Objects.equals(rule1.getTag(), rule2.getTag()); + } + // If both rules don't have tag, compare all SplitRules' properties that may affect their + // SplitAttributes. // TODO(b/231655482): add util method to do the comparison in SplitPairRule. - return rule1.getSplitRatio() == rule2.getSplitRatio() - && rule1.getLayoutDirection() == rule2.getLayoutDirection() - && rule1.getFinishPrimaryWithSecondary() - == rule2.getFinishPrimaryWithSecondary() - && rule1.getFinishSecondaryWithPrimary() - == rule2.getFinishSecondaryWithPrimary(); + return rule1.getDefaultSplitAttributes().equals(rule2.getDefaultSplitAttributes()) + && rule1.checkParentMetrics(parentWindowMetrics) + == rule2.checkParentMetrics(parentWindowMetrics) + && rule1.getFinishPrimaryWithSecondary() == rule2.getFinishPrimaryWithSecondary() + && rule1.getFinishSecondaryWithPrimary() == rule2.getFinishSecondaryWithPrimary(); } /** 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 2ef8e4c64855..79603233ae14 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -22,11 +22,11 @@ import android.app.Activity; import android.app.ActivityThread; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; -import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -42,9 +42,21 @@ import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.window.extensions.embedding.SplitAttributes.SplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.HingeSplitType; +import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; +import androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams; +import androidx.window.extensions.embedding.TaskContainer.TaskProperties; +import androidx.window.extensions.layout.DisplayFeature; +import androidx.window.extensions.layout.FoldingFeature; +import androidx.window.extensions.layout.WindowLayoutInfo; import com.android.internal.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -66,11 +78,25 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} + private static final int CONTAINER_POSITION_LEFT = 0; + private static final int CONTAINER_POSITION_TOP = 1; + private static final int CONTAINER_POSITION_RIGHT = 2; + private static final int CONTAINER_POSITION_BOTTOM = 3; + + @IntDef(value = { + CONTAINER_POSITION_LEFT, + CONTAINER_POSITION_TOP, + CONTAINER_POSITION_RIGHT, + CONTAINER_POSITION_BOTTOM, + }) + private @interface ContainerPosition {} + /** * 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. + * {@link #shouldShowSplit(SplitAttributes)} and minimum dimensions is + * satisfied. */ static final int RESULT_NOT_EXPANDED = 0; /** @@ -78,7 +104,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Activity, Activity, Intent)}. * The splitContainer should be expanded. It is usually because minimum dimensions is not * satisfied. - * @see #shouldShowSideBySide(Rect, SplitRule, Pair) + * @see #shouldShowSplit(SplitAttributes) */ static final int RESULT_EXPANDED = 1; /** @@ -101,6 +127,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface ResultCode {} + @VisibleForTesting + static final SplitAttributes EXPAND_CONTAINERS_ATTRIBUTES = + new SplitAttributes.Builder() + .setSplitType(new ExpandContainersSplitType()) + .build(); + private final SplitController mController; SplitPresenter(@NonNull Executor executor, @NonNull SplitController controller) { @@ -129,14 +161,17 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @return The newly created secondary container. */ @NonNull + @GuardedBy("mController.mLock") TaskFragmentContainer createNewSplitWithEmptySideContainer( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) { - final Rect parentBounds = getParentContainerBounds(primaryActivity); + final TaskProperties taskProperties = getTaskProperties(primaryActivity); final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair( primaryActivity, secondaryIntent); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - primaryActivity, minDimensionsPair); + final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule, + minDimensionsPair); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, primaryActivity, primaryRectBounds, null); @@ -144,8 +179,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final int taskId = primaryContainer.getTaskId(); final TaskFragmentContainer secondaryContainer = mController.newContainer( secondaryIntent, primaryActivity, taskId); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, - rule, primaryActivity, minDimensionsPair); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForSplitTaskFragment(secondaryRectBounds); createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), @@ -154,9 +189,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); - mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); + mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule, + splitAttributes); return secondaryContainer; } @@ -176,16 +212,18 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { void createNewSplitContainer(@NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) { - final Rect parentBounds = getParentContainerBounds(primaryActivity); + final TaskProperties taskProperties = getTaskProperties(primaryActivity); final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - primaryActivity, minDimensionsPair); + final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule, + minDimensionsPair); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, primaryActivity, primaryRectBounds, null); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - primaryActivity, minDimensionsPair); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity( secondaryActivity); TaskFragmentContainer containerToAvoid = primaryContainer; @@ -200,9 +238,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); - mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); + mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule, + splitAttributes); } /** @@ -244,16 +283,16 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @param rule The split rule to be applied to the container. * @param isPlaceholder Whether the launch is a placeholder. */ + @GuardedBy("mController.mLock") void startActivityToSide(@NonNull WindowContainerTransaction wct, @NonNull Activity launchingActivity, @NonNull Intent activityIntent, - @Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) { - final Rect parentBounds = getParentContainerBounds(launchingActivity); - final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair( - launchingActivity, activityIntent); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - launchingActivity, minDimensionsPair); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - launchingActivity, minDimensionsPair); + @Nullable Bundle activityOptions, @NonNull SplitRule rule, + @NonNull SplitAttributes splitAttributes, boolean isPlaceholder) { + final TaskProperties taskProperties = getTaskProperties(launchingActivity); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( launchingActivity); @@ -268,7 +307,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final int windowingMode = mController.getTaskContainer(taskId) .getWindowingModeForSplitTaskFragment(primaryRectBounds); mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, - rule); + rule, splitAttributes); startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds, activityIntent, activityOptions, rule, windowingMode); @@ -284,22 +323,24 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @param updatedContainer The task fragment that was updated and caused this split update. * @param wct WindowContainerTransaction that this update should be performed with. */ + @GuardedBy("mController.mLock") void updateSplitContainer(@NonNull SplitContainer splitContainer, @NonNull TaskFragmentContainer updatedContainer, @NonNull WindowContainerTransaction wct) { - // Getting the parent bounds using the updated container - it will have the recent value. - final Rect parentBounds = getParentContainerBounds(updatedContainer); + // Getting the parent configuration using the updated container - it will have the recent + // value. final SplitRule rule = splitContainer.getSplitRule(); final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer(); final Activity activity = primaryContainer.getTopNonFinishingActivity(); if (activity == null) { return; } - final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair(); - final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, - activity, minDimensionsPair); - final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, - activity, minDimensionsPair); + final TaskProperties taskProperties = getTaskProperties(updatedContainer); + final SplitAttributes splitAttributes = splitContainer.getSplitAttributes(); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, + splitAttributes); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, + splitAttributes); final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); // Whether the placeholder is becoming side-by-side with the primary from fullscreen. final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer() @@ -311,7 +352,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, - minDimensionsPair); + splitAttributes); if (isPlaceholderBecomingSplit) { // When placeholder is shown in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); @@ -323,14 +364,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); } + @GuardedBy("mController.mLock") private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, - @NonNull Pair<Size, Size> minDimensionsPair) { - final Rect parentBounds = getParentContainerBounds(primaryContainer); + @NonNull SplitAttributes splitAttributes) { // Clear adjacent TaskFragments if the container is shown in fullscreen, or the // secondaryContainer could not be finished. - if (!shouldShowSideBySide(parentBounds, splitRule, minDimensionsPair)) { + if (!shouldShowSplit(splitAttributes)) { setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), null /* secondary */, null /* splitRule */); } else { @@ -416,8 +457,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * 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. + * @return the {@link ResultCode} based on + * {@link #shouldShowSplit(SplitAttributes)} and if + * {@link android.window.TaskFragmentInfo} has reported to the client side. */ @ResultCode int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, @@ -427,7 +469,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { 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); @@ -436,7 +477,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { secondaryIntent); } // Expand the splitContainer if minimum dimensions are not satisfied. - if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) { + final TaskContainer taskContainer = splitContainer.getTaskContainer(); + final SplitAttributes splitAttributes = sanitizeSplitAttributes( + taskContainer.getTaskProperties(), splitContainer.getSplitAttributes(), + minDimensionsPair); + splitContainer.setSplitAttributes(splitAttributes); + if (!shouldShowSplit(splitAttributes)) { // 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 @@ -450,36 +496,63 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return RESULT_NOT_EXPANDED; } - static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { - return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); + static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) { + return shouldShowSplit(splitContainer.getSplitAttributes()); } - static boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) { - final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer()); + static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) { + return !(splitAttributes.getSplitType() instanceof ExpandContainersSplitType); + } - return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule(), - splitContainer.getMinDimensionsPair()); + @GuardedBy("mController.mLock") + @NonNull + SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties, + @NonNull SplitRule rule, @Nullable Pair<Size, Size> minDimensionsPair) { + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration); + final SplitAttributesCalculator calculator = mController.getSplitAttributesCalculator(); + final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes(); + final boolean isDefaultMinSizeSatisfied = rule.checkParentMetrics(taskWindowMetrics); + if (calculator == null) { + if (!isDefaultMinSizeSatisfied) { + return EXPAND_CONTAINERS_ATTRIBUTES; + } + return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes, + minDimensionsPair); + } + final WindowLayoutInfo windowLayoutInfo = mController.mWindowLayoutComponent + .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(), + taskConfiguration.windowConfiguration); + final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams( + taskWindowMetrics, taskConfiguration, defaultSplitAttributes, + isDefaultMinSizeSatisfied, windowLayoutInfo, rule.getTag()); + final SplitAttributes splitAttributes = calculator.computeSplitAttributesForParams(params); + return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair); } - static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule, + /** + * Returns {@link #EXPAND_CONTAINERS_ATTRIBUTES} if the passed {@link SplitAttributes} doesn't + * meet the minimum dimensions set in {@link ActivityInfo.WindowLayout}. Otherwise, returns + * the passed {@link SplitAttributes}. + */ + @NonNull + private SplitAttributes sanitizeSplitAttributes(@NonNull TaskProperties taskProperties, + @NonNull SplitAttributes splitAttributes, @Nullable Pair<Size, Size> minDimensionsPair) { - // TODO(b/190433398): Supply correct insets. - final WindowMetrics parentMetrics = new WindowMetrics(parentBounds, - new WindowInsets(new Rect())); - // Don't show side by side if bounds is not qualified. - if (!rule.checkParentMetrics(parentMetrics)) { - return false; - } - final float splitRatio = rule.getSplitRatio(); - // We only care the size of the bounds regardless of its position. - final Rect primaryBounds = getPrimaryBounds(parentBounds, splitRatio, true /* isLtr */); - final Rect secondaryBounds = getSecondaryBounds(parentBounds, splitRatio, true /* isLtr */); - if (minDimensionsPair == null) { - return true; - } - return !boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) - && !boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second); + return splitAttributes; + } + final FoldingFeature foldingFeature = getFoldingFeature(taskProperties); + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final Rect primaryBounds = getPrimaryBounds(taskConfiguration, splitAttributes, + foldingFeature); + final Rect secondaryBounds = getSecondaryBounds(taskConfiguration, splitAttributes, + foldingFeature); + if (boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) + || boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second)) { + return EXPAND_CONTAINERS_ATTRIBUTES; + } + return splitAttributes; } @NonNull @@ -541,20 +614,25 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @VisibleForTesting @NonNull - static Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds, - @NonNull SplitRule rule, @NonNull Activity primaryActivity, - @Nullable Pair<Size, Size> minDimensionsPair) { - if (!shouldShowSideBySide(parentBounds, rule, minDimensionsPair)) { + Rect getBoundsForPosition(@Position int position, @NonNull TaskProperties taskProperties, + @NonNull SplitAttributes splitAttributes) { + final Configuration taskConfiguration = taskProperties.getConfiguration(); + final FoldingFeature foldingFeature = getFoldingFeature(taskProperties); + final SplitType splitType = computeSplitType(splitAttributes, taskConfiguration, + foldingFeature); + final SplitAttributes computedSplitAttributes = new SplitAttributes.Builder() + .setSplitType(splitType) + .setLayoutDirection(splitAttributes.getLayoutDirection()) + .build(); + if (!shouldShowSplit(computedSplitAttributes)) { return new Rect(); } - final boolean isLtr = isLtr(primaryActivity, rule); - final float splitRatio = rule.getSplitRatio(); - switch (position) { case POSITION_START: - return getPrimaryBounds(parentBounds, splitRatio, isLtr); + return getPrimaryBounds(taskConfiguration, computedSplitAttributes, foldingFeature); case POSITION_END: - return getSecondaryBounds(parentBounds, splitRatio, isLtr); + return getSecondaryBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); case POSITION_FILL: default: return new Rect(); @@ -562,74 +640,303 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @NonNull - private static Rect getPrimaryBounds(@NonNull Rect parentBounds, float splitRatio, - boolean isLtr) { - return isLtr ? getLeftContainerBounds(parentBounds, splitRatio) - : getRightContainerBounds(parentBounds, 1 - splitRatio); + private Rect getPrimaryBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + if (!shouldShowSplit(splitAttributes)) { + return new Rect(); + } + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { + return getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { + return getRightContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.LOCALE: { + final boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr + ? getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature) + : getRightContainerBounds(taskConfiguration, splitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { + return getTopContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { + return getBottomContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + default: + throw new IllegalArgumentException("Unknown layout direction:" + + splitAttributes.getLayoutDirection()); + } } @NonNull - private static Rect getSecondaryBounds(@NonNull Rect parentBounds, float splitRatio, - boolean isLtr) { - return isLtr ? getRightContainerBounds(parentBounds, splitRatio) - : getLeftContainerBounds(parentBounds, 1 - splitRatio); + private Rect getSecondaryBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + if (!shouldShowSplit(splitAttributes)) { + return new Rect(); + } + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { + return getRightContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { + return getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.LOCALE: { + final boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr + ? getRightContainerBounds(taskConfiguration, splitAttributes, + foldingFeature) + : getLeftContainerBounds(taskConfiguration, splitAttributes, + foldingFeature); + } + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { + return getBottomContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { + return getTopContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + } + default: + throw new IllegalArgumentException("Unknown layout direction:" + + splitAttributes.getLayoutDirection()); + } } - private static Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) { - return new Rect( - parentBounds.left, - parentBounds.top, - (int) (parentBounds.left + parentBounds.width() * splitRatio), - parentBounds.bottom); + @NonNull + private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int right = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_LEFT, foldingFeature); + final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(taskBounds.left, taskBounds.top, right, taskBounds.bottom); } - private static Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) { - return new Rect( - (int) (parentBounds.left + parentBounds.width() * splitRatio), - parentBounds.top, - parentBounds.right, - parentBounds.bottom); + @NonNull + private Rect getRightContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int left = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_RIGHT, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(left, parentBounds.top, parentBounds.right, parentBounds.bottom); + } + + @NonNull + private Rect getTopContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int bottom = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_TOP, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(parentBounds.left, parentBounds.top, parentBounds.right, bottom); + } + + @NonNull + private Rect getBottomContainerBounds(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { + final int top = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, + CONTAINER_POSITION_BOTTOM, foldingFeature); + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + return new Rect(parentBounds.left, top, parentBounds.right, parentBounds.bottom); } /** - * Checks if a split with the provided rule should be displays in left-to-right layout - * direction, either always or with the current configuration. + * Computes the boundary position between the primary and the secondary containers for the given + * {@link ContainerPosition} with {@link SplitAttributes}, current window and device states. + * <ol> + * <li>For {@link #CONTAINER_POSITION_TOP}, it computes the boundary with the bottom + * container, which is {@link Rect#bottom} of the top container bounds.</li> + * <li>For {@link #CONTAINER_POSITION_BOTTOM}, it computes the boundary with the top + * container, which is {@link Rect#top} of the bottom container bounds.</li> + * <li>For {@link #CONTAINER_POSITION_LEFT}, it computes the boundary with the right + * container, which is {@link Rect#right} of the left container bounds.</li> + * <li>For {@link #CONTAINER_POSITION_RIGHT}, it computes the boundary with the bottom + * container, which is {@link Rect#left} of the right container bounds.</li> + * </ol> + * + * @see #getTopContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getBottomContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getLeftContainerBounds(Configuration, SplitAttributes, FoldingFeature) + * @see #getRightContainerBounds(Configuration, SplitAttributes, FoldingFeature) */ - private static boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) { - switch (rule.getLayoutDirection()) { - case LayoutDirection.LOCALE: - return context.getResources().getConfiguration().getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; - case LayoutDirection.RTL: - return false; - case LayoutDirection.LTR: + private int computeBoundaryBetweenContainers(@NonNull Configuration taskConfiguration, + @NonNull SplitAttributes splitAttributes, @ContainerPosition int position, + @Nullable FoldingFeature foldingFeature) { + final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); + final int startPoint = shouldSplitHorizontally(splitAttributes) + ? parentBounds.top + : parentBounds.left; + final int dimen = shouldSplitHorizontally(splitAttributes) + ? parentBounds.height() + : parentBounds.width(); + final SplitType splitType = splitAttributes.getSplitType(); + if (splitType instanceof RatioSplitType) { + final RatioSplitType splitRatio = (RatioSplitType) splitType; + return (int) (startPoint + dimen * splitRatio.getRatio()); + } + // At this point, SplitType must be a HingeSplitType and foldingFeature must be + // non-null. RatioSplitType and ExpandContainerSplitType have been handled earlier. + Objects.requireNonNull(foldingFeature); + if (!(splitType instanceof HingeSplitType)) { + throw new IllegalArgumentException("Unknown splitType:" + splitType); + } + final Rect hingeArea = foldingFeature.getBounds(); + switch (position) { + case CONTAINER_POSITION_LEFT: + return hingeArea.left; + case CONTAINER_POSITION_TOP: + return hingeArea.top; + case CONTAINER_POSITION_RIGHT: + return hingeArea.right; + case CONTAINER_POSITION_BOTTOM: + return hingeArea.bottom; default: - return true; + throw new IllegalArgumentException("Unknown position:" + position); } } - @NonNull - static Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { - return container.getTaskContainer().getTaskBounds(); + @Nullable + private FoldingFeature getFoldingFeature(@NonNull TaskProperties taskProperties) { + final int displayId = taskProperties.getDisplayId(); + final WindowConfiguration windowConfiguration = taskProperties.getConfiguration() + .windowConfiguration; + final WindowLayoutInfo info = mController.mWindowLayoutComponent + .getCurrentWindowLayoutInfo(displayId, windowConfiguration); + final List<DisplayFeature> displayFeatures = info.getDisplayFeatures(); + if (displayFeatures.isEmpty()) { + return null; + } + final List<FoldingFeature> foldingFeatures = new ArrayList<>(); + for (DisplayFeature displayFeature : displayFeatures) { + if (displayFeature instanceof FoldingFeature) { + foldingFeatures.add((FoldingFeature) displayFeature); + } + } + // TODO(b/240219484): Support device with multiple hinges. + if (foldingFeatures.size() != 1) { + return null; + } + return foldingFeatures.get(0); } - @NonNull - Rect getParentContainerBounds(@NonNull Activity activity) { - final TaskFragmentContainer container = mController.getContainerWithActivity(activity); - if (container != null) { - return getParentContainerBounds(container); + /** + * Indicates that this {@link SplitAttributes} splits the task horizontally. Returns + * {@code false} if this {@link SplitAttributes} splits the task vertically. + */ + private static boolean shouldSplitHorizontally(SplitAttributes splitAttributes) { + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return true; + default: + return false; } - // 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()}. + * Computes the {@link SplitType} with the {@link SplitAttributes} and the current device and + * window state. + * If passed {@link SplitAttributes#getSplitType} is a {@link RatioSplitType}. It reversed + * the ratio if the computed {@link SplitAttributes#getLayoutDirection} is + * {@link SplitAttributes.LayoutDirection.LEFT_TO_RIGHT} or + * {@link SplitAttributes.LayoutDirection.BOTTOM_TO_TOP} to make the bounds calculation easier. + * If passed {@link SplitAttributes#getSplitType} is a {@link HingeSplitType}, it checks + * the current device and window states to determine whether the split container should split + * by hinge or use {@link HingeSplitType#getFallbackSplitType}. */ + private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes, + @NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) { + final int layoutDirection = splitAttributes.getLayoutDirection(); + final SplitType splitType = splitAttributes.getSplitType(); + if (splitType instanceof ExpandContainersSplitType) { + return splitType; + } else if (splitType instanceof RatioSplitType) { + final RatioSplitType splitRatio = (RatioSplitType) splitType; + // Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary + // computation have the same direction, which is from (top, left) to (bottom, right). + final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio()); + switch (layoutDirection) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + return splitType; + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return reversedSplitType; + case LayoutDirection.LOCALE: { + boolean isLtr = taskConfiguration.getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + return isLtr ? splitType : reversedSplitType; + } + } + } else if (splitType instanceof HingeSplitType) { + final HingeSplitType hinge = (HingeSplitType) splitType; + @WindowingMode + final int windowingMode = taskConfiguration.windowConfiguration.getWindowingMode(); + return shouldSplitByHinge(splitAttributes, foldingFeature, windowingMode) + ? hinge : hinge.getFallbackSplitType(); + } + throw new IllegalArgumentException("Unknown SplitType:" + splitType); + } + + private static boolean shouldSplitByHinge(@NonNull SplitAttributes splitAttributes, + @Nullable FoldingFeature foldingFeature, @WindowingMode int taskWindowingMode) { + // Only HingeSplitType may split the task bounds by hinge. + if (!(splitAttributes.getSplitType() instanceof HingeSplitType)) { + return false; + } + // Device is not foldable, so there's no hinge to match. + if (foldingFeature == null) { + return false; + } + // The task is in multi-window mode. Match hinge doesn't make sense because current task + // bounds may not fit display bounds. + if (WindowConfiguration.inMultiWindowMode(taskWindowingMode)) { + return false; + } + // Return true if how the split attributes split the task bounds matches the orientation of + // folding area orientation. + return shouldSplitHorizontally(splitAttributes) == isFoldingAreaHorizontal(foldingFeature); + } + + private static boolean isFoldingAreaHorizontal(@NonNull FoldingFeature foldingFeature) { + final Rect bounds = foldingFeature.getBounds(); + return bounds.width() > bounds.height(); + } + + @NonNull + static TaskProperties getTaskProperties(@NonNull TaskFragmentContainer container) { + return container.getTaskContainer().getTaskProperties(); + } + + @NonNull + TaskProperties getTaskProperties(@NonNull Activity activity) { + final TaskContainer taskContainer = mController.getTaskContainer( + mController.getTaskId(activity)); + if (taskContainer != null) { + return taskContainer.getTaskProperties(); + } + // Use a copy of configuration because activity's configuration may be updated later, + // or we may get unexpected TaskContainer's configuration if Activity's configuration is + // updated. An example is Activity is going to be in split. + return new TaskProperties(activity.getDisplayId(), + new Configuration(activity.getResources().getConfiguration())); + } + + @NonNull + WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) { + return getTaskWindowMetrics(getTaskProperties(activity).getConfiguration()); + } + + @NonNull + private static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) { + final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); + // TODO(b/190433398): Supply correct insets. + return new WindowMetrics(taskBounds, WindowInsets.CONSUMED); + } + + /** Obtains the bounds from a non-embedded Activity. */ @NonNull static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) { final WindowConfiguration windowConfiguration = diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index b5636777568e..91573ffef568 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -24,10 +24,13 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import android.app.Activity; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.IBinder; import android.util.ArraySet; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -42,13 +45,10 @@ class TaskContainer { /** The unique task id. */ private final int mTaskId; + // TODO(b/240219484): consolidate to mConfiguration /** Available window bounds of this Task. */ private final Rect mTaskBounds = new Rect(); - /** Windowing mode of this Task. */ - @WindowingMode - private int mWindowingMode = WINDOWING_MODE_UNDEFINED; - /** Active TaskFragments in this Task. */ @NonNull final List<TaskFragmentContainer> mContainers = new ArrayList<>(); @@ -57,24 +57,56 @@ class TaskContainer { @NonNull final List<SplitContainer> mSplitContainers = new ArrayList<>(); + @NonNull + private final Configuration mConfiguration; + + private int mDisplayId; + + private boolean mIsVisible; + /** * TaskFragments that the organizer has requested to be closed. They should be removed when - * the organizer receives {@link SplitController#onTaskFragmentVanished(TaskFragmentInfo)} event - * for them. + * the organizer receives + * {@link SplitController#onTaskFragmentVanished(WindowContainerTransaction, TaskFragmentInfo)} + * event for them. */ final Set<IBinder> mFinishedContainer = new ArraySet<>(); - TaskContainer(int taskId) { + /** + * The {@link TaskContainer} constructor + * + * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with + * {@code activityInTask}. + * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to + * initialize the {@link TaskContainer} properties. + * + */ + TaskContainer(int taskId, @NonNull Activity activityInTask) { if (taskId == INVALID_TASK_ID) { throw new IllegalArgumentException("Invalid Task id"); } mTaskId = taskId; + // Make a copy in case the activity's config is updated, and updates the TaskContainer's + // config unexpectedly. + mConfiguration = new Configuration(activityInTask.getResources().getConfiguration()); + mDisplayId = activityInTask.getDisplayId(); + // Note that it is always called when there's a new Activity is started, which implies + // the host task is visible. + mIsVisible = true; } int getTaskId() { return mTaskId; } + int getDisplayId() { + return mDisplayId; + } + + boolean isVisible() { + return mIsVisible; + } + @NonNull Rect getTaskBounds() { return mTaskBounds; @@ -94,13 +126,21 @@ class TaskContainer { return !mTaskBounds.isEmpty(); } - void setWindowingMode(int windowingMode) { - mWindowingMode = windowingMode; + @NonNull + Configuration getConfiguration() { + // Make a copy in case the config is updated unexpectedly. + return new Configuration(mConfiguration); + } + + @NonNull + TaskProperties getTaskProperties() { + return new TaskProperties(mDisplayId, mConfiguration); } - /** Whether the Task windowing mode has been initialized. */ - boolean isWindowingModeInitialized() { - return mWindowingMode != WINDOWING_MODE_UNDEFINED; + void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { + mConfiguration.setTo(info.getConfiguration()); + mDisplayId = info.getDisplayId(); + mIsVisible = info.isVisibleRequested(); } /** @@ -123,13 +163,20 @@ class TaskContainer { // DecorCaptionView won't work correctly. As a result, have the TaskFragment to be in the // Task windowing mode if the Task is in multi window. // TODO we won't need this anymore after we migrate Freeform caption to WM Shell. - return WindowConfiguration.inMultiWindowMode(mWindowingMode) - ? mWindowingMode - : WINDOWING_MODE_MULTI_WINDOW; + return isInMultiWindow() ? getWindowingMode() : WINDOWING_MODE_MULTI_WINDOW; } boolean isInPictureInPicture() { - return mWindowingMode == WINDOWING_MODE_PINNED; + return getWindowingMode() == WINDOWING_MODE_PINNED; + } + + boolean isInMultiWindow() { + return WindowConfiguration.inMultiWindowMode(getWindowingMode()); + } + + @WindowingMode + private int getWindowingMode() { + return getConfiguration().windowConfiguration.getWindowingMode(); } /** Whether there is any {@link TaskFragmentContainer} below this Task. */ @@ -173,4 +220,28 @@ class TaskContainer { int indexOf(@NonNull TaskFragmentContainer child) { return mContainers.indexOf(child); } + + /** + * A wrapper class which contains the display ID and {@link Configuration} of a + * {@link TaskContainer} + */ + static final class TaskProperties { + private final int mDisplayId; + @NonNull + private final Configuration mConfiguration; + + TaskProperties(int displayId, @NonNull Configuration configuration) { + mDisplayId = displayId; + mConfiguration = configuration; + } + + int getDisplayId() { + return mDisplayId; + } + + @NonNull + Configuration getConfiguration() { + return mConfiguration; + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index f24401f0cd53..c76f568e117f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -47,6 +47,7 @@ import androidx.window.common.RawFoldingFeatureProducer; import androidx.window.util.DataProducer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -68,6 +69,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; + private final List<CommonFoldingFeature> mLastReportedFoldingFeatures = new ArrayList<>(); + private final Map<IBinder, WindowContextConfigListener> mWindowContextConfigListeners = new ArrayMap<>(); @@ -80,6 +83,11 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); } + /** Registers to listen to {@link CommonFoldingFeature} changes */ + public void addFoldingStateChangedCallback(Consumer<List<CommonFoldingFeature>> consumer) { + mFoldingFeatureProducer.addDataChangedCallback(consumer); + } + /** * Adds a listener interested in receiving updates to {@link WindowLayoutInfo} * @@ -186,6 +194,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { + mLastReportedFoldingFeatures.clear(); + mLastReportedFoldingFeatures.addAll(storedFeatures); for (Context context : getContextsListeningForLayoutChanges()) { // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer. Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(context); @@ -207,6 +217,27 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } /** + * Gets the current {@link WindowLayoutInfo} computed with passed {@link WindowConfiguration}. + * + * @return current {@link WindowLayoutInfo} on the default display. Returns + * empty {@link WindowLayoutInfo} on secondary displays. + */ + @NonNull + public WindowLayoutInfo getCurrentWindowLayoutInfo(int displayId, + @NonNull WindowConfiguration windowConfiguration) { + return getWindowLayoutInfo(displayId, windowConfiguration, mLastReportedFoldingFeatures); + } + + /** @see #getWindowLayoutInfo(Context, List) */ + private WindowLayoutInfo getWindowLayoutInfo(int displayId, + @NonNull WindowConfiguration windowConfiguration, + List<CommonFoldingFeature> storedFeatures) { + List<DisplayFeature> displayFeatureList = getDisplayFeatures(displayId, windowConfiguration, + storedFeatures); + return new WindowLayoutInfo(displayFeatureList); + } + + /** * Translate from the {@link CommonFoldingFeature} to * {@link DisplayFeature} for a given {@link Activity}. If a * {@link CommonFoldingFeature} is not valid then it will be omitted. @@ -225,12 +256,23 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { */ private List<DisplayFeature> getDisplayFeatures( @NonNull @UiContext Context context, List<CommonFoldingFeature> storedFeatures) { - List<DisplayFeature> features = new ArrayList<>(); if (!shouldReportDisplayFeatures(context)) { + return Collections.emptyList(); + } + return getDisplayFeatures(context.getDisplayId(), + context.getResources().getConfiguration().windowConfiguration, + storedFeatures); + } + + /** @see #getDisplayFeatures(Context, List) */ + private List<DisplayFeature> getDisplayFeatures(int displayId, + @NonNull WindowConfiguration windowConfiguration, + List<CommonFoldingFeature> storedFeatures) { + List<DisplayFeature> features = new ArrayList<>(); + if (displayId != DEFAULT_DISPLAY) { return features; } - int displayId = context.getDisplay().getDisplayId(); for (CommonFoldingFeature baseFeature : storedFeatures) { Integer state = convertToExtensionState(baseFeature.getState()); if (state == null) { @@ -238,7 +280,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } Rect featureRect = baseFeature.getRect(); rotateRectToDisplayRotation(displayId, featureRect); - transformToWindowSpaceRect(context, featureRect); + transformToWindowSpaceRect(windowConfiguration, featureRect); if (!isZero(featureRect)) { // TODO(b/228641877): Remove guarding when fixed. @@ -263,6 +305,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { windowingMode = ActivityClient.getInstance().getTaskWindowingMode( context.getActivityToken()); } else { + // TODO(b/242674941): use task windowing mode for window context that associates with + // activity. windowingMode = context.getResources().getConfiguration().windowConfiguration .getWindowingMode(); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java index 31bf96313a95..9e2611f392a3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java @@ -21,6 +21,7 @@ import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; +import android.app.WindowConfiguration; import android.content.Context; import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; @@ -89,13 +90,21 @@ public final class ExtensionHelper { /** Transforms rectangle from absolute coordinate space to the window coordinate space. */ public static void transformToWindowSpaceRect(@NonNull @UiContext Context context, Rect inOutRect) { - Rect windowRect = getWindowBounds(context); - if (!Rect.intersects(inOutRect, windowRect)) { + transformToWindowSpaceRect(getWindowBounds(context), inOutRect); + } + + /** @see ExtensionHelper#transformToWindowSpaceRect(Context, Rect) */ + public static void transformToWindowSpaceRect(@NonNull WindowConfiguration windowConfiguration, + Rect inOutRect) { + transformToWindowSpaceRect(windowConfiguration.getBounds(), inOutRect); + } + + private static void transformToWindowSpaceRect(@NonNull Rect bounds, @NonNull Rect inOutRect) { + if (!inOutRect.intersect(bounds)) { inOutRect.setEmpty(); return; } - inOutRect.intersect(windowRect); - inOutRect.offset(-windowRect.left, -windowRect.top); + inOutRect.offset(-bounds.left, -bounds.top); } /** 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 effc1a3ef3ea..40f7a273980a 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 @@ -16,9 +16,12 @@ package androidx.window.extensions.embedding; +import static android.view.Display.DEFAULT_DISPLAY; + import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; import static androidx.window.extensions.embedding.SplitRule.FINISH_NEVER; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import android.annotation.NonNull; @@ -26,32 +29,68 @@ import android.app.Activity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.util.Pair; import android.window.TaskFragmentInfo; import android.window.WindowContainerToken; +import androidx.window.extensions.embedding.SplitAttributes.SplitType; +import androidx.window.extensions.layout.DisplayFeature; +import androidx.window.extensions.layout.FoldingFeature; +import androidx.window.extensions.layout.WindowLayoutInfo; + +import java.util.ArrayList; import java.util.Collections; +import java.util.List; public class EmbeddingTestUtils { static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); static final int TASK_ID = 10; - static final float SPLIT_RATIO = 0.5f; + static final SplitType SPLIT_TYPE = SplitType.RatioSplitType.splitEqually(); + static final SplitAttributes SPLIT_ATTRIBUTES = new SplitAttributes.Builder().build(); + static final String TEST_TAG = "test"; /** Default finish behavior in Jetpack. */ static final int DEFAULT_FINISH_PRIMARY_WITH_SECONDARY = FINISH_NEVER; static final int DEFAULT_FINISH_SECONDARY_WITH_PRIMARY = FINISH_ALWAYS; + private static final float SPLIT_RATIO = 0.5f; private EmbeddingTestUtils() {} /** Gets the bounds of a TaskFragment that is in split. */ static Rect getSplitBounds(boolean isPrimary) { - final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO); + return getSplitBounds(isPrimary, false /* shouldSplitHorizontally */); + } + + /** Gets the bounds of a TaskFragment that is in split. */ + static Rect getSplitBounds(boolean isPrimary, boolean shouldSplitHorizontally) { + final int dimension = (int) ( + (shouldSplitHorizontally ? TASK_BOUNDS.height() : TASK_BOUNDS.width()) + * SPLIT_RATIO); + if (shouldSplitHorizontally) { + return isPrimary + ? new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.right, + TASK_BOUNDS.top + dimension) + : new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top + dimension, + TASK_BOUNDS.right, + TASK_BOUNDS.bottom); + } return isPrimary - ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width, - TASK_BOUNDS.bottom) + ? new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.left + dimension, + TASK_BOUNDS.bottom) : new Rect( - TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right, + TASK_BOUNDS.left + dimension, + TASK_BOUNDS.top, + TASK_BOUNDS.right, TASK_BOUNDS.bottom); } @@ -69,10 +108,15 @@ public class EmbeddingTestUtils { activityPair -> false, targetPair::equals, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes( + new SplitAttributes.Builder() + .setSplitType(SPLIT_TYPE) + .build() + ) .setShouldClearTop(clearTop) .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setTag(TEST_TAG) .build(); } @@ -101,10 +145,15 @@ public class EmbeddingTestUtils { targetPair::equals, activityIntentPair -> false, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes( + new SplitAttributes.Builder() + .setSplitType(SPLIT_TYPE) + .build() + ) .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary) .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary) .setShouldClearTop(clearTop) + .setTag(TEST_TAG) .build(); } @@ -130,4 +179,29 @@ public class EmbeddingTestUtils { primaryBounds.width() + 1, primaryBounds.height() + 1); return aInfo; } + + static TaskContainer createTestTaskContainer() { + Resources resources = mock(Resources.class); + doReturn(new Configuration()).when(resources).getConfiguration(); + Activity activity = mock(Activity.class); + doReturn(resources).when(activity).getResources(); + doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); + + return new TaskContainer(TASK_ID, activity); + } + + static WindowLayoutInfo createWindowLayoutInfo() { + final FoldingFeature foldingFeature = new FoldingFeature( + new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top + TASK_BOUNDS.height() / 2 - 5, + TASK_BOUNDS.right, + TASK_BOUNDS.top + TASK_BOUNDS.height() / 2 + 5 + ), + FoldingFeature.TYPE_HINGE, + FoldingFeature.STATE_HALF_OPENED); + final List<DisplayFeature> displayFeatures = new ArrayList<>(); + displayFeatures.add(foldingFeature); + return new WindowLayoutInfo(displayFeatures); + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 58a627bafa16..957a24873998 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -19,6 +19,7 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -26,12 +27,14 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Point; +import android.os.Handler; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; import android.window.TaskFragmentTransaction; @@ -65,7 +68,10 @@ public class JetpackTaskFragmentOrganizerTest { private WindowContainerTransaction mTransaction; @Mock private JetpackTaskFragmentOrganizer.TaskFragmentCallback mCallback; + @Mock private SplitController mSplitController; + @Mock + private Handler mHandler; private JetpackTaskFragmentOrganizer mOrganizer; @Before @@ -73,9 +79,8 @@ public class JetpackTaskFragmentOrganizerTest { MockitoAnnotations.initMocks(this); mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback); mOrganizer.registerOrganizer(); - mSplitController = new SplitController(); spyOn(mOrganizer); - spyOn(mSplitController); + doReturn(mHandler).when(mSplitController).getHandler(); } @Test @@ -113,7 +118,7 @@ public class JetpackTaskFragmentOrganizerTest { @Test public void testExpandTaskFragment() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, new Intent(), taskContainer, mSplitController); final TaskFragmentInfo info = createMockInfo(container); 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 58870a66feea..179696a063b1 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 @@ -19,6 +19,7 @@ package androidx.window.extensions.embedding; import static android.app.ActivityManager.START_CANCELED; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; @@ -27,15 +28,20 @@ import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_I import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; -import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; 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.TEST_TAG; 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.createTestTaskContainer; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -73,14 +79,19 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.view.WindowInsets; +import android.view.WindowMetrics; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOrganizer; +import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import androidx.window.extensions.layout.WindowLayoutInfo; import org.junit.Before; import org.junit.Test; @@ -116,6 +127,8 @@ public class SplitControllerTest { private WindowContainerTransaction mTransaction; @Mock private Handler mHandler; + @Mock + private WindowLayoutComponentImpl mWindowLayoutComponent; private SplitController mSplitController; private SplitPresenter mSplitPresenter; @@ -123,7 +136,9 @@ public class SplitControllerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mSplitController = new SplitController(); + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + mSplitController = new SplitController(mWindowLayoutComponent); mSplitPresenter = mSplitController.mPresenter; spyOn(mSplitController); spyOn(mSplitPresenter); @@ -138,7 +153,7 @@ public class SplitControllerTest { @Test public void testGetTopActiveContainer() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); // tf1 has no running activity so is not active. final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, new Intent(), taskContainer, mSplitController); @@ -198,6 +213,7 @@ public class SplitControllerTest { @Test public void testOnTaskFragmentAppearEmptyTimeout() { final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); + doCallRealMethod().when(mSplitController).onTaskFragmentAppearEmptyTimeout(any(), any()); mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf); verify(mSplitPresenter).cleanupContainer(mTransaction, tf, @@ -268,6 +284,8 @@ public class SplitControllerTest { final SplitContainer splitContainer = mock(SplitContainer.class); doReturn(tf).when(splitContainer).getPrimaryContainer(); doReturn(tf).when(splitContainer).getSecondaryContainer(); + doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer(); + doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule(); final List<SplitContainer> splitContainers = mSplitController.getTaskContainer(TASK_ID).mSplitContainers; splitContainers.add(splitContainer); @@ -298,7 +316,7 @@ public class SplitControllerTest { // Verify if the top active split is updated if both of its containers are not finished. doReturn(false).when(mSplitController) - .dismissPlaceholderIfNecessary(mTransaction, splitContainer); + .dismissPlaceholderIfNecessary(mTransaction, splitContainer); mSplitController.updateContainer(mTransaction, tf); @@ -308,7 +326,7 @@ public class SplitControllerTest { @Test public void testOnStartActivityResultError() { final Intent intent = new Intent(); - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, intent, taskContainer, mSplitController); final SplitController.ActivityStartMonitor monitor = @@ -608,7 +626,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -624,7 +642,7 @@ public class SplitControllerTest { assertFalse(result); verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(), - anyBoolean()); + any(), anyBoolean()); } @Test @@ -641,7 +659,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -656,7 +674,7 @@ public class SplitControllerTest { assertFalse(result); verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(), - anyBoolean()); + any(), anyBoolean()); } @Test @@ -674,7 +692,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT, mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */), - placeholderRule, true /* isPlaceholder */); + placeholderRule, SPLIT_ATTRIBUTES, true /* isPlaceholder */); } @Test @@ -693,14 +711,15 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - splitRule); + splitRule, + SPLIT_ATTRIBUTES); clearInvocations(mSplitController); final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); assertTrue(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); - verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @Test @@ -720,7 +739,8 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - splitRule); + splitRule, + SPLIT_ATTRIBUTES); final Activity launchedActivity = createMockActivity(); primaryContainer.addPendingAppearedActivity(launchedActivity); @@ -741,7 +761,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt()); - verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @Test @@ -778,7 +798,8 @@ public class SplitControllerTest { primaryContainer, mActivity, secondaryContainer, - placeholderRule); + placeholderRule, + SPLIT_ATTRIBUTES); final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity, false /* isOnReparent */); @@ -1038,15 +1059,16 @@ public class SplitControllerTest { @Test public void testOnTransactionReady_taskFragmentParentInfoChanged() { final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); - final Configuration taskConfig = new Configuration(); + final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(Configuration.EMPTY, + DEFAULT_DISPLAY, true); transaction.addChange(new TaskFragmentTransaction.Change( TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED) .setTaskId(TASK_ID) - .setTaskConfiguration(taskConfig)); + .setTaskFragmentParentInfo(parentInfo)); mSplitController.onTransactionReady(transaction); verify(mSplitController).onTaskFragmentParentInfoChanged(any(), eq(TASK_ID), - eq(taskConfig)); + eq(parentInfo)); verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), anyInt(), anyBoolean()); } @@ -1088,6 +1110,47 @@ public class SplitControllerTest { anyInt(), anyBoolean()); } + @Test + public void testHasSamePresentation() { + SplitPairRule splitRule1 = new SplitPairRule.Builder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> true) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .build(); + SplitPairRule splitRule2 = new SplitPairRule.Builder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> true) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .build(); + + assertTrue("Rules must have same presentation if tags are null and has same properties.", + SplitController.haveSamePresentation(splitRule1, splitRule2, + new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); + + splitRule2 = new SplitPairRule.Builder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> true) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .setTag(TEST_TAG) + .build(); + + assertFalse("Rules must have different presentations if tags are not equal regardless" + + "of other properties", + SplitController.haveSamePresentation(splitRule1, splitRule2, + new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); + + + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); @@ -1097,6 +1160,7 @@ public class SplitControllerTest { doReturn(activity).when(mSplitController).getActivity(activityToken); doReturn(TASK_ID).when(activity).getTaskId(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); + doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); return activity; } @@ -1135,7 +1199,7 @@ public class SplitControllerTest { private void setupPlaceholderRule(@NonNull Activity primaryActivity) { final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT, primaryActivity::equals, i -> false, w -> true) - .setSplitRatio(SPLIT_RATIO) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) .build(); mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); } @@ -1188,7 +1252,8 @@ public class SplitControllerTest { primaryContainer, primaryContainer.getTopNonFinishingActivity(), secondaryContainer, - rule); + rule, + SPLIT_ATTRIBUTES); // We need to set those in case we are not respecting clear top. // TODO(b/231845476) we should always respect clearTop. 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 25f0e25eec75..6dae0a1086b3 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 @@ -16,23 +16,28 @@ package androidx.window.extensions.embedding; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.Display.DEFAULT_DISPLAY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; 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.createWindowLayoutInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; +import static androidx.window.extensions.embedding.SplitPresenter.EXPAND_CONTAINERS_ATTRIBUTES; 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; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -66,6 +71,8 @@ import android.window.WindowContainerTransaction; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import androidx.window.extensions.layout.WindowLayoutInfo; import org.junit.Before; import org.junit.Test; @@ -73,6 +80,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; + /** * Test class for {@link SplitPresenter}. * @@ -94,13 +103,17 @@ public class SplitPresenterTest { private TaskFragmentInfo mTaskFragmentInfo; @Mock private WindowContainerTransaction mTransaction; + @Mock + private WindowLayoutComponentImpl mWindowLayoutComponent; private SplitController mController; private SplitPresenter mPresenter; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new SplitController(); + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + mController = new SplitController(mWindowLayoutComponent); mPresenter = mController.mPresenter; spyOn(mController); spyOn(mPresenter); @@ -162,59 +175,263 @@ public class SplitPresenterTest { @Test public void testShouldShowSideBySide() { - Activity secondaryActivity = createMockActivity(); - final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); + assertTrue(SplitPresenter.shouldShowSplit(SPLIT_ATTRIBUTES)); + + final SplitAttributes expandContainers = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType()) + .build(); - assertTrue(shouldShowSideBySide(TASK_BOUNDS, splitRule)); + assertFalse(SplitPresenter.shouldShowSplit(expandContainers)); + } - // Set minDimensions of primary container to larger than primary bounds. - final Rect primaryBounds = getSplitBounds(true /* isPrimary */); - Pair<Size, Size> minDimensionsPair = new Pair<>( - new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + @Test + public void testGetBoundsForPosition_expandContainers() { + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType()) + .build(); - assertFalse(shouldShowSideBySide(TASK_BOUNDS, splitRule, minDimensionsPair)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); } @Test - public void testGetBoundsForPosition() { - Activity secondaryActivity = createMockActivity(); - final SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); - final Rect primaryBounds = getSplitBounds(true /* isPrimary */); - final Rect secondaryBounds = getSplitBounds(false /* isPrimary */); + public void testGetBoundsForPosition_splitVertically() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + false /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + false /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.RIGHT_TO_LEFT) + .build(); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); assertEquals("Primary bounds must be reported.", primaryBounds, - getBoundsForPosition(POSITION_START, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE) + .build(); + // Layout direction should follow screen layout for SplitAttributes.LayoutDirection.LOCALE. + taskProperties.getConfiguration().screenLayout |= Configuration.SCREENLAYOUT_LAYOUTDIR_RTL; assertEquals("Secondary bounds must be reported.", secondaryBounds, - getBoundsForPosition(POSITION_END, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_splitHorizontally() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + true /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + true /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM) + .build(); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + splitAttributes = new SplitAttributes.Builder() + .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually()) + .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP) + .build(); + + assertEquals("Secondary bounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("Primary bounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_useHingeFallback() { + final Rect primaryBounds = getSplitBounds(true /* isPrimary */, + false /* splitHorizontally */); + final Rect secondaryBounds = getSplitBounds(false /* isPrimary */, + false /* splitHorizontally */); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + SplitAttributes.SplitType.RatioSplitType.splitEqually() + )).setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); + + // There's no hinge on the device. Use fallback SplitType. + doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + // Hinge is reported, but the host task is in multi-window mode. Still use fallback + // splitType. + doReturn(createWindowLayoutInfo()).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + taskProperties.getConfiguration().windowConfiguration + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + + // Hinge is reported, and the host task is in fullscreen, but layout direction doesn't match + // folding area orientation. Still use fallback splitType. + doReturn(createWindowLayoutInfo()).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + taskProperties.getConfiguration().windowConfiguration + .setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } + + @Test + public void testGetBoundsForPosition_fallbackToExpandContainers() { + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + new SplitAttributes.SplitType.ExpandContainersSplitType() + )).setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT) + .build(); + assertEquals("Task bounds must be reported.", new Rect(), - getBoundsForPosition(POSITION_FILL, TASK_BOUNDS, splitRule, - mActivity, null /* miniDimensionsPair */)); + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); - Pair<Size, Size> minDimensionsPair = new Pair<>( - new Size(primaryBounds.width() + 1, primaryBounds.height() + 1), null); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", + new Rect(), + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); + } - assertEquals("Fullscreen bounds must be reported because of min dimensions.", + @Test + public void testGetBoundsForPosition_useHingeSplitType() { + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + new SplitAttributes.SplitType.ExpandContainersSplitType() + )).setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM) + .build(); + final WindowLayoutInfo windowLayoutInfo = createWindowLayoutInfo(); + doReturn(windowLayoutInfo).when(mWindowLayoutComponent) + .getCurrentWindowLayoutInfo(anyInt(), any()); + final Rect hingeBounds = windowLayoutInfo.getDisplayFeatures().get(0).getBounds(); + final Rect primaryBounds = new Rect( + TASK_BOUNDS.left, + TASK_BOUNDS.top, + TASK_BOUNDS.right, + hingeBounds.top + ); + final Rect secondaryBounds = new Rect( + TASK_BOUNDS.left, + hingeBounds.bottom, + TASK_BOUNDS.right, + TASK_BOUNDS.bottom + ); + + assertEquals("PrimaryBounds must be reported.", + primaryBounds, + mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes)); + + assertEquals("SecondaryBounds must be reported.", + secondaryBounds, + mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes)); + assertEquals("Task bounds must be reported.", new Rect(), - getBoundsForPosition(POSITION_START, TASK_BOUNDS, - splitRule, mActivity, minDimensionsPair)); + mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes)); } @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(); + SplitContainer splitContainer = new SplitContainer(primaryTf, secondaryActivity, + secondaryTf, splitRule, SPLIT_ATTRIBUTES); assertThrows(IllegalArgumentException.class, () -> mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, @@ -224,11 +441,13 @@ public class SplitPresenterTest { splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); verify(mPresenter, never()).expandTaskFragment(any(), any()); + splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES); doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo(); assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded( mTransaction, splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); + splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES); primaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(primaryTf, mActivity)); secondaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(secondaryTf, secondaryActivity)); @@ -238,6 +457,7 @@ public class SplitPresenterTest { verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES); clearInvocations(mPresenter); assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, @@ -256,6 +476,7 @@ public class SplitPresenterTest { final SplitPairRule rule = new SplitPairRule.Builder(pair -> pair.first == mActivity && pair.second == secondaryActivity, pair -> false, metrics -> true) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) .setShouldClearTop(false) .build(); @@ -268,6 +489,49 @@ public class SplitPresenterTest { assertTrue(secondaryTf.isAbove(primaryTf)); } + @Test + public void testComputeSplitAttributes() { + final SplitPairRule splitPairRule = new SplitPairRule.Builder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> windowMetrics.getBounds().equals(TASK_BOUNDS)) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) + .build(); + final TaskContainer.TaskProperties taskProperties = getTaskProperty(); + + assertEquals(SPLIT_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, null /* minDimensionsPair */)); + + final Pair<Size, Size> minDimensionsPair = new Pair<>( + new Size(TASK_BOUNDS.width(), TASK_BOUNDS.height()), null); + + assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, minDimensionsPair)); + + taskProperties.getConfiguration().windowConfiguration.setBounds(new Rect( + TASK_BOUNDS.left + 1, TASK_BOUNDS.top + 1, TASK_BOUNDS.right + 1, + TASK_BOUNDS.bottom + 1)); + + assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, null /* minDimensionsPair */)); + + final SplitAttributes splitAttributes = new SplitAttributes.Builder() + .setSplitType( + new SplitAttributes.SplitType.HingeSplitType( + SplitAttributes.SplitType.RatioSplitType.splitEqually() + ) + ).build(); + + mController.setSplitAttributesCalculator(params -> { + return splitAttributes; + }); + + assertEquals(splitAttributes, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, null /* minDimensionsPair */)); + } + private Activity createMockActivity() { final Activity activity = mock(Activity.class); final Configuration activityConfig = new Configuration(); @@ -279,4 +543,10 @@ public class SplitPresenterTest { doReturn(mock(IBinder.class)).when(activity).getActivityToken(); return activity; } + + private static TaskContainer.TaskProperties getTaskProperty() { + final Configuration configuration = new Configuration(); + configuration.windowConfiguration.setBounds(TASK_BOUNDS); + return new TaskContainer.TaskProperties(DEFAULT_DISPLAY, configuration); + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index dd67e48ef353..af9c6ba5c162 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -21,9 +21,10 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; 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.createTestTaskContainer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -34,8 +35,10 @@ import static org.mockito.Mockito.mock; import android.app.Activity; import android.content.Intent; +import android.content.res.Configuration; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.window.TaskFragmentParentInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -66,7 +69,7 @@ public class TaskContainerTest { @Test public void testIsTaskBoundsInitialized() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertFalse(taskContainer.isTaskBoundsInitialized()); @@ -77,7 +80,7 @@ public class TaskContainerTest { @Test public void testSetTaskBounds() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertFalse(taskContainer.setTaskBounds(new Rect())); @@ -87,30 +90,24 @@ public class TaskContainerTest { } @Test - public void testIsWindowingModeInitialized() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); - - assertFalse(taskContainer.isWindowingModeInitialized()); - - taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - - assertTrue(taskContainer.isWindowingModeInitialized()); - } - - @Test public void testGetWindowingModeForSplitTaskFragment() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final Rect splitBounds = new Rect(0, 0, 500, 1000); + final Configuration configuration = new Configuration(); assertEquals(WINDOWING_MODE_MULTI_WINDOW, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); - taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertEquals(WINDOWING_MODE_MULTI_WINDOW, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); - taskContainer.setWindowingMode(WINDOWING_MODE_FREEFORM); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertEquals(WINDOWING_MODE_FREEFORM, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); @@ -123,22 +120,27 @@ public class TaskContainerTest { @Test public void testIsInPictureInPicture() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); + final Configuration configuration = new Configuration(); assertFalse(taskContainer.isInPictureInPicture()); - taskContainer.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertFalse(taskContainer.isInPictureInPicture()); - taskContainer.setWindowingMode(WINDOWING_MODE_PINNED); + configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_PINNED); + taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, + DEFAULT_DISPLAY, true /* visible */)); assertTrue(taskContainer.isInPictureInPicture()); } @Test public void testIsEmpty() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertTrue(taskContainer.isEmpty()); @@ -155,7 +157,7 @@ public class TaskContainerTest { @Test public void testGetTopTaskFragmentContainer() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertNull(taskContainer.getTopTaskFragmentContainer()); final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, @@ -169,7 +171,7 @@ public class TaskContainerTest { @Test public void testGetTopNonFinishingActivity() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); assertNull(taskContainer.getTopNonFinishingActivity()); final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 082774e048a9..73428a2dc800 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -16,8 +16,8 @@ package androidx.window.extensions.embedding; -import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -90,7 +90,7 @@ public class TaskFragmentContainerTest { @Test public void testNewContainer() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); // One of the activity and the intent must be non-null assertThrows(IllegalArgumentException.class, @@ -103,7 +103,7 @@ public class TaskFragmentContainerTest { @Test public void testFinish() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, null /* pendingAppearedIntent */, taskContainer, mController); doReturn(container).when(mController).getContainerWithActivity(mActivity); @@ -136,7 +136,7 @@ public class TaskFragmentContainerTest { @Test public void testFinish_notFinishActivityThatIsReparenting() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity, null /* pendingAppearedIntent */, taskContainer, mController); final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity); @@ -157,7 +157,7 @@ public class TaskFragmentContainerTest { @Test public void testSetInfo() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); // Pending activity should be cleared when it has appeared on server side. final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity, null /* pendingAppearedIntent */, taskContainer, mController); @@ -185,7 +185,7 @@ public class TaskFragmentContainerTest { @Test public void testIsWaitingActivityAppear() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); @@ -207,7 +207,7 @@ public class TaskFragmentContainerTest { @Test public void testAppearEmptyTimeout() { doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any()); - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); @@ -247,7 +247,7 @@ public class TaskFragmentContainerTest { @Test public void testCollectNonFinishingActivities() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); List<Activity> activities = container.collectNonFinishingActivities(); @@ -275,7 +275,7 @@ public class TaskFragmentContainerTest { @Test public void testAddPendingActivity() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); container.addPendingAppearedActivity(mActivity); @@ -289,7 +289,7 @@ public class TaskFragmentContainerTest { @Test public void testIsAbove() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container0 = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */, @@ -301,7 +301,7 @@ public class TaskFragmentContainerTest { @Test public void testGetBottomMostActivity() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); container.addPendingAppearedActivity(mActivity); @@ -318,7 +318,7 @@ public class TaskFragmentContainerTest { @Test public void testOnActivityDestroyed() { - final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskContainer taskContainer = createTestTaskContainer(); final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, mIntent, taskContainer, mController); container.addPendingAppearedActivity(mActivity); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java index 9230c22c5d95..ca977ed2cb94 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -179,6 +179,14 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { } /** + * Returns the {@link DisplayAreaInfo} of the {@link DisplayAreaInfo#displayId}. + */ + @Nullable + public DisplayAreaInfo getDisplayAreaInfo(int displayId) { + return mDisplayAreasInfo.get(displayId); + } + + /** * Applies the {@link DisplayAreaInfo} to the {@link DisplayAreaContext} specified by * {@link DisplayAreaInfo#displayId}. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index d88cc007c7b5..d150261b31b8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -18,6 +18,7 @@ package com.android.wm.shell.activityembedding; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; +import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import android.animation.Animator; import android.animation.ValueAnimator; @@ -129,12 +130,20 @@ class ActivityEmbeddingAnimationRunner { @NonNull private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters( @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) { + boolean isChangeTransition = false; for (TransitionInfo.Change change : info.getChanges()) { - if (change.getMode() == TRANSIT_CHANGE + if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) { + // Skip the animation if the windows are behind an app starting window. + return new ArrayList<>(); + } + if (!isChangeTransition && change.getMode() == TRANSIT_CHANGE && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) { - return createChangeAnimationAdapters(info, startTransaction); + isChangeTransition = true; } } + if (isChangeTransition) { + return createChangeAnimationAdapters(info, startTransaction); + } if (Transitions.isClosingType(info.getType())) { return createCloseAnimationAdapters(info); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 27d3e35fa07a..35e88e9abb3c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -27,7 +27,6 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; -import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.TaskViewTransitions; @@ -599,13 +598,13 @@ public abstract class WMShellModule { static Optional<DesktopModeController> provideDesktopModeController( Context context, ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, - RootDisplayAreaOrganizer rootDisplayAreaOrganizer, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, @ShellMainThread Handler mainHandler, Transitions transitions ) { if (DesktopMode.IS_SUPPORTED) { return Optional.of(new DesktopModeController(context, shellInit, shellTaskOrganizer, - rootDisplayAreaOrganizer, mainHandler, transitions)); + rootTaskDisplayAreaOrganizer, mainHandler, transitions)); } else { return Optional.empty(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index c07ce1065302..6e44d58cffae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -22,19 +22,21 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; +import android.app.WindowConfiguration; import android.content.Context; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; +import android.window.DisplayAreaInfo; import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.RootDisplayAreaOrganizer; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; @@ -47,18 +49,18 @@ public class DesktopModeController { private final Context mContext; private final ShellTaskOrganizer mShellTaskOrganizer; - private final RootDisplayAreaOrganizer mRootDisplayAreaOrganizer; + private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final SettingsObserver mSettingsObserver; private final Transitions mTransitions; public DesktopModeController(Context context, ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, - RootDisplayAreaOrganizer rootDisplayAreaOrganizer, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, @ShellMainThread Handler mainHandler, Transitions transitions) { mContext = context; mShellTaskOrganizer = shellTaskOrganizer; - mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer; + mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mSettingsObserver = new SettingsObserver(mContext, mainHandler); mTransitions = transitions; shellInit.addInitCallback(this::onInit, this); @@ -92,15 +94,32 @@ public class DesktopModeController { wct.merge(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(displayId), true /* transfer */); } - wct.merge(mRootDisplayAreaOrganizer.prepareWindowingModeChange(displayId, - targetWindowingMode), true /* transfer */); + prepareWindowingModeChange(wct, displayId, targetWindowingMode); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mTransitions.startTransition(TRANSIT_CHANGE, wct, null); } else { - mRootDisplayAreaOrganizer.applyTransaction(wct); + mRootTaskDisplayAreaOrganizer.applyTransaction(wct); } } + private void prepareWindowingModeChange(WindowContainerTransaction wct, + int displayId, @WindowConfiguration.WindowingMode int windowingMode) { + DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer + .getDisplayAreaInfo(displayId); + if (displayAreaInfo == null) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, + "unable to update windowing mode for display %d display not found", displayId); + return; + } + + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId, + displayAreaInfo.configuration.windowConfiguration.getWindowingMode(), + windowingMode); + + wct.setWindowingMode(displayAreaInfo.token, windowingMode); + } + /** * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE} */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 3758471664cc..991f136c0055 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -133,6 +133,20 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Retention(RetentionPolicy.SOURCE) @interface ExitReason{} + public static final int ENTER_REASON_UNKNOWN = 0; + public static final int ENTER_REASON_MULTI_INSTANCE = 1; + public static final int ENTER_REASON_DRAG = 2; + public static final int ENTER_REASON_LAUNCHER = 3; + /** Acts as a mapping to the actual EnterReasons as defined in the logging proto */ + @IntDef(value = { + ENTER_REASON_MULTI_INSTANCE, + ENTER_REASON_DRAG, + ENTER_REASON_LAUNCHER, + ENTER_REASON_UNKNOWN + }) + public @interface SplitEnterReason { + } + private final ShellCommandHandler mShellCommandHandler; private final ShellController mShellController; private final ShellTaskOrganizer mTaskOrganizer; @@ -394,7 +408,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, */ public void startShortcut(String packageName, String shortcutId, @SplitPosition int position, @Nullable Bundle options, UserHandle user, @NonNull InstanceId instanceId) { - mStageCoordinator.getLogger().enterRequested(instanceId); + mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER); startShortcut(packageName, shortcutId, position, options, user); } @@ -442,7 +456,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, */ public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options, @NonNull InstanceId instanceId) { - mStageCoordinator.getLogger().enterRequested(instanceId); + mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER); startIntent(intent, fillInIntent, position, options); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java index 626ccb1d2890..2dc4a0441b06 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java @@ -17,6 +17,8 @@ package com.android.wm.shell.splitscreen; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__MULTI_INSTANCE; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED; @@ -28,6 +30,10 @@ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED_ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_DRAG; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_UNKNOWN; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED; @@ -38,6 +44,7 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN; +import android.annotation.Nullable; import android.util.Slog; import com.android.internal.logging.InstanceId; @@ -59,7 +66,7 @@ public class SplitscreenEventLogger { // Drag info private @SplitPosition int mDragEnterPosition; - private InstanceId mEnterSessionId; + private @Nullable InstanceId mEnterSessionId; // For deduping async events private int mLastMainStagePosition = -1; @@ -67,6 +74,7 @@ public class SplitscreenEventLogger { private int mLastSideStagePosition = -1; private int mLastSideStageUid = -1; private float mLastSplitRatio = -1f; + private @SplitScreenController.SplitEnterReason int mEnterReason = ENTER_REASON_UNKNOWN; public SplitscreenEventLogger() { mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE); @@ -79,20 +87,35 @@ public class SplitscreenEventLogger { return mLoggerSessionId != null; } + public boolean isEnterRequestedByDrag() { + return mEnterReason == ENTER_REASON_DRAG; + } + /** * May be called before logEnter() to indicate that the session was started from a drag. */ public void enterRequestedByDrag(@SplitPosition int position, InstanceId enterSessionId) { mDragEnterPosition = position; - enterRequested(enterSessionId); + enterRequested(enterSessionId, ENTER_REASON_DRAG); } /** * May be called before logEnter() to indicate that the session was started from launcher. * This specifically is for all the scenarios where split started without a drag interaction */ - public void enterRequested(InstanceId enterSessionId) { + public void enterRequested(@Nullable InstanceId enterSessionId, + @SplitScreenController.SplitEnterReason int enterReason) { mEnterSessionId = enterSessionId; + mEnterReason = enterReason; + } + + /** + * @return if an enterSessionId has been set via either + * {@link #enterRequested(InstanceId, int)} or + * {@link #enterRequestedByDrag(int, InstanceId)} + */ + public boolean hasValidEnterSessionId() { + return mEnterSessionId != null; } /** @@ -103,9 +126,7 @@ public class SplitscreenEventLogger { @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { mLoggerSessionId = mIdSequence.newInstanceId(); - int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED - ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape) - : SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER; + int enterReason = getLoggerEnterReason(isLandscape); updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), mainStageUid); updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), @@ -124,6 +145,20 @@ public class SplitscreenEventLogger { mLoggerSessionId.getId()); } + private int getLoggerEnterReason(boolean isLandscape) { + switch (mEnterReason) { + case ENTER_REASON_MULTI_INSTANCE: + return SPLITSCREEN_UICHANGED__ENTER_REASON__MULTI_INSTANCE; + case ENTER_REASON_LAUNCHER: + return SPLITSCREEN_UICHANGED__ENTER_REASON__LAUNCHER; + case ENTER_REASON_DRAG: + return getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape); + case ENTER_REASON_UNKNOWN: + default: + return SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER; + } + } + /** * Returns the framework logging constant given a splitscreen exit reason. */ @@ -189,6 +224,7 @@ public class SplitscreenEventLogger { mLastMainStageUid = -1; mLastSideStagePosition = -1; mLastSideStageUid = -1; + mEnterReason = ENTER_REASON_UNKNOWN; } /** 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 25793ed0317b..c17f8226c925 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 @@ -18,6 +18,8 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; @@ -44,6 +46,8 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER; +import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; @@ -501,6 +505,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct); + // If split still not active, apply windows bounds first to avoid surface reset to + // wrong pos by SurfaceAnimator from wms. + if (!mMainStage.isActive() && mLogger.isEnterRequestedByDrag()) { + updateWindowBounds(mSplitLayout, wct); + } + wct.sendPendingIntent(intent, fillInIntent, options); mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); } @@ -669,7 +679,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void setEnterInstanceId(InstanceId instanceId) { if (instanceId != null) { - mLogger.enterRequested(instanceId); + mLogger.enterRequested(instanceId, ENTER_REASON_LAUNCHER); } } @@ -1107,6 +1117,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void addActivityOptions(Bundle opts, StageTaskListener stage) { opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); + // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split + // will be canceled. + opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); + opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); } void updateActivityOptions(Bundle opts, @SplitPosition int position) { @@ -1453,18 +1467,27 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } } else if (isSideStage && hasChildren && !mMainStage.isActive()) { - // TODO (b/238697912) : Add the validation to prevent entering non-recovered status - onSplitScreenEnter(); final WindowContainerTransaction wct = new WindowContainerTransaction(); mSplitLayout.init(); - mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); - mMainStage.activate(wct, true /* includingTopTask */); - updateWindowBounds(mSplitLayout, wct); - wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + if (mLogger.isEnterRequestedByDrag()) { + prepareEnterSplitScreen(wct); + } else { + // TODO (b/238697912) : Add the validation to prevent entering non-recovered status + onSplitScreenEnter(); + mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); + mMainStage.activate(wct, true /* includingTopTask */); + updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); + } + mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { - mSplitLayout.flingDividerToCenter(); + if (mLogger.isEnterRequestedByDrag()) { + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); + } else { + mSplitLayout.flingDividerToCenter(); + } }); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { @@ -1472,6 +1495,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateRecentTasksSplitPair(); if (!mLogger.hasStartedSession()) { + if (!mLogger.hasValidEnterSessionId()) { + mLogger.enterRequested(null /*enterSessionId*/, ENTER_REASON_MULTI_INSTANCE); + } mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), getMainStagePosition(), mMainStage.getTopChildTaskUid(), getSideStagePosition(), mSideStage.getTopChildTaskUid(), diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index 3ca5b9c38aff..d6adaa7d533b 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -48,6 +48,6 @@ android_test { "wm-flicker-common-assertions", "wm-flicker-common-app-helpers", "platform-test-annotations", - "wmshell-flicker-test-components", + "flickertestapplib", ], } diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml index 574a9f4da627..1284c41af855 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml @@ -19,7 +19,7 @@ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true"/> <option name="test-file-name" value="WMShellFlickerTests.apk"/> - <option name="test-file-name" value="WMShellFlickerTestApp.apk" /> + <option name="test-file-name" value="FlickerTestApp.apk" /> </target_preparer> <test class="com.android.tradefed.testtype.AndroidJUnitTest"> <option name="package" value="com.android.wm.shell.flicker"/> diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt new file mode 100644 index 000000000000..c045325f19c3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.app.Instrumentation +import android.content.Context +import android.provider.Settings +import android.util.Log +import com.android.compatibility.common.util.SystemUtil +import java.io.IOException + +object MultiWindowUtils { + private fun executeShellCommand(instrumentation: Instrumentation, cmd: String) { + try { + SystemUtil.runShellCommand(instrumentation, cmd) + } catch (e: IOException) { + Log.e(MultiWindowUtils::class.simpleName, "executeShellCommand error! $e") + } + } + + fun getDevEnableNonResizableMultiWindow(context: Context): Int = + Settings.Global.getInt(context.contentResolver, + Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW) + + fun setDevEnableNonResizableMultiWindow(context: Context, configValue: Int) = + Settings.Global.putInt(context.contentResolver, + Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW, configValue) + + fun setSupportsNonResizableMultiWindow(instrumentation: Instrumentation, configValue: Int) = + executeShellCommand( + instrumentation, + createConfigSupportsNonResizableMultiWindowCommand(configValue)) + + fun resetMultiWindowConfig(instrumentation: Instrumentation) = + executeShellCommand(instrumentation, resetMultiWindowConfigCommand) + + private fun createConfigSupportsNonResizableMultiWindowCommand(configValue: Int): String = + "wm set-multi-window-config --supportsNonResizable $configValue" + + private const val resetMultiWindowConfigCommand: String = "wm reset-multi-window-config" +} 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 1390334f7938..cbe085be8952 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 @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.bubble import android.app.INotificationManager import android.app.NotificationManager import android.content.Context +import android.content.pm.PackageManager import android.os.ServiceManager import android.view.Surface import androidx.test.uiautomator.By @@ -28,9 +29,9 @@ import com.android.server.wm.flicker.Flicker import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.LaunchBubbleHelper import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE import com.android.wm.shell.flicker.BaseTest -import com.android.wm.shell.flicker.helpers.LaunchBubbleHelper import org.junit.runners.Parameterized /** @@ -47,7 +48,7 @@ abstract class BaseBubbleScreen( ServiceManager.getService(Context.NOTIFICATION_SERVICE)) private val uid = context.packageManager.getApplicationInfo( - testApp.`package`, 0).uid + testApp.`package`, PackageManager.ApplicationInfoFlags.of(0)).uid @JvmOverloads protected open fun buildTransition( 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 deleted file mode 100644 index 01ba9907c24c..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.helpers - -import android.app.Instrumentation -import android.content.pm.PackageManager.FEATURE_LEANBACK -import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY -import android.support.test.launcherhelper.LauncherStrategyFactory -import android.util.Log -import androidx.test.uiautomator.By -import androidx.test.uiautomator.UiObject2 -import androidx.test.uiautomator.Until -import com.android.compatibility.common.util.SystemUtil -import com.android.server.wm.flicker.helpers.StandardAppHelper -import com.android.server.wm.traces.common.IComponentNameMatcher -import java.io.IOException - -abstract class BaseAppHelper( - instrumentation: Instrumentation, - launcherName: String, - component: IComponentNameMatcher -) : StandardAppHelper( - instrumentation, - launcherName, - component, - LauncherStrategyFactory.getInstance(instrumentation).launcherStrategy -) { - private val appSelector = By.pkg(`package`).depth(0) - - protected val isTelevision: Boolean - get() = context.packageManager.run { - hasSystemFeature(FEATURE_LEANBACK) || hasSystemFeature(FEATURE_LEANBACK_ONLY) - } - - val ui: UiObject2? - get() = uiDevice.findObject(appSelector) - - fun waitUntilClosed(): Boolean { - return uiDevice.wait(Until.gone(appSelector), APP_CLOSE_WAIT_TIME_MS) - } - - companion object { - private const val APP_CLOSE_WAIT_TIME_MS = 3_000L - - fun executeShellCommand(instrumentation: Instrumentation, cmd: String) { - try { - SystemUtil.runShellCommand(instrumentation, cmd) - } catch (e: IOException) { - Log.e("BaseAppHelper", "executeShellCommand error! $e") - } - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt deleted file mode 100644 index 471e010cf560..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.helpers - -import android.app.Instrumentation -import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.wm.shell.flicker.testapp.Components - -class FixedAppHelper(instrumentation: Instrumentation) : BaseAppHelper( - instrumentation, - Components.FixedActivity.LABEL, - Components.FixedActivity.COMPONENT.toFlickerComponent() -)
\ No newline at end of file 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 deleted file mode 100644 index 2e690de666f4..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.helpers - -import android.app.Instrumentation -import androidx.test.uiautomator.By -import androidx.test.uiautomator.Until -import com.android.server.wm.flicker.helpers.FIND_TIMEOUT -import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.testapp.Components - -open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( - instrumentation, - Components.ImeActivity.LABEL, - Components.ImeActivity.COMPONENT.toFlickerComponent() -) { - /** - * Opens the IME and wait for it to be displayed - * - * @param wmHelper Helper used to wait for WindowManager states - */ - open fun openIME(wmHelper: WindowManagerStateHelper) { - if (!isTelevision) { - val editText = uiDevice.wait( - Until.findObject(By.res(getPackage(), "plain_text_input")), - 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() - 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. - launchViaIntent(action = Components.ImeActivity.ACTION_OPEN_IME) - } - } - - /** - * Opens the IME and wait for it to be gone - * - * @param wmHelper Helper used to wait for WindowManager states - */ - open fun closeIME(wmHelper: WindowManagerStateHelper) { - if (!isTelevision) { - uiDevice.pressBack() - // Using only the AccessibilityInfo it is not possible to identify if the IME is active - 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) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt deleted file mode 100644 index 52e5d7e14ab9..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.helpers - -import android.app.Instrumentation -import android.graphics.Point -import android.os.SystemClock -import android.view.InputDevice -import android.view.MotionEvent -import android.view.ViewConfiguration -import androidx.test.uiautomator.By -import androidx.test.uiautomator.BySelector -import androidx.test.uiautomator.UiDevice -import androidx.test.uiautomator.Until -import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.server.wm.traces.common.IComponentMatcher -import com.android.server.wm.traces.common.IComponentNameMatcher -import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.SPLIT_DECOR_MANAGER -import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME -import com.android.wm.shell.flicker.testapp.Components - -class SplitScreenHelper( - instrumentation: Instrumentation, - activityLabel: String, - componentInfo: IComponentNameMatcher -) : BaseAppHelper(instrumentation, activityLabel, componentInfo) { - - companion object { - const val TIMEOUT_MS = 3_000L - const val DRAG_DURATION_MS = 1_000L - const val NOTIFICATION_SCROLLER = "notification_stack_scroller" - const val DIVIDER_BAR = "docked_divider_handle" - const val GESTURE_STEP_MS = 16L - const val LONG_PRESS_TIME_MS = 100L - - private val notificationScrollerSelector: BySelector - get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER) - private val notificationContentSelector: BySelector - get() = By.text("Notification content") - private val dividerBarSelector: BySelector - get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR) - - fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper( - instrumentation, - Components.SplitScreenActivity.LABEL, - Components.SplitScreenActivity.COMPONENT.toFlickerComponent() - ) - - fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper( - instrumentation, - Components.SplitScreenSecondaryActivity.LABEL, - Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent() - ) - - fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper( - instrumentation, - Components.NonResizeableActivity.LABEL, - Components.NonResizeableActivity.COMPONENT.toFlickerComponent() - ) - - fun getSendNotification(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper( - instrumentation, - Components.SendNotificationActivity.LABEL, - Components.SendNotificationActivity.COMPONENT.toFlickerComponent() - ) - - fun getIme(instrumentation: Instrumentation): SplitScreenHelper = - SplitScreenHelper( - instrumentation, - Components.ImeActivity.LABEL, - Components.ImeActivity.COMPONENT.toFlickerComponent() - ) - - fun waitForSplitComplete( - wmHelper: WindowManagerStateHelper, - primaryApp: IComponentMatcher, - secondaryApp: IComponentMatcher, - ) { - wmHelper.StateSyncBuilder() - .withWindowSurfaceAppeared(primaryApp) - .withWindowSurfaceAppeared(secondaryApp) - .withSplitDividerVisible() - .waitForAndVerify() - } - - fun enterSplit( - wmHelper: WindowManagerStateHelper, - tapl: LauncherInstrumentation, - primaryApp: SplitScreenHelper, - secondaryApp: SplitScreenHelper - ) { - tapl.workspace.switchToOverview().dismissAllTasks() - primaryApp.launchViaIntent(wmHelper) - secondaryApp.launchViaIntent(wmHelper) - tapl.goHome() - wmHelper.StateSyncBuilder() - .withHomeActivityVisible() - .waitForAndVerify() - splitFromOverview(tapl) - waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } - - fun splitFromOverview(tapl: LauncherInstrumentation) { - // Note: The initial split position in landscape is different between tablet and phone. - // In landscape, tablet will let the first app split to right side, and phone will - // split to left side. - if (tapl.isTablet) { - tapl.workspace.switchToOverview().overviewActions - .clickSplit() - .currentTask - .open() - } else { - tapl.workspace.switchToOverview().currentTask - .tapMenu() - .tapSplitMenuItem() - .currentTask - .open() - } - SystemClock.sleep(TIMEOUT_MS) - } - - fun dragFromNotificationToSplit( - instrumentation: Instrumentation, - device: UiDevice, - wmHelper: WindowManagerStateHelper - ) { - val displayBounds = wmHelper.currentState.layerState - .displays.firstOrNull { !it.isVirtual } - ?.layerStackSpace - ?: error("Display not found") - - // Pull down the notifications - device.swipe( - displayBounds.centerX(), 5, - displayBounds.centerX(), displayBounds.bottom, 20 /* steps */ - ) - SystemClock.sleep(TIMEOUT_MS) - - // Find the target notification - val notificationScroller = device.wait( - Until.findObject(notificationScrollerSelector), TIMEOUT_MS - ) - var notificationContent = notificationScroller.findObject(notificationContentSelector) - - while (notificationContent == null) { - device.swipe( - displayBounds.centerX(), displayBounds.centerY(), - displayBounds.centerX(), displayBounds.centerY() - 150, 20 /* steps */ - ) - notificationContent = notificationScroller.findObject(notificationContentSelector) - } - - // Drag to split - val dragStart = notificationContent.visibleCenter - val dragMiddle = Point(dragStart.x + 50, dragStart.y) - val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4) - val downTime = SystemClock.uptimeMillis() - - touch( - instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, - TIMEOUT_MS, dragStart - ) - // It needs a horizontal movement to trigger the drag - touchMove( - instrumentation, downTime, SystemClock.uptimeMillis(), - DRAG_DURATION_MS, dragStart, dragMiddle - ) - touchMove( - instrumentation, downTime, SystemClock.uptimeMillis(), - DRAG_DURATION_MS, dragMiddle, dragEnd - ) - // Wait for a while to start splitting - SystemClock.sleep(TIMEOUT_MS) - touch( - instrumentation, MotionEvent.ACTION_UP, downTime, SystemClock.uptimeMillis(), - GESTURE_STEP_MS, dragEnd - ) - SystemClock.sleep(TIMEOUT_MS) - } - - fun touch( - instrumentation: Instrumentation, - action: Int, - downTime: Long, - eventTime: Long, - duration: Long, - point: Point - ) { - val motionEvent = MotionEvent.obtain( - downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0 - ) - motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN - instrumentation.uiAutomation.injectInputEvent(motionEvent, true) - motionEvent.recycle() - SystemClock.sleep(duration) - } - - fun touchMove( - instrumentation: Instrumentation, - downTime: Long, - eventTime: Long, - duration: Long, - from: Point, - to: Point - ) { - val steps: Long = duration / GESTURE_STEP_MS - var currentTime = eventTime - var currentX = from.x.toFloat() - var currentY = from.y.toFloat() - val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat() - val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat() - - for (i in 1..steps) { - val motionMove = MotionEvent.obtain( - downTime, currentTime, MotionEvent.ACTION_MOVE, currentX, currentY, 0 - ) - motionMove.source = InputDevice.SOURCE_TOUCHSCREEN - instrumentation.uiAutomation.injectInputEvent(motionMove, true) - motionMove.recycle() - - currentTime += GESTURE_STEP_MS - if (i == steps - 1) { - currentX = to.x.toFloat() - currentY = to.y.toFloat() - } else { - currentX += stepX - currentY += stepY - } - SystemClock.sleep(GESTURE_STEP_MS) - } - } - - fun longPress( - instrumentation: Instrumentation, - point: Point - ) { - val downTime = SystemClock.uptimeMillis() - touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, point) - SystemClock.sleep(LONG_PRESS_TIME_MS) - touch(instrumentation, MotionEvent.ACTION_UP, downTime, downTime, TIMEOUT_MS, point) - } - - fun createShortcutOnHotseatIfNotExist( - tapl: LauncherInstrumentation, - appName: String - ) { - tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0)) - val allApps = tapl.workspace.switchToAllApps() - allApps.freeze() - try { - allApps.getAppIcon(appName).dragToHotseat(0) - } finally { - allApps.unfreeze() - } - } - - fun dragDividerToResizeAndWait( - device: UiDevice, - wmHelper: WindowManagerStateHelper - ) { - val displayBounds = wmHelper.currentState.layerState - .displays.firstOrNull { !it.isVirtual } - ?.layerStackSpace - ?: error("Display not found") - val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) - dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3)) - - wmHelper.StateSyncBuilder() - .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER) - .waitForAndVerify() - } - - fun dragDividerToDismissSplit( - device: UiDevice, - wmHelper: WindowManagerStateHelper, - dragToRight: Boolean, - dragToBottom: Boolean - ) { - val displayBounds = wmHelper.currentState.layerState - .displays.firstOrNull { !it.isVirtual } - ?.layerStackSpace - ?: error("Display not found") - val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) - dividerBar.drag(Point( - if (dragToRight) { - displayBounds.width * 4 / 5 - } else { - displayBounds.width * 1 / 5 - }, - if (dragToBottom) { - displayBounds.height * 4 / 5 - } else { - displayBounds.height * 1 / 5 - })) - } - - fun doubleTapDividerToSwitch(device: UiDevice) { - val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) - val interval = (ViewConfiguration.getDoubleTapTimeout() + - ViewConfiguration.getDoubleTapMinTime()) / 2 - dividerBar.click() - SystemClock.sleep(interval.toLong()) - dividerBar.click() - } - - fun copyContentInSplit( - instrumentation: Instrumentation, - device: UiDevice, - sourceApp: IComponentNameMatcher, - destinationApp: IComponentNameMatcher, - ) { - // Copy text from sourceApp - val textView = device.wait(Until.findObject( - By.res(sourceApp.packageName, "SplitScreenTest")), TIMEOUT_MS) - longPress(instrumentation, textView.getVisibleCenter()) - - val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS) - copyBtn.click() - - // Paste text to destinationApp - val editText = device.wait(Until.findObject( - By.res(destinationApp.packageName, "plain_text_input")), TIMEOUT_MS) - longPress(instrumentation, editText.getVisibleCenter()) - - val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS) - pasteBtn.click() - - // Verify text - if (!textView.getText().contentEquals(editText.getText())) { - error("Fail to copy content in split") - } - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt deleted file mode 100644 index f9b08000290f..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (C) 2021 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. - */ - -@file:JvmName("CommonAssertions") -package com.android.wm.shell.flicker.pip - -internal const val PIP_WINDOW_COMPONENT = "PipMenuActivity" 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 87d800c3dc6a..4788507c1443 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 @@ -28,16 +28,17 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.entireScreenCovered +import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper 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.navBarLayerPositionAtStartAndEnd import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule -import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.server.wm.flicker.testapp.ActivityOptions.Pip.ACTION_ENTER_PIP +import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION +import com.android.server.wm.traces.common.ComponentNameMatcher 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.FixedActivity.EXTRA_FIXED_ORIENTATION -import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder @@ -72,7 +73,7 @@ import org.junit.runners.Parameterized class EnterPipToOtherOrientationTest( testSpec: FlickerTestParameter ) : PipTransition(testSpec) { - private val testApp = FixedAppHelper(instrumentation) + private val testApp = FixedOrientationAppHelper(instrumentation) private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) @@ -127,7 +128,7 @@ class EnterPipToOtherOrientationTest( } /** - * Checks that the [ComponentMatcher.NAV_BAR] has the correct position at + * Checks that the [ComponentNameMatcher.NAV_BAR] has the correct position at * the start and end of the transition */ @FlakyTest diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt index 45851c8b6326..628599160c65 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt @@ -18,14 +18,14 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit import com.android.server.wm.flicker.FlickerTestParameter -import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper import org.junit.Test /** * Base class for pip expand tests */ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - protected val testApp = FixedAppHelper(instrumentation) + protected val testApp = FixedOrientationAppHelper(instrumentation) /** * Checks that the pip app window remains inside the display bounds throughout the whole diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt index 19bdca51f8af..5f9419694c13 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt @@ -18,9 +18,9 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper import com.android.server.wm.flicker.traces.region.RegionSubject import com.android.wm.shell.flicker.Direction -import com.android.wm.shell.flicker.helpers.FixedAppHelper import org.junit.Test /** @@ -29,7 +29,7 @@ import org.junit.Test abstract class MovePipShelfHeightTransition( testSpec: FlickerTestParameter ) : PipTransition(testSpec) { - protected val testApp = FixedAppHelper(instrumentation) + protected val testApp = FixedOrientationAppHelper(instrumentation) /** * Checks [pipApp] window remains visible throughout the animation diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt index 3d3b53de3dc8..2aa0da95dfd5 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt @@ -24,11 +24,11 @@ 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.ImeAppHelper import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.traces.common.ComponentNameMatcher -import com.android.wm.shell.flicker.helpers.ImeAppHelper import org.junit.Assume.assumeFalse import org.junit.Before import org.junit.FixMethodOrder diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt index f13698f1d04e..0fce64ea073b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt @@ -25,10 +25,10 @@ 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.SimpleAppHelper import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.helpers.setRotation -import com.android.wm.shell.flicker.helpers.FixedAppHelper import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder @@ -62,7 +62,7 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group4 open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - private val fixedApp = FixedAppHelper(instrumentation) + private val testApp = SimpleAppHelper(instrumentation) private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.startRotation) private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.endRotation) @@ -74,7 +74,7 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS override val transition: FlickerBuilder.() -> Unit get() = buildTransition { setup { - fixedApp.launchViaIntent(wmHelper) + testApp.launchViaIntent(wmHelper) setRotation(testSpec.startRotation) } transitions { @@ -90,48 +90,48 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() /** - * Checks that [fixedApp] layer is within [screenBoundsStart] at the start of the transition + * Checks that [testApp] layer is within [screenBoundsStart] at the start of the transition */ @Presubmit @Test fun fixedAppLayer_StartingBounds() { testSpec.assertLayersStart { - visibleRegion(fixedApp).coversAtMost(screenBoundsStart) + visibleRegion(testApp).coversAtMost(screenBoundsStart) } } /** - * Checks that [fixedApp] layer is within [screenBoundsEnd] at the end of the transition + * Checks that [testApp] layer is within [screenBoundsEnd] at the end of the transition */ @Presubmit @Test fun fixedAppLayer_EndingBounds() { testSpec.assertLayersEnd { - visibleRegion(fixedApp).coversAtMost(screenBoundsEnd) + visibleRegion(testApp).coversAtMost(screenBoundsEnd) } } /** - * Checks that [fixedApp] plus [pipApp] layers are within [screenBoundsEnd] at the start + * Checks that [testApp] plus [pipApp] layers are within [screenBoundsEnd] at the start * of the transition */ @Presubmit @Test fun appLayers_StartingBounds() { testSpec.assertLayersStart { - visibleRegion(fixedApp.or(pipApp)).coversExactly(screenBoundsStart) + visibleRegion(testApp.or(pipApp)).coversExactly(screenBoundsStart) } } /** - * Checks that [fixedApp] plus [pipApp] layers are within [screenBoundsEnd] at the end + * Checks that [testApp] plus [pipApp] layers are within [screenBoundsEnd] at the end * of the transition */ @Presubmit @Test fun appLayers_EndingBounds() { testSpec.assertLayersEnd { - visibleRegion(fixedApp.or(pipApp)).coversExactly(screenBoundsEnd) + visibleRegion(testApp.or(pipApp)).coversExactly(screenBoundsEnd) } } @@ -165,26 +165,26 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS } /** - * Ensure that the [pipApp] window does not obscure the [fixedApp] at the start of the + * Ensure that the [pipApp] window does not obscure the [testApp] at the start of the * transition */ @Presubmit @Test fun pipIsAboveFixedAppWindow_Start() { testSpec.assertWmStart { - isAboveWindow(pipApp, fixedApp) + isAboveWindow(pipApp, testApp) } } /** - * Ensure that the [pipApp] window does not obscure the [fixedApp] at the end of the + * Ensure that the [pipApp] window does not obscure the [testApp] at the end of the * transition */ @Presubmit @Test fun pipIsAboveFixedAppWindow_End() { testSpec.assertWmEnd { - isAboveWindow(pipApp, fixedApp) + isAboveWindow(pipApp, testApp) } } 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 b67cf772358b..ff505a04290b 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,12 +21,12 @@ import android.content.Intent import android.view.Surface import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.PipAppHelper import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome +import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.wm.shell.flicker.BaseTest -import com.android.wm.shell.flicker.helpers.PipAppHelper -import com.android.wm.shell.flicker.testapp.Components abstract class PipTransition(testSpec: FlickerTestParameter) : BaseTest(testSpec) { protected val pipApp = PipAppHelper(instrumentation) @@ -61,11 +61,11 @@ abstract class PipTransition(testSpec: FlickerTestParameter) : BaseTest(testSpec * * @param eachRun If the pip app should be launched in each run (otherwise only 1x per test) * @param stringExtras Arguments to pass to the PIP launch intent - * @param extraSpec Addicional segment of flicker specification + * @param extraSpec Additional segment of flicker specification */ @JvmOverloads protected open fun buildTransition( - stringExtras: Map<String, String> = mapOf(Components.PipActivity.EXTRA_ENTER_PIP to "true"), + stringExtras: Map<String, String> = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true"), extraSpec: FlickerBuilder.() -> Unit = {} ): FlickerBuilder.() -> Unit { return { 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 866e4e83ec1e..30332f6c4aaf 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 @@ -31,9 +31,9 @@ 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.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome +import com.android.server.wm.flicker.testapp.ActivityOptions +import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE -import com.android.wm.shell.flicker.testapp.Components -import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder @@ -68,7 +68,7 @@ open class SetRequestedOrientationWhilePinnedTest( pipApp.launchViaIntent(wmHelper, stringExtras = mapOf( EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())) // Enter PiP. - broadcastActionTrigger.doAction(Components.PipActivity.ACTION_ENTER_PIP) + broadcastActionTrigger.doAction(ActivityOptions.Pip.ACTION_ENTER_PIP) // System bar may fade out during fixed rotation. wmHelper.StateSyncBuilder() .withPipShown() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt new file mode 100644 index 000000000000..cdd768abd5bd --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip.tv + +import android.app.Instrumentation +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.PipAppHelper +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper + +/** + * Helper class for PIP app on AndroidTV + */ +open class PipAppHelperTv(instrumentation: Instrumentation) : PipAppHelper(instrumentation) { + private val appSelector = By.pkg(`package`).depth(0) + + val ui: UiObject2? + get() = uiDevice.findObject(appSelector) + + private fun focusOnObject(selector: BySelector): Boolean { + // We expect all the focusable UI elements to be arranged in a way so that it is possible + // to "cycle" over all them by clicking the D-Pad DOWN button, going back up to "the top" + // from "the bottom". + repeat(FOCUS_ATTEMPTS) { + uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true } + ?: error("The object we try to focus on is gone.") + + uiDevice.pressDPadDown() + uiDevice.waitForIdle() + } + return false + } + + override fun clickObject(resId: String) { + val selector = By.res(`package`, resId) + focusOnObject(selector) || error("Could not focus on `$resId` object") + uiDevice.pressDPadCenter() + } + + @Deprecated( + "Use PipAppHelper.closePipWindow(wmHelper) instead", + ReplaceWith("closePipWindow(wmHelper)") + ) + override fun closePipWindow() { + uiDevice.closeTvPipWindow() + } + + /** + * Taps the pip window and dismisses it by clicking on the X button. + */ + override fun closePipWindow(wmHelper: WindowManagerStateHelper) { + uiDevice.closeTvPipWindow() + + // Wait for animation to complete. + wmHelper.StateSyncBuilder() + .withPipGone() + .withHomeActivityVisible() + .waitForAndVerify() + } + + fun waitUntilClosed(): Boolean { + val appSelector = By.pkg(`package`).depth(0) + return uiDevice.wait(Until.gone(appSelector), APP_CLOSE_WAIT_TIME_MS) + } + + companion object { + private const val FOCUS_ATTEMPTS = 20 + private const val APP_CLOSE_WAIT_TIME_MS = 3_000L + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt index 180ced0a6814..a16f5f6f1620 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt @@ -21,7 +21,6 @@ import android.content.pm.PackageManager import android.view.Surface import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice -import com.android.wm.shell.flicker.helpers.PipAppHelper import org.junit.Before import org.junit.runners.Parameterized @@ -38,7 +37,7 @@ abstract class PipTestBase( hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY) } } - protected val testApp = PipAppHelper(instrumentation) + protected val testApp = PipAppHelperTv(instrumentation) @Before open fun televisionSetUp() { 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 4be19d61278b..68dbbfb89b6c 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 @@ -19,8 +19,8 @@ package com.android.wm.shell.flicker.pip.tv import android.graphics.Rect import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.UiObject2 +import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME -import com.android.wm.shell.flicker.testapp.Components import com.android.wm.shell.flicker.wait import org.junit.Assert.assertNull import org.junit.Assert.assertTrue @@ -165,44 +165,44 @@ class TvPipMenuTests : TvPipTestBase() { enterPip_openMenu_assertShown() // PiP menu should contain "No-Op", "Off" and "Clear" buttons... - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_NO_OP) + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_NO_OP) ?: fail("\"No-Op\" button should be shown in Pip menu") - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF) + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF) ?: fail("\"Off\" button should be shown in Pip menu") - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) ?: fail("\"Clear\" button should be shown in Pip menu") // ... and should also contain the "Full screen" and "Close" buttons. assertFullscreenAndCloseButtonsAreShown() - uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF) + uiDevice.clickTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF) // Invoking the "Off" action should replace it with the "On" action/button and should // remove the "No-Op" action/button. "Clear" action/button should remain in the menu ... - uiDevice.waitForTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_ON) + uiDevice.waitForTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_ON) ?: fail("\"On\" button should be shown in Pip for a corresponding custom action") assertNull("\"No-Op\" button should not be shown in Pip menu", uiDevice.findTvPipMenuElementWithDescription( - Components.PipActivity.MENU_ACTION_NO_OP)) - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + ActivityOptions.Pip.MENU_ACTION_NO_OP)) + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) ?: fail("\"Clear\" button should be shown in Pip menu") // ... as well as the "Full screen" and "Close" buttons. assertFullscreenAndCloseButtonsAreShown() - uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + uiDevice.clickTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) // Invoking the "Clear" action should remove all the custom actions and their corresponding // buttons, ... uiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone( - Components.PipActivity.MENU_ACTION_ON)?.also { + ActivityOptions.Pip.MENU_ACTION_ON)?.also { isGone -> if (!isGone) fail("\"On\" button should not be shown in Pip menu") } assertNull("\"Off\" button should not be shown in Pip menu", uiDevice.findTvPipMenuElementWithDescription( - Components.PipActivity.MENU_ACTION_OFF)) + ActivityOptions.Pip.MENU_ACTION_OFF)) assertNull("\"Clear\" button should not be shown in Pip menu", uiDevice.findTvPipMenuElementWithDescription( - Components.PipActivity.MENU_ACTION_CLEAR)) + ActivityOptions.Pip.MENU_ACTION_CLEAR)) assertNull("\"No-Op\" button should not be shown in Pip menu", uiDevice.findTvPipMenuElementWithDescription( - Components.PipActivity.MENU_ACTION_NO_OP)) + ActivityOptions.Pip.MENU_ACTION_NO_OP)) // ... but the menu should still contain the "Full screen" and "Close" buttons. assertFullscreenAndCloseButtonsAreShown() @@ -217,11 +217,11 @@ class TvPipMenuTests : TvPipTestBase() { enterPip_openMenu_assertShown() // PiP menu should contain "No-Op", "Off" and "Clear" buttons for the custom actions... - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_NO_OP) + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_NO_OP) ?: fail("\"No-Op\" button should be shown in Pip menu") - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF) + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF) ?: fail("\"Off\" button should be shown in Pip menu") - uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) ?: fail("\"Clear\" button should be shown in Pip menu") // ... should also contain the "Full screen" and "Close" buttons, ... assertFullscreenAndCloseButtonsAreShown() @@ -231,7 +231,7 @@ class TvPipMenuTests : TvPipTestBase() { assertNull("\"Pause\" button should not be shown in menu when there are custom actions", uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription)) - uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + uiDevice.clickTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR) // Invoking the "Clear" action should remove all the custom actions, which should bring up // media buttons... uiDevice.waitForTvPipMenuElementWithDescription(pauseButtonDescription) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt index 3c439fdb9d98..7dbd27949bdd 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt @@ -28,7 +28,6 @@ import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowKeepVisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerKeepVisible import com.android.wm.shell.flicker.splitAppLayerBoundsKeepVisible import org.junit.FixMethodOrder @@ -48,16 +47,16 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { - private val textEditApp = SplitScreenHelper.getIme(instrumentation) + private val textEditApp = SplitScreenUtils.getIme(instrumentation) override val transition: FlickerBuilder.() -> Unit get() = { super.transition(this) setup { - SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, textEditApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, textEditApp) } transitions { - SplitScreenHelper.copyContentInSplit( + SplitScreenUtils.copyContentInSplit( instrumentation, device, primaryApp, textEditApp) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt index 60e5f78fa724..3646fd7f0177 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt @@ -30,7 +30,6 @@ import com.android.server.wm.flicker.helpers.WindowUtils import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowBecomesInvisible import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesInvisible import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible @@ -57,14 +56,14 @@ class DismissSplitScreenByDivider (testSpec: FlickerTestParameter) : SplitScreen get() = { super.transition(this) setup { - SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) } transitions { if (tapl.isTablet) { - SplitScreenHelper.dragDividerToDismissSplit(device, wmHelper, + SplitScreenUtils.dragDividerToDismissSplit(device, wmHelper, dragToRight = false, dragToBottom = true) } else { - SplitScreenHelper.dragDividerToDismissSplit(device, wmHelper, + SplitScreenUtils.dragDividerToDismissSplit(device, wmHelper, dragToRight = true, dragToBottom = true) } wmHelper.StateSyncBuilder() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt index 2db3009b8d68..80abedd476e6 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt @@ -28,7 +28,6 @@ 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.wm.shell.flicker.appWindowBecomesInvisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesInvisible import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible @@ -56,7 +55,7 @@ class DismissSplitScreenByGoHome( get() = { super.transition(this) setup { - SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) } transitions { tapl.goHome() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt index fddd84cfe4db..2915787d5a43 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt @@ -29,7 +29,6 @@ import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowKeepVisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerKeepVisible import com.android.wm.shell.flicker.splitAppLayerBoundsChanges import org.junit.FixMethodOrder @@ -54,10 +53,10 @@ class DragDividerToResize (testSpec: FlickerTestParameter) : SplitScreenBase(tes get() = { super.transition(this) setup { - SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) } transitions { - SplitScreenHelper.dragDividerToResizeAndWait(device, wmHelper) + SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper) } } 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 a7c689884e98..8e041a703802 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 @@ -30,7 +30,6 @@ import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowBecomesVisible import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag @@ -76,7 +75,7 @@ class EnterSplitScreenByDragFromAllApps( .openAllApps() .getAppIcon(secondaryApp.appName) .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) - SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt index 7d8a8db1a1ee..047bcfe6d200 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt @@ -31,7 +31,6 @@ import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag @@ -60,7 +59,7 @@ class EnterSplitScreenByDragFromNotification( testSpec: FlickerTestParameter ) : SplitScreenBase(testSpec) { - private val sendNotificationApp = SplitScreenHelper.getSendNotification(instrumentation) + private val sendNotificationApp = SplitScreenUtils.getSendNotification(instrumentation) @Before fun before() { @@ -76,7 +75,7 @@ class EnterSplitScreenByDragFromNotification( sendNotificationApp.launchViaIntent(wmHelper) val sendNotification = device.wait( Until.findObject(By.text("Send Notification")), - SplitScreenHelper.TIMEOUT_MS + SplitScreenUtils.TIMEOUT_MS ) sendNotification?.click() ?: error("Send notification button not found") @@ -84,8 +83,8 @@ class EnterSplitScreenByDragFromNotification( primaryApp.launchViaIntent(wmHelper) } transitions { - SplitScreenHelper.dragFromNotificationToSplit(instrumentation, device, wmHelper) - SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, sendNotificationApp) + SplitScreenUtils.dragFromNotificationToSplit(instrumentation, device, wmHelper) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, sendNotificationApp) } teardown { sendNotificationApp.exit(wmHelper) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt index bfd8a3a53437..a11874e00228 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt @@ -30,7 +30,6 @@ import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowBecomesVisible import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag @@ -70,7 +69,7 @@ class EnterSplitScreenByDragFromTaskbar( super.transition(this) setup { tapl.goHome() - SplitScreenHelper.createShortcutOnHotseatIfNotExist( + SplitScreenUtils.createShortcutOnHotseatIfNotExist( tapl, secondaryApp.appName ) primaryApp.launchViaIntent(wmHelper) @@ -79,7 +78,7 @@ class EnterSplitScreenByDragFromTaskbar( tapl.launchedAppState.taskbar .getAppIcon(secondaryApp.appName) .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) - SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt index cefb9f52d4fe..6064b52938c8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt @@ -26,7 +26,6 @@ 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.wm.shell.flicker.appWindowBecomesVisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible @@ -64,8 +63,8 @@ class EnterSplitScreenFromOverview(testSpec: FlickerTestParameter) : SplitScreen .waitForAndVerify() } transitions { - SplitScreenHelper.splitFromOverview(tapl) - SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + SplitScreenUtils.splitFromOverview(tapl) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt index eab473ca55a1..e6d6379e750c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt @@ -21,12 +21,11 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.setRotation import com.android.wm.shell.flicker.BaseTest -import com.android.wm.shell.flicker.helpers.SplitScreenHelper abstract class SplitScreenBase(testSpec: FlickerTestParameter) : BaseTest(testSpec) { protected val context: Context = instrumentation.context - protected val primaryApp = SplitScreenHelper.getPrimary(instrumentation) - protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) + protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation) + protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation) /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt new file mode 100644 index 000000000000..96b9faabd8fe --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt @@ -0,0 +1,341 @@ +/* + * 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.wm.shell.flicker.splitscreen + +import android.app.Instrumentation +import android.graphics.Point +import android.os.SystemClock +import android.view.InputDevice +import android.view.MotionEvent +import android.view.ViewConfiguration +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.NotificationAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.helpers.StandardAppHelper +import com.android.server.wm.flicker.testapp.ActivityOptions +import com.android.server.wm.traces.common.ComponentNameMatcher +import com.android.server.wm.traces.common.IComponentMatcher +import com.android.server.wm.traces.common.IComponentNameMatcher +import com.android.server.wm.traces.parser.toFlickerComponent +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME + +internal object SplitScreenUtils { + internal const val TIMEOUT_MS = 3_000L + private const val DRAG_DURATION_MS = 1_000L + private const val NOTIFICATION_SCROLLER = "notification_stack_scroller" + private const val DIVIDER_BAR = "docked_divider_handle" + private const val GESTURE_STEP_MS = 16L + private const val LONG_PRESS_TIME_MS = 100L + private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#") + + private val notificationScrollerSelector: BySelector + get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER) + private val notificationContentSelector: BySelector + get() = By.text("Notification content") + private val dividerBarSelector: BySelector + get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR) + + fun getPrimary(instrumentation: Instrumentation): StandardAppHelper = + SimpleAppHelper( + instrumentation, + ActivityOptions.SplitScreen.Primary.LABEL, + ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent() + ) + + fun getSecondary(instrumentation: Instrumentation): StandardAppHelper = + SimpleAppHelper( + instrumentation, + ActivityOptions.SplitScreen.Secondary.LABEL, + ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent() + ) + + fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper = + NonResizeableAppHelper(instrumentation) + + fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper = + NotificationAppHelper(instrumentation) + + fun getIme(instrumentation: Instrumentation): ImeAppHelper = + ImeAppHelper(instrumentation) + + fun waitForSplitComplete( + wmHelper: WindowManagerStateHelper, + primaryApp: IComponentMatcher, + secondaryApp: IComponentMatcher, + ) { + wmHelper.StateSyncBuilder() + .withWindowSurfaceAppeared(primaryApp) + .withWindowSurfaceAppeared(secondaryApp) + .withSplitDividerVisible() + .waitForAndVerify() + } + + fun enterSplit( + wmHelper: WindowManagerStateHelper, + tapl: LauncherInstrumentation, + primaryApp: StandardAppHelper, + secondaryApp: StandardAppHelper + ) { + tapl.workspace.switchToOverview().dismissAllTasks() + primaryApp.launchViaIntent(wmHelper) + secondaryApp.launchViaIntent(wmHelper) + tapl.goHome() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .waitForAndVerify() + splitFromOverview(tapl) + waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + + fun splitFromOverview(tapl: LauncherInstrumentation) { + // Note: The initial split position in landscape is different between tablet and phone. + // In landscape, tablet will let the first app split to right side, and phone will + // split to left side. + if (tapl.isTablet) { + tapl.workspace.switchToOverview().overviewActions + .clickSplit() + .currentTask + .open() + } else { + tapl.workspace.switchToOverview().currentTask + .tapMenu() + .tapSplitMenuItem() + .currentTask + .open() + } + SystemClock.sleep(TIMEOUT_MS) + } + + fun dragFromNotificationToSplit( + instrumentation: Instrumentation, + device: UiDevice, + wmHelper: WindowManagerStateHelper + ) { + val displayBounds = wmHelper.currentState.layerState + .displays.firstOrNull { !it.isVirtual } + ?.layerStackSpace + ?: error("Display not found") + + // Pull down the notifications + device.swipe( + displayBounds.centerX(), 5, + displayBounds.centerX(), displayBounds.bottom, 20 /* steps */ + ) + SystemClock.sleep(TIMEOUT_MS) + + // Find the target notification + val notificationScroller = device.wait( + Until.findObject(notificationScrollerSelector), TIMEOUT_MS + ) + var notificationContent = notificationScroller.findObject(notificationContentSelector) + + while (notificationContent == null) { + device.swipe( + displayBounds.centerX(), displayBounds.centerY(), + displayBounds.centerX(), displayBounds.centerY() - 150, 20 /* steps */ + ) + notificationContent = notificationScroller.findObject(notificationContentSelector) + } + + // Drag to split + val dragStart = notificationContent.visibleCenter + val dragMiddle = Point(dragStart.x + 50, dragStart.y) + val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4) + val downTime = SystemClock.uptimeMillis() + + touch( + instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, + TIMEOUT_MS, dragStart + ) + // It needs a horizontal movement to trigger the drag + touchMove( + instrumentation, downTime, SystemClock.uptimeMillis(), + DRAG_DURATION_MS, dragStart, dragMiddle + ) + touchMove( + instrumentation, downTime, SystemClock.uptimeMillis(), + DRAG_DURATION_MS, dragMiddle, dragEnd + ) + // Wait for a while to start splitting + SystemClock.sleep(TIMEOUT_MS) + touch( + instrumentation, MotionEvent.ACTION_UP, downTime, SystemClock.uptimeMillis(), + GESTURE_STEP_MS, dragEnd + ) + SystemClock.sleep(TIMEOUT_MS) + } + + fun touch( + instrumentation: Instrumentation, + action: Int, + downTime: Long, + eventTime: Long, + duration: Long, + point: Point + ) { + val motionEvent = MotionEvent.obtain( + downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0 + ) + motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN + instrumentation.uiAutomation.injectInputEvent(motionEvent, true) + motionEvent.recycle() + SystemClock.sleep(duration) + } + + fun touchMove( + instrumentation: Instrumentation, + downTime: Long, + eventTime: Long, + duration: Long, + from: Point, + to: Point + ) { + val steps: Long = duration / GESTURE_STEP_MS + var currentTime = eventTime + var currentX = from.x.toFloat() + var currentY = from.y.toFloat() + val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat() + val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat() + + for (i in 1..steps) { + val motionMove = MotionEvent.obtain( + downTime, currentTime, MotionEvent.ACTION_MOVE, currentX, currentY, 0 + ) + motionMove.source = InputDevice.SOURCE_TOUCHSCREEN + instrumentation.uiAutomation.injectInputEvent(motionMove, true) + motionMove.recycle() + + currentTime += GESTURE_STEP_MS + if (i == steps - 1) { + currentX = to.x.toFloat() + currentY = to.y.toFloat() + } else { + currentX += stepX + currentY += stepY + } + SystemClock.sleep(GESTURE_STEP_MS) + } + } + + fun longPress( + instrumentation: Instrumentation, + point: Point + ) { + val downTime = SystemClock.uptimeMillis() + touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, point) + SystemClock.sleep(LONG_PRESS_TIME_MS) + touch(instrumentation, MotionEvent.ACTION_UP, downTime, downTime, TIMEOUT_MS, point) + } + + fun createShortcutOnHotseatIfNotExist( + tapl: LauncherInstrumentation, + appName: String + ) { + tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0)) + val allApps = tapl.workspace.switchToAllApps() + allApps.freeze() + try { + allApps.getAppIcon(appName).dragToHotseat(0) + } finally { + allApps.unfreeze() + } + } + + fun dragDividerToResizeAndWait( + device: UiDevice, + wmHelper: WindowManagerStateHelper + ) { + val displayBounds = wmHelper.currentState.layerState + .displays.firstOrNull { !it.isVirtual } + ?.layerStackSpace + ?: error("Display not found") + val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) + dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3)) + + wmHelper.StateSyncBuilder() + .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER) + .waitForAndVerify() + } + + fun dragDividerToDismissSplit( + device: UiDevice, + wmHelper: WindowManagerStateHelper, + dragToRight: Boolean, + dragToBottom: Boolean + ) { + val displayBounds = wmHelper.currentState.layerState + .displays.firstOrNull { !it.isVirtual } + ?.layerStackSpace + ?: error("Display not found") + val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) + dividerBar.drag(Point( + if (dragToRight) { + displayBounds.width * 4 / 5 + } else { + displayBounds.width * 1 / 5 + }, + if (dragToBottom) { + displayBounds.height * 4 / 5 + } else { + displayBounds.height * 1 / 5 + })) + } + + fun doubleTapDividerToSwitch(device: UiDevice) { + val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) + val interval = (ViewConfiguration.getDoubleTapTimeout() + + ViewConfiguration.getDoubleTapMinTime()) / 2 + dividerBar.click() + SystemClock.sleep(interval.toLong()) + dividerBar.click() + } + + fun copyContentInSplit( + instrumentation: Instrumentation, + device: UiDevice, + sourceApp: IComponentNameMatcher, + destinationApp: IComponentNameMatcher, + ) { + // Copy text from sourceApp + val textView = device.wait(Until.findObject( + By.res(sourceApp.packageName, "SplitScreenTest")), TIMEOUT_MS) + longPress(instrumentation, textView.visibleCenter) + + val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS) + copyBtn.click() + + // Paste text to destinationApp + val editText = device.wait(Until.findObject( + By.res(destinationApp.packageName, "plain_text_input")), TIMEOUT_MS) + longPress(instrumentation, editText.visibleCenter) + + val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS) + pasteBtn.click() + + // Verify text + if (!textView.text.contentEquals(editText.text)) { + error("Fail to copy content in split") + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt index 2d5d2b71b270..6697b64f4043 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt @@ -29,7 +29,6 @@ import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.layerKeepVisible import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd @@ -55,10 +54,10 @@ class SwitchAppByDoubleTapDivider (testSpec: FlickerTestParameter) : SplitScreen get() = { super.transition(this) setup { - SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) } transitions { - SplitScreenHelper.doubleTapDividerToSwitch(device) + SplitScreenUtils.doubleTapDividerToSwitch(device) wmHelper.StateSyncBuilder() .withAppTransitionIdle() .waitForAndVerify() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt index 20c6af7c0530..5c3011643270 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt @@ -27,7 +27,6 @@ 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.wm.shell.flicker.appWindowBecomesVisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible @@ -48,13 +47,13 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 class SwitchBackToSplitFromAnotherApp(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { - val thirdApp = SplitScreenHelper.getNonResizeable(instrumentation) + val thirdApp = SplitScreenUtils.getNonResizeable(instrumentation) override val transition: FlickerBuilder.() -> Unit get() = { super.transition(this) setup { - SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) thirdApp.launchViaIntent(wmHelper) wmHelper.StateSyncBuilder() @@ -63,7 +62,7 @@ class SwitchBackToSplitFromAnotherApp(testSpec: FlickerTestParameter) : SplitScr } transitions { tapl.launchedAppState.quickSwitchToPreviousApp() - SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt index cb9ca9f13849..9c66a3734da3 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt @@ -27,7 +27,6 @@ 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.wm.shell.flicker.appWindowBecomesVisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible @@ -53,7 +52,7 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas get() = { super.transition(this) setup { - SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) tapl.goHome() wmHelper.StateSyncBuilder() @@ -62,7 +61,7 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas } transitions { tapl.workspace.quickSwitchToPreviousApp() - SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt index 266276749c13..e8862bd4c982 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt @@ -27,7 +27,6 @@ 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.wm.shell.flicker.appWindowBecomesVisible -import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesVisible import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible @@ -53,7 +52,7 @@ class SwitchBackToSplitFromRecent(testSpec: FlickerTestParameter) : SplitScreenB get() = { super.transition(this) setup { - SplitScreenHelper.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) + SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp) tapl.goHome() wmHelper.StateSyncBuilder() @@ -64,7 +63,7 @@ class SwitchBackToSplitFromRecent(testSpec: FlickerTestParameter) : SplitScreenB tapl.workspace.switchToOverview() .currentTask .open() - SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) } } diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp deleted file mode 100644 index ea606df1536d..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2020 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_test { - name: "WMShellFlickerTestApp", - srcs: ["**/*.java"], - sdk_version: "current", - test_suites: ["device-tests"], -} - -java_library { - name: "wmshell-flicker-test-components", - srcs: ["src/**/Components.java"], - sdk_version: "test_current", -} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml deleted file mode 100644 index bc0b0b6292b4..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml +++ /dev/null @@ -1,147 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2020 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.wm.shell.flicker.testapp"> - - <uses-sdk android:minSdkVersion="29" - android:targetSdkVersion="29"/> - <application android:allowBackup="false" - android:supportsRtl="true"> - <activity android:name=".FixedActivity" - android:resizeableActivity="true" - android:supportsPictureInPicture="true" - android:launchMode="singleTop" - android:theme="@style/CutoutShortEdges" - android:label="FixedApp" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - <activity android:name=".PipActivity" - android:resizeableActivity="true" - android:supportsPictureInPicture="true" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" - android:taskAffinity="com.android.wm.shell.flicker.testapp.PipActivity" - android:theme="@style/CutoutShortEdges" - android:launchMode="singleTop" - android:label="PipApp" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> - </intent-filter> - </activity> - - <activity android:name=".ImeActivity" - android:taskAffinity="com.android.wm.shell.flicker.testapp.ImeActivity" - android:theme="@style/CutoutShortEdges" - android:label="ImeApp" - android:launchMode="singleTop" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> - </intent-filter> - </activity> - - <activity android:name=".SplitScreenActivity" - android:resizeableActivity="true" - android:taskAffinity="com.android.wm.shell.flicker.testapp.SplitScreenActivity" - android:theme="@style/CutoutShortEdges" - android:label="SplitScreenPrimaryApp" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - - <activity android:name=".SplitScreenSecondaryActivity" - android:resizeableActivity="true" - android:taskAffinity="com.android.wm.shell.flicker.testapp.SplitScreenSecondaryActivity" - android:theme="@style/CutoutShortEdges" - android:label="SplitScreenSecondaryApp" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - - <activity android:name=".SendNotificationActivity" - android:taskAffinity="com.android.wm.shell.flicker.testapp.SendNotificationActivity" - android:theme="@style/CutoutShortEdges" - android:label="SendNotificationApp" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - - <activity android:name=".NonResizeableActivity" - android:resizeableActivity="false" - android:taskAffinity="com.android.wm.shell.flicker.testapp.NonResizeableActivity" - android:theme="@style/CutoutShortEdges" - android:label="NonResizeableApp" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - - <activity android:name=".SimpleActivity" - android:taskAffinity="com.android.wm.shell.flicker.testapp.SimpleActivity" - android:theme="@style/CutoutShortEdges" - android:label="SimpleApp" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - <activity - android:name=".LaunchBubbleActivity" - android:label="LaunchBubbleApp" - android:exported="true" - android:theme="@style/CutoutShortEdges" - android:launchMode="singleTop"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <action android:name="android.intent.action.VIEW" /> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - <activity - android:name=".BubbleActivity" - android:label="BubbleApp" - android:exported="false" - android:theme="@style/CutoutShortEdges" - android:resizeableActivity="true" /> - </application> -</manifest> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml deleted file mode 100644 index 4708cfd48381..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml +++ /dev/null @@ -1,27 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2018 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:layout_width="match_parent" - android:layout_height="match_parent" - android:focusableInTouchMode="true" - android:background="@android:color/holo_green_light"> - <EditText android:id="@+id/plain_text_input" - android:layout_height="wrap_content" - android:layout_width="match_parent" - android:inputType="text"/> -</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml deleted file mode 100644 index 45d5917f86d6..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2020 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:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:background="@android:color/holo_orange_light"> - - <TextView - android:id="@+id/NonResizeableTest" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:gravity="center_vertical|center_horizontal" - android:text="NonResizeableActivity" - android:textAppearance="?android:attr/textAppearanceLarge"/> - -</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml deleted file mode 100644 index 8d59b567e59b..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2021 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="match_parent" - android:orientation="vertical" - android:background="@android:color/black"> - - <Button - android:id="@+id/button_send_notification" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerHorizontal="true" - android:layout_centerVertical="true" - android:text="Send Notification" /> -</RelativeLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml deleted file mode 100644 index 5d94e5177dcc..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2018 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:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@android:color/holo_orange_light"> - -</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml deleted file mode 100644 index 674bb70ad01e..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2020 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:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:background="@android:color/holo_blue_light"> - - <TextView - android:id="@+id/SplitScreenTest" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:gravity="center_vertical|center_horizontal" - android:text="SecondaryActivity" - android:textAppearance="?android:attr/textAppearanceLarge"/> - -</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java deleted file mode 100644 index a2b580da5898..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.testapp; - -import android.content.ComponentName; - -public class Components { - public static final String PACKAGE_NAME = "com.android.wm.shell.flicker.testapp"; - - public static class SimpleActivity { - public static final String LABEL = "SimpleApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".SimpleActivity"); - } - - public static class FixedActivity { - public static final String EXTRA_FIXED_ORIENTATION = "fixed_orientation"; - public static final String LABEL = "FixedApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".FixedActivity"); - } - - public static class NonResizeableActivity { - public static final String LABEL = "NonResizeableApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".NonResizeableActivity"); - } - - public static class PipActivity { - // Test App > Pip Activity - public static final String LABEL = "PipApp"; - public static final String MENU_ACTION_NO_OP = "No-Op"; - public static final String MENU_ACTION_ON = "On"; - public static final String MENU_ACTION_OFF = "Off"; - public static final String MENU_ACTION_CLEAR = "Clear"; - - // Intent action that this activity dynamically registers to enter picture-in-picture - public static final String ACTION_ENTER_PIP = PACKAGE_NAME + ".PipActivity.ENTER_PIP"; - // Intent action that this activity dynamically registers to set requested orientation. - // Will apply the oriention to the value set in the EXTRA_FIXED_ORIENTATION extra. - public static final String ACTION_SET_REQUESTED_ORIENTATION = - PACKAGE_NAME + ".PipActivity.SET_REQUESTED_ORIENTATION"; - - // Calls enterPictureInPicture() on creation - public static final String EXTRA_ENTER_PIP = "enter_pip"; - // Sets the fixed orientation (can be one of {@link ActivityInfo.ScreenOrientation} - public static final String EXTRA_PIP_ORIENTATION = "fixed_orientation"; - // Adds a click listener to finish this activity when it is clicked - public static final String EXTRA_TAP_TO_FINISH = "tap_to_finish"; - - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".PipActivity"); - } - - public static class ImeActivity { - public static final String LABEL = "ImeApp"; - public static final String ACTION_CLOSE_IME = - PACKAGE_NAME + ".action.CLOSE_IME"; - public static final String ACTION_OPEN_IME = - PACKAGE_NAME + ".action.OPEN_IME"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".ImeActivity"); - } - - public static class SplitScreenActivity { - public static final String LABEL = "SplitScreenPrimaryApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".SplitScreenActivity"); - } - - public static class SplitScreenSecondaryActivity { - public static final String LABEL = "SplitScreenSecondaryApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".SplitScreenSecondaryActivity"); - } - - public static class SendNotificationActivity { - public static final String LABEL = "SendNotificationApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".SendNotificationActivity"); - } - - public static class LaunchBubbleActivity { - public static final String LABEL = "LaunchBubbleApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".LaunchBubbleActivity"); - } - - public static class BubbleActivity { - public static final String LABEL = "BubbleApp"; - public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, - PACKAGE_NAME + ".BubbleActivity"); - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java deleted file mode 100644 index 59c64a1345ab..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.testapp; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.view.View; -import android.view.WindowManager; -import android.view.inputmethod.InputMethodManager; - -public class ImeActivity extends Activity { - private static final String ACTION_OPEN_IME = - "com.android.wm.shell.flicker.testapp.action.OPEN_IME"; - private static final String ACTION_CLOSE_IME = - "com.android.wm.shell.flicker.testapp.action.CLOSE_IME"; - - private InputMethodManager mImm; - private View mEditText; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); - setContentView(R.layout.activity_ime); - - mEditText = findViewById(R.id.plain_text_input); - mImm = getSystemService(InputMethodManager.class); - - handleIntent(getIntent()); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - handleIntent(intent); - } - - private void handleIntent(Intent intent) { - final String action = intent.getAction(); - if (ACTION_OPEN_IME.equals(action)) { - mEditText.requestFocus(); - mImm.showSoftInput(mEditText, InputMethodManager.SHOW_FORCED); - } else if (ACTION_CLOSE_IME.equals(action)) { - mImm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0); - mEditText.clearFocus(); - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java deleted file mode 100644 index 5343c1893d4e..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.testapp; - -import android.app.Activity; -import android.os.Bundle; -import android.view.WindowManager; - -public class SimpleActivity extends Activity { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); - setContentView(R.layout.activity_simple); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java index a7234c1d3cb8..98b59126227c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -18,7 +18,9 @@ package com.android.wm.shell.activityembedding; import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; +import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; @@ -27,6 +29,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import android.animation.Animator; import android.window.TransitionInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -76,4 +79,18 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim verify(mController).onAnimationFinished(mTransition); } + + @Test + public void testChangesBehindStartingWindow() { + final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); + final TransitionInfo.Change embeddingChange = createChange(); + embeddingChange.setFlags(FLAG_IS_BEHIND_STARTING_WINDOW); + info.addChange(embeddingChange); + final Animator animator = mAnimRunner.createAnimator( + info, mStartTransaction, mFinishTransaction, + () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */)); + + // The animation should be empty when it is behind starting window. + assertEquals(0, animator.getDuration()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java index 577942505b13..c628f3994d8d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -32,13 +32,14 @@ import android.app.WindowConfiguration; import android.os.Handler; import android.os.IBinder; import android.testing.AndroidTestingRunner; +import android.window.DisplayAreaInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import android.window.WindowContainerTransaction.Change; import androidx.test.filters.SmallTest; -import com.android.wm.shell.RootDisplayAreaOrganizer; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.ShellExecutor; @@ -59,7 +60,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Mock private ShellTaskOrganizer mShellTaskOrganizer; @Mock - private RootDisplayAreaOrganizer mRootDisplayAreaOrganizer; + private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; @Mock private ShellExecutor mTestExecutor; @Mock @@ -75,7 +76,7 @@ public class DesktopModeControllerTest extends ShellTestCase { mShellInit = Mockito.spy(new ShellInit(mTestExecutor)); mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer, - mRootDisplayAreaOrganizer, mMockHandler, mMockTransitions); + mRootTaskDisplayAreaOrganizer, mMockHandler, mMockTransitions); mShellInit.init(); } @@ -94,19 +95,19 @@ public class DesktopModeControllerTest extends ShellTestCase { when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks( mContext.getDisplayId())).thenReturn(taskWct); - // Create a fake WCT to simulate setting display windowing mode to freeform - WindowContainerTransaction displayWct = new WindowContainerTransaction(); + // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly MockToken displayMockToken = new MockToken(); - displayWct.setWindowingMode(displayMockToken.token(), WINDOWING_MODE_FREEFORM); - when(mRootDisplayAreaOrganizer.prepareWindowingModeChange(mContext.getDisplayId(), - WINDOWING_MODE_FREEFORM)).thenReturn(displayWct); + DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken, + mContext.getDisplayId(), 0); + when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) + .thenReturn(displayAreaInfo); // The test mController.updateDesktopModeActive(true); ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( WindowContainerTransaction.class); - verify(mRootDisplayAreaOrganizer).applyTransaction(arg.capture()); + verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); // WCT should have 2 changes - clear task wm mode and set display wm mode WindowContainerTransaction wct = arg.getValue(); @@ -118,7 +119,7 @@ public class DesktopModeControllerTest extends ShellTestCase { assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); // Verify executed WCT has a change for setting display windowing mode to freeform - Change displayWmModeChange = wct.getChanges().get(displayMockToken.binder()); + Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder()); assertThat(displayWmModeChange).isNotNull(); assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM); } @@ -139,19 +140,19 @@ public class DesktopModeControllerTest extends ShellTestCase { when(mShellTaskOrganizer.prepareClearBoundsForStandardTasks( mContext.getDisplayId())).thenReturn(taskBoundsWct); - // Create a fake WCT to simulate setting display windowing mode to fullscreen - WindowContainerTransaction displayWct = new WindowContainerTransaction(); + // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly MockToken displayMockToken = new MockToken(); - displayWct.setWindowingMode(displayMockToken.token(), WINDOWING_MODE_FULLSCREEN); - when(mRootDisplayAreaOrganizer.prepareWindowingModeChange(mContext.getDisplayId(), - WINDOWING_MODE_FULLSCREEN)).thenReturn(displayWct); + DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken, + mContext.getDisplayId(), 0); + when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) + .thenReturn(displayAreaInfo); // The test mController.updateDesktopModeActive(false); ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( WindowContainerTransaction.class); - verify(mRootDisplayAreaOrganizer).applyTransaction(arg.capture()); + verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode WindowContainerTransaction wct = arg.getValue(); @@ -171,7 +172,7 @@ public class DesktopModeControllerTest extends ShellTestCase { .isTrue(); // Verify executed WCT has a change for setting display windowing mode to fullscreen - Change displayWmModeChange = wct.getChanges().get(displayMockToken.binder()); + Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder()); assertThat(displayWmModeChange).isNotNull(); assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index 9240abfbe47f..835087007b30 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -16,7 +16,10 @@ package com.android.wm.shell.splitscreen; +import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; +import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -28,11 +31,10 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -42,8 +44,10 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Bundle; import android.view.SurfaceControl; import android.view.SurfaceSession; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -115,7 +119,6 @@ public class StageCoordinatorTests extends ShellTestCase { mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, mMainExecutor, Optional.empty())); - doNothing().when(mStageCoordinator).updateActivityOptions(any(), anyInt()); when(mSplitLayout.getBounds1()).thenReturn(mBounds1); when(mSplitLayout.getBounds2()).thenReturn(mBounds2); @@ -303,4 +306,16 @@ public class StageCoordinatorTests extends ShellTestCase { verify(mSplitLayout).applySurfaceChanges(any(), any(), any(), any(), any(), eq(false)); } + + @Test + public void testAddActivityOptions_addsBackgroundActivitiesFlags() { + Bundle options = mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN, + SPLIT_POSITION_UNDEFINED, null /* options */, null /* wct */); + + assertEquals(options.getParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, WindowContainerToken.class), + mMainStage.mRootTaskInfo.token); + assertTrue(options.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)); + assertTrue(options.getBoolean( + KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION)); + } } diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index 1f96617a1ede..cb0f22f974ad 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -238,6 +238,7 @@ LIBANDROID { ASurfaceControl_createFromWindow; # introduced=29 ASurfaceControl_acquire; # introduced=31 ASurfaceControl_release; # introduced=29 + ASurfaceControl_fromSurfaceControl; # introduced=34 ASurfaceTexture_acquireANativeWindow; # introduced=28 ASurfaceTexture_attachToGLContext; # introduced=28 ASurfaceTexture_detachFromGLContext; # introduced=28 @@ -255,6 +256,7 @@ LIBANDROID { ASurfaceTransaction_apply; # introduced=29 ASurfaceTransaction_create; # introduced=29 ASurfaceTransaction_delete; # introduced=29 + ASurfaceTransaction_fromTransaction; # introduced=34 ASurfaceTransaction_reparent; # introduced=29 ASurfaceTransaction_setBuffer; # introduced=29 ASurfaceTransaction_setBufferAlpha; # introduced=29 diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp index 42f4406ce5e8..9e4d72671502 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -17,6 +17,8 @@ #include <android/hardware/configstore/1.0/ISurfaceFlingerConfigs.h> #include <android/native_window.h> #include <android/surface_control.h> +#include <android/surface_control_jni.h> +#include <android_runtime/android_view_SurfaceControl.h> #include <configstore/Utils.h> #include <gui/HdrMetadata.h> #include <gui/ISurfaceComposer.h> @@ -28,6 +30,8 @@ #include <ui/DynamicDisplayInfo.h> #include <utils/Timers.h> +#include <utility> + using namespace android::hardware::configstore; using namespace android::hardware::configstore::V1_0; using namespace android; @@ -134,6 +138,11 @@ void ASurfaceControl_release(ASurfaceControl* aSurfaceControl) { SurfaceControl_release(surfaceControl); } +ASurfaceControl* ASurfaceControl_fromSurfaceControl(JNIEnv* env, jobject surfaceControlObj) { + return reinterpret_cast<ASurfaceControl*>( + android_view_SurfaceControl_getNativeSurfaceControl(env, surfaceControlObj)); +} + struct ASurfaceControlStats { std::variant<int64_t, sp<Fence>> acquireTimeOrFence; sp<Fence> previousReleaseFence; @@ -190,6 +199,11 @@ void ASurfaceTransaction_delete(ASurfaceTransaction* aSurfaceTransaction) { delete transaction; } +ASurfaceTransaction* ASurfaceTransaction_fromTransaction(JNIEnv* env, jobject transactionObj) { + return reinterpret_cast<ASurfaceTransaction*>( + android_view_SurfaceTransaction_getNativeSurfaceTransaction(env, transactionObj)); +} + void ASurfaceTransaction_apply(ASurfaceTransaction* aSurfaceTransaction) { CHECK_NOT_NULL(aSurfaceTransaction); diff --git a/packages/CredentialManager/Android.bp b/packages/CredentialManager/Android.bp new file mode 100644 index 000000000000..ed92af997a68 --- /dev/null +++ b/packages/CredentialManager/Android.bp @@ -0,0 +1,36 @@ +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: "CredentialManager", + defaults: ["platform_app_defaults"], + certificate: "platform", + srcs: ["src/**/*.kt"], + resource_dirs: ["res"], + + static_libs: [ + "androidx.activity_activity-compose", + "androidx.appcompat_appcompat", + "androidx.compose.material_material", + "androidx.compose.runtime_runtime", + "androidx.compose.ui_ui", + "androidx.compose.ui_ui-tooling", + "androidx.core_core-ktx", + "androidx.lifecycle_lifecycle-extensions", + "androidx.lifecycle_lifecycle-livedata", + "androidx.lifecycle_lifecycle-runtime-ktx", + "androidx.lifecycle_lifecycle-viewmodel-compose", + "androidx.navigation_navigation-compose", + "androidx.recyclerview_recyclerview", + ], + + platform_apis: true, + + kotlincflags: ["-Xjvm-default=enable"], +} diff --git a/packages/CredentialManager/AndroidManifest.xml b/packages/CredentialManager/AndroidManifest.xml new file mode 100644 index 000000000000..586ef86f26f6 --- /dev/null +++ b/packages/CredentialManager/AndroidManifest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (c) 2017 Google Inc. + * + * 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.credentialmanager"> + + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> + <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"/> + + <application + android:allowBackup="true" + android:dataExtractionRules="@xml/data_extraction_rules" + android:fullBackupContent="@xml/backup_rules" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/Theme.CredentialSelector"> + + <activity + android:name=".CredentialSelectorActivity" + android:exported="true" + android:label="@string/app_name" + android:launchMode="singleInstance" + android:noHistory="true" + android:excludeFromRecents="true" + android:theme="@style/Theme.CredentialSelector"> + </activity> + </application> + +</manifest> diff --git a/packages/CredentialManager/res/drawable-v24/ic_launcher_foreground.xml b/packages/CredentialManager/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000000..966abaff2074 --- /dev/null +++ b/packages/CredentialManager/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt" + android:width="108dp" + android:height="108dp" + android:viewportHeight="108" + android:viewportWidth="108"> + <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> + <aapt:attr name="android:fillColor"> + <gradient + android:endX="85.84757" + android:endY="92.4963" + android:startX="42.9492" + android:startY="49.59793" + android:type="linear"> + <item + android:color="#44000000" + android:offset="0.0" /> + <item + android:color="#00000000" + android:offset="1.0" /> + </gradient> + </aapt:attr> + </path> + <path + android:fillColor="#FFFFFF" + android:fillType="nonZero" + android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" + android:strokeColor="#00000000" + android:strokeWidth="1" /> +</vector>
\ No newline at end of file diff --git a/packages/CredentialManager/res/drawable/ic_launcher_background.xml b/packages/CredentialManager/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000000..61bb79edb709 --- /dev/null +++ b/packages/CredentialManager/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportHeight="108" + android:viewportWidth="108"> + <path + android:fillColor="#3DDC84" + android:pathData="M0,0h108v108h-108z" /> + <path + android:fillColor="#00000000" + android:pathData="M9,0L9,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,0L19,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M29,0L29,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M39,0L39,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M49,0L49,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M59,0L59,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M69,0L69,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M79,0L79,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M89,0L89,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M99,0L99,108" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,9L108,9" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,19L108,19" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,29L108,29" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,39L108,39" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,49L108,49" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,59L108,59" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,69L108,69" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,79L108,79" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,89L108,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M0,99L108,99" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,29L89,29" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,39L89,39" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,49L89,49" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,59L89,59" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,69L89,69" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M19,79L89,79" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M29,19L29,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M39,19L39,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M49,19L49,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M59,19L59,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M69,19L69,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + <path + android:fillColor="#00000000" + android:pathData="M79,19L79,89" + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> +</vector> diff --git a/packages/CredentialManager/res/drawable/ic_passkey.xml b/packages/CredentialManager/res/drawable/ic_passkey.xml new file mode 100644 index 000000000000..041a32164073 --- /dev/null +++ b/packages/CredentialManager/res/drawable/ic_passkey.xml @@ -0,0 +1,16 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="28dp" + android:height="24dp" + android:viewportWidth="28" + android:viewportHeight="24"> + <path + android:pathData="M27.453,13.253C27.453,14.952 26.424,16.411 24.955,17.041L26.21,18.295L24.839,19.666L26.21,21.037L23.305,23.942L22.012,22.65L22.012,17.156C20.385,16.605 19.213,15.066 19.213,13.253C19.213,10.977 21.058,9.133 23.333,9.133C25.609,9.133 27.453,10.977 27.453,13.253ZM25.47,13.254C25.47,14.434 24.514,15.39 23.334,15.39C22.154,15.39 21.197,14.434 21.197,13.254C21.197,12.074 22.154,11.118 23.334,11.118C24.514,11.118 25.47,12.074 25.47,13.254Z" + android:fillColor="#00639B" + android:fillType="evenOdd"/> + <path + android:pathData="M17.85,5.768C17.85,8.953 15.268,11.536 12.083,11.536C8.897,11.536 6.315,8.953 6.315,5.768C6.315,2.582 8.897,0 12.083,0C15.268,0 17.85,2.582 17.85,5.768Z" + android:fillColor="#00639B"/> + <path + android:pathData="M0.547,20.15C0.547,16.32 8.23,14.382 12.083,14.382C13.59,14.382 15.684,14.679 17.674,15.269C18.116,16.454 18.952,17.447 20.022,18.089V23.071H0.547V20.15Z" + android:fillColor="#00639B"/> +</vector> diff --git a/packages/CredentialManager/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/CredentialManager/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000000..03eed2533da2 --- /dev/null +++ b/packages/CredentialManager/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon>
\ No newline at end of file diff --git a/packages/CredentialManager/res/mipmap-anydpi-v26/ic_launcher_round.xml b/packages/CredentialManager/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000000..03eed2533da2 --- /dev/null +++ b/packages/CredentialManager/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon>
\ No newline at end of file diff --git a/packages/CredentialManager/res/mipmap-hdpi/ic_launcher.webp b/packages/CredentialManager/res/mipmap-hdpi/ic_launcher.webp Binary files differnew file mode 100644 index 000000000000..c209e78ecd37 --- /dev/null +++ b/packages/CredentialManager/res/mipmap-hdpi/ic_launcher.webp diff --git a/packages/CredentialManager/res/mipmap-hdpi/ic_launcher_round.webp b/packages/CredentialManager/res/mipmap-hdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 000000000000..b2dfe3d1ba5c --- /dev/null +++ b/packages/CredentialManager/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/packages/CredentialManager/res/mipmap-mdpi/ic_launcher.webp b/packages/CredentialManager/res/mipmap-mdpi/ic_launcher.webp Binary files differnew file mode 100644 index 000000000000..4f0f1d64e58b --- /dev/null +++ b/packages/CredentialManager/res/mipmap-mdpi/ic_launcher.webp diff --git a/packages/CredentialManager/res/mipmap-mdpi/ic_launcher_round.webp b/packages/CredentialManager/res/mipmap-mdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 000000000000..62b611da0816 --- /dev/null +++ b/packages/CredentialManager/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/packages/CredentialManager/res/mipmap-xhdpi/ic_launcher.webp b/packages/CredentialManager/res/mipmap-xhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 000000000000..948a3070fe34 --- /dev/null +++ b/packages/CredentialManager/res/mipmap-xhdpi/ic_launcher.webp diff --git a/packages/CredentialManager/res/mipmap-xhdpi/ic_launcher_round.webp b/packages/CredentialManager/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 000000000000..1b9a6956b3ac --- /dev/null +++ b/packages/CredentialManager/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/packages/CredentialManager/res/mipmap-xxhdpi/ic_launcher.webp b/packages/CredentialManager/res/mipmap-xxhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 000000000000..28d4b77f9f03 --- /dev/null +++ b/packages/CredentialManager/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/packages/CredentialManager/res/mipmap-xxhdpi/ic_launcher_round.webp b/packages/CredentialManager/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 000000000000..9287f5083623 --- /dev/null +++ b/packages/CredentialManager/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/packages/CredentialManager/res/mipmap-xxxhdpi/ic_launcher.webp b/packages/CredentialManager/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 000000000000..aa7d6427e6fa --- /dev/null +++ b/packages/CredentialManager/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/packages/CredentialManager/res/mipmap-xxxhdpi/ic_launcher_round.webp b/packages/CredentialManager/res/mipmap-xxxhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 000000000000..9126ae37cbc3 --- /dev/null +++ b/packages/CredentialManager/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/packages/CredentialManager/res/values/colors.xml b/packages/CredentialManager/res/values/colors.xml new file mode 100644 index 000000000000..09837df62f44 --- /dev/null +++ b/packages/CredentialManager/res/values/colors.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="purple_200">#FFBB86FC</color> + <color name="purple_500">#FF6200EE</color> + <color name="purple_700">#FF3700B3</color> + <color name="teal_200">#FF03DAC5</color> + <color name="teal_700">#FF018786</color> + <color name="black">#FF000000</color> + <color name="white">#FFFFFFFF</color> +</resources>
\ No newline at end of file diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml new file mode 100644 index 000000000000..2901705d5836 --- /dev/null +++ b/packages/CredentialManager/res/values/strings.xml @@ -0,0 +1,13 @@ +<resources> + <string name="app_name">CredentialManager</string> + <string name="string_cancel">Cancel</string> + <string name="string_continue">Continue</string> + <string name="string_more_options">More options</string> + <string name="string_no_thanks">No thanks</string> + <string name="passkey_creation_intro_title">A simple way to sign in safely</string> + <string name="passkey_creation_intro_body">Use your fingerprint, face or screen lock to sign in with a unique passkey that can’t be forgotten or stolen. Learn more</string> + <string name="choose_provider_title">Choose your default provider</string> + <string name="choose_provider_body">This provider will store passkeys and passwords for you and help you easily autofill and sign in. Learn more</string> + <string name="choose_create_option_title">Create a passkey at</string> + <string name="choose_sign_in_title">Use saved sign in</string> +</resources>
\ No newline at end of file diff --git a/packages/CredentialManager/res/values/themes.xml b/packages/CredentialManager/res/values/themes.xml new file mode 100644 index 000000000000..feec74608000 --- /dev/null +++ b/packages/CredentialManager/res/values/themes.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="Theme.CredentialSelector" parent="@android:style/ThemeOverlay.Material"> + <item name="android:statusBarColor">@color/purple_700</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:windowNoTitle">true</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowIsTranslucent">true</item> + <item name="android:colorBackgroundCacheHint">@null</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/CredentialManager/res/xml/backup_rules.xml b/packages/CredentialManager/res/xml/backup_rules.xml new file mode 100644 index 000000000000..9b42d90d94bb --- /dev/null +++ b/packages/CredentialManager/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Sample backup rules file; uncomment and customize as necessary. + See https://developer.android.com/guide/topics/data/autobackup + for details. + Note: This file is ignored for devices older that API 31 + See https://developer.android.com/about/versions/12/backup-restore +--> +<full-backup-content> + <!-- + <include domain="sharedpref" path="."/> + <exclude domain="sharedpref" path="device.xml"/> +--> +</full-backup-content>
\ No newline at end of file diff --git a/packages/CredentialManager/res/xml/data_extraction_rules.xml b/packages/CredentialManager/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000000..c6c3bb05a956 --- /dev/null +++ b/packages/CredentialManager/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Sample data extraction rules file; uncomment and customize as necessary. + See https://developer.android.com/about/versions/12/backup-restore#xml-changes + for details. +--> +<data-extraction-rules> + <cloud-backup> + <!-- TODO: Use <include> and <exclude> to control what is backed up. + <include .../> + <exclude .../> + --> + </cloud-backup> + <!-- + <device-transfer> + <include .../> + <exclude .../> + </device-transfer> + --> +</data-extraction-rules>
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt new file mode 100644 index 000000000000..f20104a19af2 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -0,0 +1,131 @@ +package com.android.credentialmanager + +import android.content.Context +import com.android.credentialmanager.createflow.CreateOptionInfo +import com.android.credentialmanager.createflow.ProviderInfo +import com.android.credentialmanager.createflow.ProviderList +import com.android.credentialmanager.getflow.CredentialOptionInfo + +class CredentialManagerRepo( + private val context: Context +) { + fun getCredentialProviderList(): List<com.android.credentialmanager.getflow.ProviderInfo> { + return listOf( + com.android.credentialmanager.getflow.ProviderInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + name = "Google Password Manager", + appDomainName = "tribank.us", + credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, + credentialOptions = listOf( + CredentialOptionInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + title = "Elisa Backett", + subtitle = "elisa.beckett@gmail.com", + id = "id-1", + ), + CredentialOptionInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + title = "Elisa Backett Work", + subtitle = "elisa.beckett.work@google.com", + id = "id-2", + ), + ) + ), + com.android.credentialmanager.getflow.ProviderInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + name = "Lastpass", + appDomainName = "tribank.us", + credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, + credentialOptions = listOf( + CredentialOptionInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + title = "Elisa Backett", + subtitle = "elisa.beckett@lastpass.com", + id = "id-1", + ), + ) + ), + com.android.credentialmanager.getflow.ProviderInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + name = "Dashlane", + appDomainName = "tribank.us", + credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, + credentialOptions = listOf( + CredentialOptionInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + title = "Elisa Backett", + subtitle = "elisa.beckett@dashlane.com", + id = "id-1", + ), + ) + ), + ) + } + + fun createCredentialProviderList(): ProviderList { + return ProviderList( + listOf( + ProviderInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + name = "Google Password Manager", + appDomainName = "tribank.us", + credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, + createOptions = listOf( + CreateOptionInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + title = "Elisa Backett", + subtitle = "elisa.beckett@gmail.com", + id = "id-1", + ), + CreateOptionInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + title = "Elisa Backett Work", + subtitle = "elisa.beckett.work@google.com", + id = "id-2", + ), + ) + ), + ProviderInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + name = "Lastpass", + appDomainName = "tribank.us", + credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, + createOptions = listOf( + CreateOptionInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + title = "Elisa Backett", + subtitle = "elisa.beckett@lastpass.com", + id = "id-1", + ), + ) + ), + ProviderInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + name = "Dashlane", + appDomainName = "tribank.us", + credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, + createOptions = listOf( + CreateOptionInfo( + icon = context.getDrawable(R.drawable.ic_passkey)!!, + title = "Elisa Backett", + subtitle = "elisa.beckett@dashlane.com", + id = "id-1", + ), + ) + ), + ) + ) + } + + companion object { + lateinit var repo: CredentialManagerRepo + + fun setup(context: Context) { + repo = CredentialManagerRepo(context) + } + + fun getInstance(): CredentialManagerRepo { + return repo + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt new file mode 100644 index 000000000000..dd4ba11e9202 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -0,0 +1,63 @@ +package com.android.credentialmanager + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +import com.android.credentialmanager.createflow.CreatePasskeyViewModel +import com.android.credentialmanager.createflow.createPasskeyGraph +import com.android.credentialmanager.getflow.GetCredentialViewModel +import com.android.credentialmanager.getflow.getCredentialsGraph +import com.android.credentialmanager.ui.theme.CredentialSelectorTheme + +@ExperimentalMaterialApi +class CredentialSelectorActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + CredentialManagerRepo.setup(this) + val startDestination = intent.extras?.getString( + "start_destination", + "getCredentials" + ) ?: "getCredentials" + + setContent { + CredentialSelectorTheme { + AppNavHost( + startDestination = startDestination, + onCancel = {this.finish()} + ) + } + } + } + + @ExperimentalMaterialApi + @Composable + fun AppNavHost( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController(), + startDestination: String, + onCancel: () -> Unit, + ) { + NavHost( + modifier = modifier, + navController = navController, + startDestination = startDestination + ) { + createPasskeyGraph( + navController = navController, + viewModel = CreatePasskeyViewModel(CredentialManagerRepo.repo), + onCancel = onCancel + ) + getCredentialsGraph( + navController = navController, + viewModel = GetCredentialViewModel(CredentialManagerRepo.repo), + onCancel = onCancel + ) + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt new file mode 100644 index 000000000000..62c244cfb121 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -0,0 +1,22 @@ +package com.android.credentialmanager.createflow + +import android.graphics.drawable.Drawable + +data class ProviderInfo( + val icon: Drawable, + val name: String, + val appDomainName: String, + val credentialTypeIcon: Drawable, + val createOptions: List<CreateOptionInfo>, +) + +data class ProviderList( + val providers: List<ProviderInfo>, +) + +data class CreateOptionInfo( + val icon: Drawable, + val title: String, + val subtitle: String, + val id: String, +) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt new file mode 100644 index 000000000000..fb6db2172123 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt @@ -0,0 +1,541 @@ +package com.android.credentialmanager.createflow + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Button +import androidx.compose.material.ButtonColors +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Card +import androidx.compose.material.Chip +import androidx.compose.material.ChipDefaults +import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.Text +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import androidx.navigation.navigation +import com.android.credentialmanager.R +import com.android.credentialmanager.ui.theme.Grey100 +import com.android.credentialmanager.ui.theme.Shapes +import com.android.credentialmanager.ui.theme.Typography +import com.android.credentialmanager.ui.theme.lightBackgroundColor +import com.android.credentialmanager.ui.theme.lightColorAccentSecondary +import com.android.credentialmanager.ui.theme.lightSurface1 + +@ExperimentalMaterialApi +fun NavGraphBuilder.createPasskeyGraph( + navController: NavController, + viewModel: CreatePasskeyViewModel, + onCancel: () -> Unit, + startDestination: String = "intro", // TODO: get this from view model +) { + navigation(startDestination = startDestination, route = "createPasskey") { + composable("intro") { + CreatePasskeyIntroDialog( + onCancel = onCancel, + onConfirm = {viewModel.onConfirm(navController)} + ) + } + composable("providerSelection") { + ProviderSelectionDialog( + providerList = viewModel.uiState.collectAsState().value.providerList, + onProviderSelected = {viewModel.onProviderSelected(it, navController)}, + onCancel = onCancel + ) + } + composable( + "createCredentialSelection/{providerName}", + arguments = listOf(navArgument("providerName") {type = NavType.StringType}) + ) { + val arguments = it.arguments + if (arguments == null) { + throw java.lang.IllegalStateException("createCredentialSelection without a provider name") + } + CreationSelectionDialog( + providerInfo = viewModel.getProviderInfoByName(arguments.getString("providerName")!!), + onOptionSelected = {viewModel.onCreateOptionSelected(it)}, + onCancel = onCancel, + multiProvider = viewModel.uiState.collectAsState().value.providerList.providers.size > 1, + onMoreOptionSelected = {viewModel.onMoreOptionSelected(navController)}, + ) + } + } +} + +/** + * BEGIN INTRO CONTENT + */ +@ExperimentalMaterialApi +@Composable +fun CreatePasskeyIntroDialog( + onCancel: () -> Unit = {}, + onConfirm: () -> Unit = {}, +) { + val state = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Expanded, + skipHalfExpanded = true + ) + ModalBottomSheetLayout( + sheetState = state, + sheetContent = { + ConfirmationCard( + onCancel = onCancel, onConfirm = onConfirm + ) + }, + scrimColor = Color.Transparent, + sheetShape = Shapes.medium, + ) {} + LaunchedEffect(state.currentValue) { + when (state.currentValue) { + ModalBottomSheetValue.Hidden -> { + onCancel() + } + } + } +} + +@Composable +fun ConfirmationCard( + onConfirm: () -> Unit, + onCancel: () -> Unit, +) { + Card( + backgroundColor = lightBackgroundColor, + ) { + Column() { + Icon( + painter = painterResource(R.drawable.ic_passkey), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp) + ) + Text( + text = stringResource(R.string.passkey_creation_intro_title), + style = Typography.subtitle1, + modifier = Modifier + .padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally) + ) + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Text( + text = stringResource(R.string.passkey_creation_intro_body), + style = Typography.body1, + modifier = Modifier.padding(horizontal = 28.dp) + ) + Divider( + thickness = 48.dp, + color = Color.Transparent + ) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) + ) { + CancelButton( + stringResource(R.string.string_cancel), + onclick = onCancel + ) + ConfirmButton( + stringResource(R.string.string_continue), + onclick = onConfirm + ) + } + Divider( + thickness = 18.dp, + color = Color.Transparent, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +/** + * END INTRO CONTENT + */ + +/** + * BEGIN PROVIDER SELECTION CONTENT + */ +@ExperimentalMaterialApi +@Composable +fun ProviderSelectionDialog( + providerList: ProviderList, + onProviderSelected: (String) -> Unit, + onCancel: () -> Unit, +) { + val state = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Expanded, + skipHalfExpanded = true + ) + ModalBottomSheetLayout( + sheetState = state, + sheetContent = { + ProviderSelectionCard( + providerList = providerList, + onCancel = onCancel, + onProviderSelected = onProviderSelected + ) + }, + scrimColor = Color.Transparent, + sheetShape = Shapes.medium, + ) {} + LaunchedEffect(state.currentValue) { + when (state.currentValue) { + ModalBottomSheetValue.Hidden -> { + onCancel() + } + } + } +} + +@ExperimentalMaterialApi +@Composable +fun ProviderSelectionCard( + providerList: ProviderList, + onProviderSelected: (String) -> Unit, + onCancel: () -> Unit +) { + Card( + backgroundColor = lightBackgroundColor, + ) { + Column() { + Text( + text = stringResource(R.string.choose_provider_title), + style = Typography.subtitle1, + modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) + ) + Text( + text = stringResource(R.string.choose_provider_body), + style = Typography.body1, + modifier = Modifier.padding(horizontal = 28.dp) + ) + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Card( + shape = Shapes.medium, + modifier = Modifier + .padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally) + ) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + providerList.providers.forEach { + item { + ProviderRow(providerInfo = it, onProviderSelected = onProviderSelected) + } + } + } + } + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Row( + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) + ) { + CancelButton(stringResource(R.string.string_cancel), onCancel) + } + Divider( + thickness = 18.dp, + color = Color.Transparent, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +@ExperimentalMaterialApi +@Composable +fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit) { + Chip( + modifier = Modifier.fillMaxWidth(), + onClick = {onProviderSelected(providerInfo.name)}, + leadingIcon = { + Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + bitmap = providerInfo.icon.toBitmap().asImageBitmap(), + // painter = painterResource(R.drawable.ic_passkey), + // TODO: add description. + contentDescription = "") + }, + colors = ChipDefaults.chipColors( + backgroundColor = Grey100, + leadingIconContentColor = Grey100 + ), + shape = Shapes.large + ) { + Text( + text = providerInfo.name, + style = Typography.button, + modifier = Modifier.padding(vertical = 18.dp) + ) + } +} + +/** + * END PROVIDER SELECTION CONTENT + */ + +/** + * BEGIN COMMON COMPONENTS + */ + +@Composable +fun CancelButton(text: String, onclick: () -> Unit) { + val colors = ButtonDefaults.buttonColors( + backgroundColor = lightBackgroundColor + ) + NavigationButton( + border = BorderStroke(1.dp, lightSurface1), + colors = colors, + text = text, + onclick = onclick) +} + +@Composable +fun ConfirmButton(text: String, onclick: () -> Unit) { + val colors = ButtonDefaults.buttonColors( + backgroundColor = lightColorAccentSecondary + ) + NavigationButton( + colors = colors, + text = text, + onclick = onclick) +} + +@Composable +fun NavigationButton( + border: BorderStroke? = null, + colors: ButtonColors, + text: String, + onclick: () -> Unit +) { + Button( + onClick = onclick, + shape = Shapes.small, + colors = colors, + border = border + ) { + Text(text = text, style = Typography.button) + } +} + +/** + * BEGIN CREATE OPTION SELECTION CONTENT + */ +@ExperimentalMaterialApi +@Composable +fun CreationSelectionDialog( + providerInfo: ProviderInfo, + onOptionSelected: (String) -> Unit, + onCancel: () -> Unit, + multiProvider: Boolean, + onMoreOptionSelected: () -> Unit, +) { + val state = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Expanded, + skipHalfExpanded = true + ) + ModalBottomSheetLayout( + sheetState = state, + sheetContent = { + CreationSelectionCard( + providerInfo = providerInfo, + onCancel = onCancel, + onOptionSelected = onOptionSelected, + multiProvider = multiProvider, + onMoreOptionSelected = onMoreOptionSelected, + ) + }, + scrimColor = Color.Transparent, + sheetShape = Shapes.medium, + ) {} + LaunchedEffect(state.currentValue) { + when (state.currentValue) { + ModalBottomSheetValue.Hidden -> { + onCancel() + } + } + } +} + +@ExperimentalMaterialApi +@Composable +fun CreationSelectionCard( + providerInfo: ProviderInfo, + onOptionSelected: (String) -> Unit, + onCancel: () -> Unit, + multiProvider: Boolean, + onMoreOptionSelected: () -> Unit, +) { + Card( + backgroundColor = lightBackgroundColor, + ) { + Column() { + Icon( + bitmap = providerInfo.credentialTypeIcon.toBitmap().asImageBitmap(), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp) + ) + Text( + text = "${stringResource(R.string.choose_create_option_title)} ${providerInfo.name}", + style = Typography.subtitle1, + modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) + ) + Text( + text = providerInfo.appDomainName, + style = Typography.body2, + modifier = Modifier.padding(horizontal = 28.dp) + ) + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Card( + shape = Shapes.medium, + modifier = Modifier + .padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally) + ) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + providerInfo.createOptions.forEach { + item { + CreateOptionRow(createOptionInfo = it, onOptionSelected = onOptionSelected) + } + } + if (multiProvider) { + item { + MoreOptionRow(onSelect = onMoreOptionSelected) + } + } + } + } + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Row( + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) + ) { + CancelButton(stringResource(R.string.string_cancel), onCancel) + } + Divider( + thickness = 18.dp, + color = Color.Transparent, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +@ExperimentalMaterialApi +@Composable +fun CreateOptionRow(createOptionInfo: CreateOptionInfo, onOptionSelected: (String) -> Unit) { + Chip( + modifier = Modifier.fillMaxWidth(), + onClick = {onOptionSelected(createOptionInfo.id)}, + leadingIcon = { + Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + bitmap = createOptionInfo.icon.toBitmap().asImageBitmap(), + // painter = painterResource(R.drawable.ic_passkey), + // TODO: add description. + contentDescription = "") + }, + colors = ChipDefaults.chipColors( + backgroundColor = Grey100, + leadingIconContentColor = Grey100 + ), + shape = Shapes.large + ) { + Column() { + Text( + text = createOptionInfo.title, + style = Typography.h6, + modifier = Modifier.padding(top = 16.dp) + ) + Text( + text = createOptionInfo.subtitle, + style = Typography.body2, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +@ExperimentalMaterialApi +@Composable +fun MoreOptionRow(onSelect: () -> Unit) { + Chip( + modifier = Modifier.fillMaxWidth().height(52.dp), + onClick = onSelect, + colors = ChipDefaults.chipColors( + backgroundColor = Grey100, + leadingIconContentColor = Grey100 + ), + shape = Shapes.large + ) { + Text( + text = stringResource(R.string.string_more_options), + style = Typography.h6, + ) + } +} +/** + * END CREATE OPTION SELECTION CONTENT + */ + +/** + * END COMMON COMPONENTS + */ + +@ExperimentalMaterialApi +@Preview(showBackground = true) +@Composable +fun CreatePasskeyEntryScreenPreview() { + // val providers = ProviderList( + // listOf( + // ProviderInfo(null), + // ProviderInfo(null, "Dashlane"), + // ProviderInfo(null, "LastPass") + // ) + // ) + // TatiAccountSelectorTheme { + // ConfirmationCard({}, {}) + // } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt new file mode 100644 index 000000000000..355428555735 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt @@ -0,0 +1,51 @@ +package com.android.credentialmanager.createflow + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.navigation.NavController +import com.android.credentialmanager.CredentialManagerRepo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +data class CreatePasskeyUiState( + val providerList: ProviderList, +) + +class CreatePasskeyViewModel( + credManRepo: CredentialManagerRepo +) : ViewModel() { + + private val _uiState = MutableStateFlow( + CreatePasskeyUiState(credManRepo.createCredentialProviderList()) + ) + val uiState: StateFlow<CreatePasskeyUiState> = _uiState.asStateFlow() + + fun onConfirm(navController: NavController) { + if (uiState.value.providerList.providers.size > 1) { + navController.navigate("providerSelection") + } else if (uiState.value.providerList.providers.size == 1) { + onProviderSelected(uiState.value.providerList.providers[0].name, navController) + } else { + throw java.lang.IllegalStateException("Empty provider list.") + } + } + + fun onProviderSelected(providerName: String, navController: NavController) { + return navController.navigate("createCredentialSelection/$providerName") + } + + fun onCreateOptionSelected(createOptionId: String) { + Log.d("Account Selector", "Option selected for creation: $createOptionId") + } + + fun getProviderInfoByName(providerName: String): ProviderInfo { + return uiState.value.providerList.providers.single { + it.name.equals(providerName) + } + } + + fun onMoreOptionSelected(navController: NavController) { + navController.navigate("moreOption") + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt new file mode 100644 index 000000000000..6ad14db00024 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -0,0 +1,232 @@ +package com.android.credentialmanager.getflow + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Card +import androidx.compose.material.Chip +import androidx.compose.material.ChipDefaults +import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.Text +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.android.credentialmanager.R +import com.android.credentialmanager.createflow.CancelButton +import com.android.credentialmanager.ui.theme.Grey100 +import com.android.credentialmanager.ui.theme.Shapes +import com.android.credentialmanager.ui.theme.Typography +import com.android.credentialmanager.ui.theme.lightBackgroundColor + +@ExperimentalMaterialApi +fun NavGraphBuilder.getCredentialsGraph( + navController: NavController, + viewModel: GetCredentialViewModel, + onCancel: () -> Unit, + startDestination: String = "credentialSelection", // TODO: get this from view model +) { + navigation(startDestination = startDestination, route = "getCredentials") { + composable("credentialSelection") { + CredentialSelectionDialog( + providerInfo = viewModel.getDefaultProviderInfo(), + onOptionSelected = {viewModel.onCredentailSelected(it, navController)}, + onCancel = onCancel, + multiProvider = viewModel.uiState.collectAsState().value.providers.size > 1, + onMoreOptionSelected = {viewModel.onMoreOptionSelected(navController)} + ) + } + } +} + +/** + * BEGIN CREATE OPTION SELECTION CONTENT + */ +@ExperimentalMaterialApi +@Composable +fun CredentialSelectionDialog( + providerInfo: ProviderInfo, + onOptionSelected: (String) -> Unit, + onCancel: () -> Unit, + multiProvider: Boolean, + onMoreOptionSelected: () -> Unit, +) { + val state = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Expanded, + skipHalfExpanded = true + ) + ModalBottomSheetLayout( + sheetState = state, + sheetContent = { + CredentialSelectionCard( + providerInfo = providerInfo, + onCancel = onCancel, + onOptionSelected = onOptionSelected, + multiProvider = multiProvider, + onMoreOptionSelected = onMoreOptionSelected, + ) + }, + scrimColor = Color.Transparent, + sheetShape = Shapes.medium, + ) {} + LaunchedEffect(state.currentValue) { + when (state.currentValue) { + ModalBottomSheetValue.Hidden -> { + onCancel() + } + } + } +} + +@ExperimentalMaterialApi +@Composable +fun CredentialSelectionCard( + providerInfo: ProviderInfo, + onOptionSelected: (String) -> Unit, + onCancel: () -> Unit, + multiProvider: Boolean, + onMoreOptionSelected: () -> Unit, +) { + Card( + backgroundColor = lightBackgroundColor, + ) { + Column() { + Icon( + bitmap = providerInfo.credentialTypeIcon.toBitmap().asImageBitmap(), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp) + ) + Text( + text = stringResource(R.string.choose_sign_in_title), + style = Typography.subtitle1, + modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) + ) + Text( + text = providerInfo.appDomainName, + style = Typography.body2, + modifier = Modifier.padding(horizontal = 28.dp) + ) + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Card( + shape = Shapes.medium, + modifier = Modifier + .padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally) + ) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + providerInfo.credentialOptions.forEach { + item { + CredentialOptionRow(credentialOptionInfo = it, onOptionSelected = onOptionSelected) + } + } + if (multiProvider) { + item { + MoreOptionRow(onSelect = onMoreOptionSelected) + } + } + } + } + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Row( + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) + ) { + CancelButton(stringResource(R.string.string_no_thanks), onCancel) + } + Divider( + thickness = 18.dp, + color = Color.Transparent, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +@ExperimentalMaterialApi +@Composable +fun CredentialOptionRow( + credentialOptionInfo: CredentialOptionInfo, + onOptionSelected: (String) -> Unit +) { + Chip( + modifier = Modifier.fillMaxWidth(), + onClick = {onOptionSelected(credentialOptionInfo.id)}, + leadingIcon = { + Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + bitmap = credentialOptionInfo.icon.toBitmap().asImageBitmap(), + // painter = painterResource(R.drawable.ic_passkey), + // TODO: add description. + contentDescription = "") + }, + colors = ChipDefaults.chipColors( + backgroundColor = Grey100, + leadingIconContentColor = Grey100 + ), + shape = Shapes.large + ) { + Column() { + Text( + text = credentialOptionInfo.title, + style = Typography.h6, + modifier = Modifier.padding(top = 16.dp) + ) + Text( + text = credentialOptionInfo.subtitle, + style = Typography.body2, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +@ExperimentalMaterialApi +@Composable +fun MoreOptionRow(onSelect: () -> Unit) { + Chip( + modifier = Modifier.fillMaxWidth().height(52.dp), + onClick = onSelect, + colors = ChipDefaults.chipColors( + backgroundColor = Grey100, + leadingIconContentColor = Grey100 + ), + shape = Shapes.large + ) { + Text( + text = stringResource(R.string.string_more_options), + style = Typography.h6, + ) + } +} +/** + * END CREATE OPTION SELECTION CONTENT + */ diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt new file mode 100644 index 000000000000..20057de8fec2 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt @@ -0,0 +1,36 @@ +package com.android.credentialmanager.getflow + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.navigation.NavController +import com.android.credentialmanager.CredentialManagerRepo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +data class GetCredentialUiState( + val providers: List<ProviderInfo> +) + +class GetCredentialViewModel( + credManRepo: CredentialManagerRepo +) : ViewModel() { + + private val _uiState = MutableStateFlow( + GetCredentialUiState(credManRepo.getCredentialProviderList()) + ) + val uiState: StateFlow<GetCredentialUiState> = _uiState.asStateFlow() + + fun getDefaultProviderInfo(): ProviderInfo { + // TODO: correctly get the default provider. + return uiState.value.providers.first() + } + + fun onCredentailSelected(credentialId: String, navController: NavController) { + Log.d("Account Selector", "credential selected: $credentialId") + } + + fun onMoreOptionSelected(navController: NavController) { + Log.d("Account Selector", "More Option selected") + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt new file mode 100644 index 000000000000..8710eceeb723 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -0,0 +1,18 @@ +package com.android.credentialmanager.getflow + +import android.graphics.drawable.Drawable + +data class ProviderInfo( + val icon: Drawable, + val name: String, + val appDomainName: String, + val credentialTypeIcon: Drawable, + val credentialOptions: List<CredentialOptionInfo>, +) + +data class CredentialOptionInfo( + val icon: Drawable, + val title: String, + val subtitle: String, + val id: String, +) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt new file mode 100644 index 000000000000..abb4bfbf915e --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt @@ -0,0 +1,14 @@ +package com.android.credentialmanager.ui.theme + +import androidx.compose.ui.graphics.Color + +val Grey100 = Color(0xFFF1F3F4) +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) +val lightColorAccentSecondary = Color(0xFFC2E7FF) +val lightBackgroundColor = Color(0xFFF0F0F0) +val lightSurface1 = Color(0xFF6991D6) +val textColorSecondary = Color(0xFF40484B) +val textColorPrimary = Color(0xFF191C1D) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt new file mode 100644 index 000000000000..cba86585ee59 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.android.credentialmanager.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(100.dp), + medium = RoundedCornerShape(20.dp), + large = RoundedCornerShape(0.dp) +) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt new file mode 100644 index 000000000000..a9d20ae9c42e --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt @@ -0,0 +1,47 @@ +package com.android.credentialmanager.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColors( + primary = Purple200, + primaryVariant = Purple700, + secondary = Teal200 +) + +private val LightColorPalette = lightColors( + primary = Purple500, + primaryVariant = Purple700, + secondary = Teal200 + + /* Other default colors to override + background = Color.White, + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.Black, + onBackground = Color.Black, + onSurface = Color.Black, + */ +) + +@Composable +fun CredentialSelectorTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt new file mode 100644 index 000000000000..d8fb01c17f95 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt @@ -0,0 +1,56 @@ +package com.android.credentialmanager.ui.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + subtitle1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp, + ), + body1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + ), + body2 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + color = textColorSecondary + ), + button = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + ), + h6 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + color = textColorPrimary + ), + + /* Other default text styles to override + button = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W500, + fontSize = 14.sp + ), + caption = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp + ) + */ +) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt index 773e5f3929fb..6fe88e1fd37c 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt @@ -54,7 +54,7 @@ fun createSettingsPage( parameter: List<NamedNavArgument> = emptyList(), arguments: Bundle? = null ): SettingsPage { - return SettingsPage( + return SettingsPage.create( name = SppName.name, displayName = SppName.displayName, parameter = parameter, diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt index c8a4650d3f01..c68e918eb449 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt @@ -18,14 +18,18 @@ package com.android.settingslib.spa.gallery.home import android.os.Bundle import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.util.getRuntimeArguments +import com.android.settingslib.spa.framework.util.mergeArguments import com.android.settingslib.spa.gallery.R import com.android.settingslib.spa.gallery.SettingsPageProviderEnum import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider +import com.android.settingslib.spa.gallery.createSettingsPage import com.android.settingslib.spa.gallery.page.ArgumentPageModel import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider @@ -41,27 +45,36 @@ object HomePageProvider : SettingsPageProvider { override val name = SettingsPageProviderEnum.HOME.name override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { + val owner = createSettingsPage(SettingsPageProviderEnum.HOME) return listOf( - PreferenceMainPageProvider.buildInjectEntry().build(), - ArgumentPageProvider.buildInjectEntry("foo")!!.build(), - SliderPageProvider.buildInjectEntry().build(), - SpinnerPageProvider.buildInjectEntry().build(), - SettingsPagerPageProvider.buildInjectEntry().build(), - FooterPageProvider.buildInjectEntry().build(), - IllustrationPageProvider.buildInjectEntry().build(), - CategoryPageProvider.buildInjectEntry().build(), - ActionButtonPageProvider.buildInjectEntry().build(), + PreferenceMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + ArgumentPageProvider.buildInjectEntry("foo")!!.setLink(fromPage = owner).build(), + SliderPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + SpinnerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + SettingsPagerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + FooterPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) } @Composable override fun Page(arguments: Bundle?) { + val globalRuntimeArgs = remember { getRuntimeArguments(arguments) } HomeScaffold(title = stringResource(R.string.app_name)) { for (entry in buildEntry(arguments)) { if (entry.owner.isCreateBy(SettingsPageProviderEnum.ARGUMENT.name)) { - entry.UiLayout(ArgumentPageModel.buildArgument(intParam = 0)) + entry.UiLayout( + mergeArguments( + listOf( + globalRuntimeArgs, + ArgumentPageModel.buildArgument(intParam = 0) + ) + ) + ) } else { - entry.UiLayout() + entry.UiLayout(globalRuntimeArgs) } } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt index cc0a79adaa88..9bf7ad8fb8f7 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt @@ -18,12 +18,15 @@ package com.android.settingslib.spa.gallery.page import android.os.Bundle import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.util.getRuntimeArguments +import com.android.settingslib.spa.framework.util.mergeArguments import com.android.settingslib.spa.gallery.SettingsPageProviderEnum import com.android.settingslib.spa.gallery.createSettingsPage import com.android.settingslib.spa.widget.preference.Preference @@ -98,12 +101,20 @@ object ArgumentPageProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { + val globalRuntimeArgs = remember { getRuntimeArguments(arguments) } RegularScaffold(title = ArgumentPageModel.create(arguments).genPageTitle()) { for (entry in buildEntry(arguments)) { - if (entry.owner.isCreateBy(SettingsPageProviderEnum.ARGUMENT.name)) { - entry.UiLayout(ArgumentPageModel.buildNextArgument(arguments)) + if (entry.toPage != null) { + entry.UiLayout( + mergeArguments( + listOf( + globalRuntimeArgs, + ArgumentPageModel.buildNextArgument(arguments) + ) + ) + ) } else { - entry.UiLayout() + entry.UiLayout(globalRuntimeArgs) } } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt index e75d09b7d17c..ee2bde4c2edb 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavType import androidx.navigation.navArgument -import com.android.settingslib.spa.framework.BrowseActivity import com.android.settingslib.spa.framework.common.EntrySearchData import com.android.settingslib.spa.framework.common.PageModel import com.android.settingslib.spa.framework.compose.navigator @@ -92,14 +91,12 @@ class ArgumentPageModel : PageModel() { private var arguments: Bundle? = null private var stringParam: String? = null private var intParam: Int? = null - private var highlightName: String? = null override fun initialize(arguments: Bundle?) { logMsg("init with args " + arguments.toString()) this.arguments = arguments stringParam = parameter.getStringArg(STRING_PARAM_NAME, arguments) intParam = parameter.getIntArg(INT_PARAM_NAME, arguments) - highlightName = arguments?.getString(BrowseActivity.HIGHLIGHT_ENTRY_PARAM_NAME) } @Composable @@ -133,7 +130,8 @@ class ArgumentPageModel : PageModel() { override val title = genPageTitle() override val summary = stateOf(summaryArray.joinToString(", ")) override val onClick = navigator( - SettingsPageProviderEnum.ARGUMENT.displayName + parameter.navLink(arguments)) + SettingsPageProviderEnum.ARGUMENT.displayName + parameter.navLink(arguments) + ) } } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt index 9fc736c911af..89dbcb1357b6 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt @@ -30,6 +30,7 @@ import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.util.getRuntimeArguments import com.android.settingslib.spa.gallery.SettingsPageProviderEnum import com.android.settingslib.spa.gallery.createSettingsPage import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_TITLE @@ -177,9 +178,10 @@ object PreferencePageProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { + val globalRuntimeArgs = remember { getRuntimeArguments(arguments) } RegularScaffold(title = PAGE_TITLE) { for (entry in buildEntry(arguments)) { - entry.UiLayout() + entry.UiLayout(globalRuntimeArgs) } } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt index 9a525bf5426d..a4713b993af0 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt @@ -35,7 +35,7 @@ import com.android.settingslib.spa.widget.ui.CategoryTitle private const val TITLE = "Sample Category" object CategoryPageProvider : SettingsPageProvider { - override val name = "Spinner" + override val name = "Category" fun buildInjectEntry(): SettingsEntryBuilder { return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name)) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt index c4e88e158e7a..c698d9c7ca9a 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt @@ -153,7 +153,7 @@ open class DebugActivity( "${pageWithEntry.page.displayName} (${pageWithEntry.entries.size})" override val summary = pageWithEntry.page.formatArguments().toState() override val onClick = - navigator(route = ROUTE_PAGE + "/${pageWithEntry.page.id()}") + navigator(route = ROUTE_PAGE + "/${pageWithEntry.page.id}") }) } } @@ -172,7 +172,7 @@ open class DebugActivity( val id = arguments!!.getString(PARAM_NAME_PAGE_ID, "") val pageWithEntry = entryRepository.getPageWithEntry(id)!! RegularScaffold(title = "Page - ${pageWithEntry.page.displayName}") { - Text(text = "id = ${pageWithEntry.page.id()}") + Text(text = "id = ${pageWithEntry.page.id}") Text(text = pageWithEntry.page.formatArguments()) Text(text = "Entry size: ${pageWithEntry.entries.size}") Preference(model = object : PreferenceModel { @@ -206,7 +206,7 @@ open class DebugActivity( override val title = entry.displayTitle() override val summary = "${entry.fromPage?.displayName} -> ${entry.toPage?.displayName}".toState() - override val onClick = navigator(route = ROUTE_ENTRY + "/${entry.id()}") + override val onClick = navigator(route = ROUTE_ENTRY + "/${entry.id}") }) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt index 33f1eb5b723d..d923c1ce83e8 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt @@ -38,8 +38,9 @@ import com.android.settingslib.spa.framework.common.SettingsPage * For gallery, AuthorityPath = com.android.spa.gallery.provider * For SettingsGoogle, AuthorityPath = com.android.settings.spa.provider * Some examples: - * $ adb shell content query --uri content://<AuthorityPath>/page_start + * $ adb shell content query --uri content://<AuthorityPath>/page_debug * $ adb shell content query --uri content://<AuthorityPath>/page_info + * $ adb shell content query --uri content://<AuthorityPath>/entry_info */ open class EntryProvider( private val entryRepository: SettingsEntryRepository, @@ -50,6 +51,7 @@ open class EntryProvider( * Enum to define all column names in provider. */ enum class ColumnEnum(val id: String) { + // Columns related to page PAGE_ID("pageId"), PAGE_NAME("pageName"), PAGE_ROUTE("pageRoute"), @@ -57,6 +59,13 @@ open class EntryProvider( PAGE_ENTRY_COUNT("entryCount"), HAS_RUNTIME_PARAM("hasRuntimeParam"), PAGE_START_ADB("pageStartAdb"), + + // Columns related to entry + ENTRY_ID("entryId"), + ENTRY_NAME("entryName"), + ENTRY_ROUTE("entryRoute"), + ENTRY_TITLE("entryTitle"), + ENTRY_SEARCH_KEYWORD("entrySearchKw"), } /** @@ -67,12 +76,15 @@ open class EntryProvider( val queryMatchCode: Int, val columnNames: List<ColumnEnum> ) { - PAGE_START_COMMAND( - "page_start", 1, + // For debug + PAGE_DEBUG_QUERY( + "page_debug", 1, listOf(ColumnEnum.PAGE_START_ADB) ), + + // page related queries. PAGE_INFO_QUERY( - "page_info", 2, + "page_info", 100, listOf( ColumnEnum.PAGE_ID, ColumnEnum.PAGE_NAME, @@ -82,9 +94,24 @@ open class EntryProvider( ColumnEnum.HAS_RUNTIME_PARAM, ) ), + + // entry related queries + ENTRY_INFO_QUERY( + "entry_info", 200, + listOf( + ColumnEnum.ENTRY_ID, + ColumnEnum.ENTRY_NAME, + ColumnEnum.ENTRY_ROUTE, + ColumnEnum.ENTRY_TITLE, + ColumnEnum.ENTRY_SEARCH_KEYWORD, + ) + ) } - private var uriMatcher: UriMatcher? = null + private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) + private fun addUri(authority: String, query: QueryEnum) { + uriMatcher.addURI(authority, query.queryPath, query.queryMatchCode) + } override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { TODO("Implement this to handle requests to delete one or more rows") @@ -115,18 +142,10 @@ open class EntryProvider( } override fun attachInfo(context: Context?, info: ProviderInfo?) { - uriMatcher = UriMatcher(UriMatcher.NO_MATCH) if (info != null) { - uriMatcher!!.addURI( - info.authority, - QueryEnum.PAGE_START_COMMAND.queryPath, - QueryEnum.PAGE_START_COMMAND.queryMatchCode - ) - uriMatcher!!.addURI( - info.authority, - QueryEnum.PAGE_INFO_QUERY.queryPath, - QueryEnum.PAGE_INFO_QUERY.queryMatchCode - ) + addUri(info.authority, QueryEnum.PAGE_DEBUG_QUERY) + addUri(info.authority, QueryEnum.PAGE_INFO_QUERY) + addUri(info.authority, QueryEnum.ENTRY_INFO_QUERY) } super.attachInfo(context, info) } @@ -139,9 +158,10 @@ open class EntryProvider( sortOrder: String? ): Cursor? { return try { - when (uriMatcher!!.match(uri)) { - QueryEnum.PAGE_START_COMMAND.queryMatchCode -> queryPageStartCommand() + when (uriMatcher.match(uri)) { + QueryEnum.PAGE_DEBUG_QUERY.queryMatchCode -> queryPageDebug() QueryEnum.PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo() + QueryEnum.ENTRY_INFO_QUERY.queryMatchCode -> queryEntryInfo() else -> throw UnsupportedOperationException("Unknown Uri $uri") } } catch (e: UnsupportedOperationException) { @@ -152,8 +172,8 @@ open class EntryProvider( } } - private fun queryPageStartCommand(): Cursor { - val cursor = MatrixCursor(QueryEnum.PAGE_START_COMMAND.getColumns()) + private fun queryPageDebug(): Cursor { + val cursor = MatrixCursor(QueryEnum.PAGE_DEBUG_QUERY.getColumns()) for (pageWithEntry in entryRepository.getAllPageWithEntry()) { val command = createBrowsePageAdbCommand(pageWithEntry.page) if (command != null) { @@ -168,7 +188,7 @@ open class EntryProvider( for (pageWithEntry in entryRepository.getAllPageWithEntry()) { val page = pageWithEntry.page cursor.newRow() - .add(ColumnEnum.PAGE_ID.id, page.id()) + .add(ColumnEnum.PAGE_ID.id, page.id) .add(ColumnEnum.PAGE_NAME.id, page.displayName) .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute()) .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size) @@ -181,6 +201,24 @@ open class EntryProvider( return cursor } + private fun queryEntryInfo(): Cursor { + val cursor = MatrixCursor(QueryEnum.ENTRY_INFO_QUERY.getColumns()) + for (entry in entryRepository.getAllEntries()) { + // We can add runtime arguments if necessary + val searchData = entry.getSearchData() + cursor.newRow() + .add(ColumnEnum.ENTRY_ID.id, entry.id) + .add(ColumnEnum.ENTRY_NAME.id, entry.displayName) + .add(ColumnEnum.ENTRY_ROUTE.id, entry.buildRoute()) + .add(ColumnEnum.ENTRY_TITLE.id, searchData?.title ?: "") + .add( + ColumnEnum.ENTRY_SEARCH_KEYWORD.id, + searchData?.keyword ?: emptyList<String>() + ) + } + return cursor + } + private fun createBrowsePageIntent(page: SettingsPage): Intent { if (context == null || browseActivityClass == null || page.hasRuntimeParam()) return Intent() diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt index 2239066201bf..07a6d6cab6fd 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt @@ -17,8 +17,13 @@ package com.android.settingslib.spa.framework.common import android.os.Bundle +import android.widget.Toast import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.platform.LocalContext +import com.android.settingslib.spa.framework.BrowseActivity.Companion.HIGHLIGHT_ENTRY_PARAM_NAME const val INJECT_ENTRY_NAME = "INJECT" const val ROOT_ENTRY_NAME = "ROOT" @@ -27,6 +32,9 @@ const val ROOT_ENTRY_NAME = "ROOT" * Defines data of a Settings entry. */ data class SettingsEntry( + // The unique id of this entry, which is computed by name + owner + fromPage + toPage. + val id: String, + // The name of the page, which is used to compute the unique id, and need to be stable. private val name: String, @@ -67,14 +75,9 @@ data class SettingsEntry( */ private val uiLayoutImpl: (@Composable (arguments: Bundle?) -> Unit) = {}, ) { - // The unique id of this entry, which is computed by name + owner + fromPage + toPage. - fun id(): String { - return "$name:${owner.id()}(${fromPage?.id()}-${toPage?.id()})".toHashId() - } - fun formatContent(): String { val content = listOf( - "id = ${id()}", + "id = $id", "owner = ${owner.formatDisplayTitle()}", "linkFrom = ${fromPage?.formatDisplayTitle()}", "linkTo = ${toPage?.formatDisplayTitle()}", @@ -94,7 +97,7 @@ data class SettingsEntry( } fun buildRoute(): String { - return containerPage().buildRoute(id()) + return containerPage().buildRoute(id) } fun hasRuntimeParam(): Boolean { @@ -104,6 +107,7 @@ data class SettingsEntry( private fun fullArgument(runtimeArguments: Bundle? = null): Bundle { val arguments = Bundle() if (owner.arguments != null) arguments.putAll(owner.arguments) + // Put runtime args later, which can override page args. if (runtimeArguments != null) arguments.putAll(runtimeArguments) return arguments } @@ -114,6 +118,15 @@ data class SettingsEntry( @Composable fun UiLayout(runtimeArguments: Bundle? = null) { + val context = LocalContext.current + val highlight = rememberSaveable { + mutableStateOf(runtimeArguments?.getString(HIGHLIGHT_ENTRY_PARAM_NAME) == id) + } + if (highlight.value) { + highlight.value = false + // TODO: Add highlight entry logic + Toast.makeText(context, "entry $id highlighted", Toast.LENGTH_SHORT).show() + } uiLayoutImpl(fullArgument(runtimeArguments)) } } @@ -135,6 +148,7 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings fun build(): SettingsEntry { return SettingsEntry( + id = id(), name = name, owner = owner, displayName = displayName, @@ -190,6 +204,11 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings return this } + // The unique id of this entry, which is computed by name + owner + fromPage + toPage. + private fun id(): String { + return "$name:${owner.id}(${fromPage?.id}-${toPage?.id})".toHashId() + } + companion object { fun create(entryName: String, owner: SettingsPage): SettingsEntryBuilder { return SettingsEntryBuilder(entryName, owner) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt index 77f064d35eb7..b6f6203209b1 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt @@ -44,29 +44,23 @@ class SettingsEntryRepository(sppRepository: SettingsPageProviderRepository) { val entryQueue = LinkedList<SettingsEntry>() for (page in sppRepository.getAllRootPages()) { val rootEntry = SettingsEntryBuilder.createRoot(owner = page).build() - val rootEntryId = rootEntry.id() - if (!entryMap.containsKey(rootEntryId)) { + if (!entryMap.containsKey(rootEntry.id)) { entryQueue.push(rootEntry) - entryMap.put(rootEntryId, rootEntry) + entryMap.put(rootEntry.id, rootEntry) } } while (entryQueue.isNotEmpty() && entryMap.size < MAX_ENTRY_SIZE) { val entry = entryQueue.pop() val page = entry.toPage - val pageId = page?.id() - if (pageId == null || pageWithEntryMap.containsKey(pageId)) continue + if (page == null || pageWithEntryMap.containsKey(page.id)) continue val spp = sppRepository.getProviderOrNull(page.name) ?: continue - val newEntries = spp.buildEntry(page.arguments).map { - // Set from-page if it is missing. - if (it.fromPage == null) it.copy(fromPage = page) else it - } - pageWithEntryMap[pageId] = SettingsPageWithEntry(page, newEntries) + val newEntries = spp.buildEntry(page.arguments) + pageWithEntryMap[page.id] = SettingsPageWithEntry(page, newEntries) for (newEntry in newEntries) { - val newEntryId = newEntry.id() - if (!entryMap.containsKey(newEntryId)) { + if (!entryMap.containsKey(newEntry.id)) { entryQueue.push(newEntry) - entryMap.put(newEntryId, newEntry) + entryMap.put(newEntry.id, newEntry) } } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt index 124743a23274..0c301b937cce 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt @@ -26,6 +26,9 @@ import com.android.settingslib.spa.framework.util.navLink * Defines data to identify a Settings page. */ data class SettingsPage( + // The unique id of this page, which is computed by name + normalized(arguments) + val id: String, + // The name of the page, which is used to compute the unique id, and need to be stable. val name: String, @@ -41,17 +44,28 @@ data class SettingsPage( companion object { fun create( name: String, + displayName: String? = null, parameter: List<NamedNavArgument> = emptyList(), arguments: Bundle? = null ): SettingsPage { - return SettingsPage(name, name, parameter, arguments) + return SettingsPage( + id = id(name, parameter, arguments), + name = name, + displayName = displayName ?: name, + parameter = parameter, + arguments = arguments + ) } - } - // The unique id of this page, which is computed by name + normalized(arguments) - fun id(): String { - val normArguments = parameter.normalize(arguments) - return "$name:${normArguments?.toString()}".toHashId() + // The unique id of this page, which is computed by name + normalized(arguments) + private fun id( + name: String, + parameter: List<NamedNavArgument> = emptyList(), + arguments: Bundle? = null + ): String { + val normArguments = parameter.normalize(arguments) + return "$name:${normArguments?.toString()}".toHashId() + } } // Returns if this Settings Page is created by the given Spp. @@ -69,12 +83,12 @@ data class SettingsPage( return "$displayName ${formatArguments()}" } - fun buildRoute(highlightEntryName: String? = null): String { + fun buildRoute(highlightEntryId: String? = null): String { val highlightParam = - if (highlightEntryName == null) + if (highlightEntryId == null) "" else - "?${BrowseActivity.HIGHLIGHT_ENTRY_PARAM_NAME}=$highlightEntryName" + "?${BrowseActivity.HIGHLIGHT_ENTRY_PARAM_NAME}=$highlightEntryId" return name + parameter.navLink(arguments) + highlightParam } @@ -114,5 +128,5 @@ private fun List<NamedNavArgument>.hasRuntimeParam(arguments: Bundle? = null): B } fun String.toHashId(): String { - return this.hashCode().toString(36) + return this.hashCode().toUInt().toString(36) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt index d7d7750e37b9..452f76abb984 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt @@ -19,6 +19,7 @@ package com.android.settingslib.spa.framework.util import android.os.Bundle import androidx.navigation.NamedNavArgument import androidx.navigation.NavType +import com.android.settingslib.spa.framework.BrowseActivity fun List<NamedNavArgument>.navRoute(): String { return this.joinToString("") { argument -> "/{${argument.name}}" } @@ -70,3 +71,21 @@ fun List<NamedNavArgument>.containsIntArg(name: String): Boolean { } return false } + +fun getRuntimeArguments(arguments: Bundle? = null): Bundle { + val res = Bundle() + val highlightEntry = arguments?.getString(BrowseActivity.HIGHLIGHT_ENTRY_PARAM_NAME) + if (highlightEntry != null) { + res.putString(BrowseActivity.HIGHLIGHT_ENTRY_PARAM_NAME, highlightEntry) + } + // Append more general runtime arguments here + return res +} + +fun mergeArguments(argsList: List<Bundle?>): Bundle { + val res = Bundle() + for (args in argsList) { + if (args != null) res.putAll(args) + } + return res +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt index 80b2364ce68d..883eddff8054 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt @@ -63,7 +63,7 @@ internal class TogglePermissionAppInfoPageProvider( override val parameter = PAGE_PARAMETER override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - val owner = SettingsPage.create(name, parameter, arguments) + val owner = SettingsPage.create(name, parameter = parameter, arguments = arguments) val entryList = mutableListOf<SettingsEntry>() entryList.add( SettingsEntryBuilder.create(ENTRY_NAME, owner).setIsAllowSearch(false).build() @@ -108,7 +108,10 @@ internal class TogglePermissionAppInfoPageProvider( fun buildPageData(permissionType: String): SettingsPage { return SettingsPage.create( - PAGE_NAME, PAGE_PARAMETER, bundleOf(PERMISSION to permissionType)) + name = PAGE_NAME, + parameter = PAGE_PARAMETER, + arguments = bundleOf(PERMISSION to permissionType) + ) } } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt index bd60bd3699c1..ec7d75e969df 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt @@ -63,7 +63,7 @@ internal class TogglePermissionAppListPageProvider( override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { val permissionType = parameter.getStringArg(PERMISSION, arguments)!! - val appListPage = SettingsPage.create(name, parameter, arguments) + val appListPage = SettingsPage.create(name, parameter = parameter, arguments = arguments) val appInfoPage = TogglePermissionAppInfoPageProvider.buildPageData(permissionType) val entryList = mutableListOf<SettingsEntry>() // TODO: add more categories, such as personal, work, cloned, etc. @@ -117,7 +117,10 @@ internal class TogglePermissionAppListPageProvider( listModelSupplier: (Context) -> TogglePermissionAppListModel<out AppRecord>, ): SettingsEntryBuilder { val appListPage = SettingsPage.create( - PAGE_NAME, PAGE_PARAMETER, bundleOf(PERMISSION to permissionType)) + name = PAGE_NAME, + parameter = PAGE_PARAMETER, + arguments = bundleOf(PERMISSION to permissionType) + ) return SettingsEntryBuilder.createInject(owner = appListPage).setIsAllowSearch(false) .setUiLayoutFn { val listModel = rememberContext(listModelSupplier) diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 123c01b6e12f..79fb56602328 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -210,13 +210,15 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { LocalBluetoothLeBroadcast(Context context) { mExecutor = Executors.newSingleThreadExecutor(); - BluetoothAdapter.getDefaultAdapter(). - getProfileProxy(context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST); mBuilder = new BluetoothLeAudioContentMetadata.Builder(); mContentResolver = context.getContentResolver(); Handler handler = new Handler(Looper.getMainLooper()); mSettingsObserver = new BroadcastSettingsObserver(handler); updateBroadcastInfoFromContentProvider(); + + // Before registering callback, the constructor should finish creating the all of variables. + BluetoothAdapter.getDefaultAdapter() + .getProfileProxy(context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST); } /** diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index f05f63700710..b1979c905510 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -768,6 +768,7 @@ public class SettingsBackupTest { Settings.Secure.SMS_DEFAULT_APPLICATION, Settings.Secure.SPELL_CHECKER_ENABLED, // Intentionally removed in Q Settings.Secure.TRUST_AGENTS_INITIALIZED, + Settings.Secure.KNOWN_TRUST_AGENTS_INITIALIZED, Settings.Secure.TV_APP_USES_NON_SYSTEM_INPUTS, Settings.Secure.TV_INPUT_CUSTOM_LABELS, Settings.Secure.TV_INPUT_HIDDEN_INPUTS, diff --git a/packages/SystemUI/res/drawable/qs_hotspot_icon_off.xml b/packages/SystemUI/res/drawable/qs_hotspot_icon_off.xml new file mode 100644 index 000000000000..eb160de7f249 --- /dev/null +++ b/packages/SystemUI/res/drawable/qs_hotspot_icon_off.xml @@ -0,0 +1,99 @@ +<?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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_0_G_D_1_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="133" + android:propertyName="fillAlpha" + android:startOffset="0" + android:valueFrom="1" + android:valueTo="0.3" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_2_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="133" + android:propertyName="fillAlpha" + android:startOffset="0" + android:valueFrom="1" + android:valueTo="0.3" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="150" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:translateX="12" + android:translateY="12"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M0 -1 C-1.1,-1 -2,-0.1 -2,1 C-2,1.55 -1.77,2.05 -1.41,2.41 C-1.05,2.77 -0.55,3 0,3 C0.55,3 1.05,2.77 1.41,2.41 C1.77,2.05 2,1.55 2,1 C2,-0.1 1.1,-1 0,-1c " /> + <path + android:name="_R_G_L_0_G_D_1_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M0 -5 C-3.31,-5 -6,-2.31 -6,1 C-6,2.66 -5.32,4.15 -4.24,5.24 C-4.24,5.24 -2.82,3.82 -2.82,3.82 C-3.55,3.1 -4,2.11 -4,1 C-4,-1.21 -2.21,-3 0,-3 C2.21,-3 4,-1.21 4,1 C4,2.11 3.55,3.1 2.82,3.82 C2.82,3.82 4.24,5.24 4.24,5.24 C5.32,4.15 6,2.66 6,1 C6,-2.31 3.31,-5 0,-5c " /> + <path + android:name="_R_G_L_0_G_D_2_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M0 -9 C-5.52,-9 -10,-4.52 -10,1 C-10,3.76 -8.88,6.26 -7.07,8.07 C-7.07,8.07 -5.66,6.66 -5.66,6.66 C-7.1,5.21 -8,3.21 -8,1 C-8,-3.42 -4.42,-7 0,-7 C4.42,-7 8,-3.42 8,1 C8,3.21 7.1,5.2 5.65,6.65 C5.65,6.65 7.06,8.06 7.06,8.06 C8.88,6.26 10,3.76 10,1 C10,-4.52 5.52,-9 0,-9c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector> diff --git a/packages/SystemUI/res/drawable/qs_hotspot_icon_on.xml b/packages/SystemUI/res/drawable/qs_hotspot_icon_on.xml new file mode 100644 index 000000000000..de972a62d7f7 --- /dev/null +++ b/packages/SystemUI/res/drawable/qs_hotspot_icon_on.xml @@ -0,0 +1,110 @@ +<?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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_0_G_D_1_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="250" + android:propertyName="fillAlpha" + android:startOffset="0" + android:valueFrom="0.3" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_2_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="167" + android:propertyName="fillAlpha" + android:startOffset="0" + android:valueFrom="0.3" + android:valueTo="0.3" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="250" + android:propertyName="fillAlpha" + android:startOffset="167" + android:valueFrom="0.3" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="433" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:translateX="12" + android:translateY="12"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M0 -1 C-1.1,-1 -2,-0.1 -2,1 C-2,1.55 -1.77,2.05 -1.41,2.41 C-1.05,2.77 -0.55,3 0,3 C0.55,3 1.05,2.77 1.41,2.41 C1.77,2.05 2,1.55 2,1 C2,-0.1 1.1,-1 0,-1c " /> + <path + android:name="_R_G_L_0_G_D_1_P_0" + android:fillAlpha="0.3" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M0 -5 C-3.31,-5 -6,-2.31 -6,1 C-6,2.66 -5.32,4.15 -4.24,5.24 C-4.24,5.24 -2.82,3.82 -2.82,3.82 C-3.55,3.1 -4,2.11 -4,1 C-4,-1.21 -2.21,-3 0,-3 C2.21,-3 4,-1.21 4,1 C4,2.11 3.55,3.1 2.82,3.82 C2.82,3.82 4.24,5.24 4.24,5.24 C5.32,4.15 6,2.66 6,1 C6,-2.31 3.31,-5 0,-5c " /> + <path + android:name="_R_G_L_0_G_D_2_P_0" + android:fillAlpha="0.3" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M0 -9 C-5.52,-9 -10,-4.52 -10,1 C-10,3.76 -8.88,6.26 -7.07,8.07 C-7.07,8.07 -5.66,6.66 -5.66,6.66 C-7.1,5.21 -8,3.21 -8,1 C-8,-3.42 -4.42,-7 0,-7 C4.42,-7 8,-3.42 8,1 C8,3.21 7.1,5.2 5.65,6.65 C5.65,6.65 7.06,8.06 7.06,8.06 C8.88,6.26 10,3.76 10,1 C10,-4.52 5.52,-9 0,-9c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector> diff --git a/packages/SystemUI/res/drawable/qs_hotspot_icon_search.xml b/packages/SystemUI/res/drawable/qs_hotspot_icon_search.xml new file mode 100644 index 000000000000..e33b264ad528 --- /dev/null +++ b/packages/SystemUI/res/drawable/qs_hotspot_icon_search.xml @@ -0,0 +1,132 @@ +<?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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_0_G_D_1_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="250" + android:propertyName="fillAlpha" + android:startOffset="0" + android:valueFrom="0.3" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="250" + android:propertyName="fillAlpha" + android:startOffset="250" + android:valueFrom="1" + android:valueTo="0.3" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_2_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="167" + android:propertyName="fillAlpha" + android:startOffset="0" + android:valueFrom="0.3" + android:valueTo="0.3" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="250" + android:propertyName="fillAlpha" + android:startOffset="167" + android:valueFrom="0.3" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="250" + android:propertyName="fillAlpha" + android:startOffset="417" + android:valueFrom="1" + android:valueTo="0.3" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="850" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:translateX="12" + android:translateY="12"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M0 -1 C-1.1,-1 -2,-0.1 -2,1 C-2,1.55 -1.77,2.05 -1.41,2.41 C-1.05,2.77 -0.55,3 0,3 C0.55,3 1.05,2.77 1.41,2.41 C1.77,2.05 2,1.55 2,1 C2,-0.1 1.1,-1 0,-1c " /> + <path + android:name="_R_G_L_0_G_D_1_P_0" + android:fillAlpha="0.3" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M0 -5 C-3.31,-5 -6,-2.31 -6,1 C-6,2.66 -5.32,4.15 -4.24,5.24 C-4.24,5.24 -2.82,3.82 -2.82,3.82 C-3.55,3.1 -4,2.11 -4,1 C-4,-1.21 -2.21,-3 0,-3 C2.21,-3 4,-1.21 4,1 C4,2.11 3.55,3.1 2.82,3.82 C2.82,3.82 4.24,5.24 4.24,5.24 C5.32,4.15 6,2.66 6,1 C6,-2.31 3.31,-5 0,-5c " /> + <path + android:name="_R_G_L_0_G_D_2_P_0" + android:fillAlpha="0.3" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M0 -9 C-5.52,-9 -10,-4.52 -10,1 C-10,3.76 -8.88,6.26 -7.07,8.07 C-7.07,8.07 -5.66,6.66 -5.66,6.66 C-7.1,5.21 -8,3.21 -8,1 C-8,-3.42 -4.42,-7 0,-7 C4.42,-7 8,-3.42 8,1 C8,3.21 7.1,5.2 5.65,6.65 C5.65,6.65 7.06,8.06 7.06,8.06 C8.88,6.26 10,3.76 10,1 C10,-4.52 5.52,-9 0,-9c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector> diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml index acb47f7a037c..2d67d95ab17e 100644 --- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml +++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml @@ -14,8 +14,9 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.systemui.dreams.complication.DoubleShadowTextClock +<com.android.systemui.shared.shadow.DoubleShadowTextClock xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/time_view" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -25,4 +26,13 @@ android:format24Hour="@string/dream_time_complication_24_hr_time_format" android:fontFeatureSettings="pnum, lnum" android:letterSpacing="0.02" - android:textSize="@dimen/dream_overlay_complication_clock_time_text_size"/> + android:textSize="@dimen/dream_overlay_complication_clock_time_text_size" + app:keyShadowBlur="@dimen/dream_overlay_clock_key_text_shadow_radius" + app:keyShadowOffsetX="@dimen/dream_overlay_clock_key_text_shadow_dx" + app:keyShadowOffsetY="@dimen/dream_overlay_clock_key_text_shadow_dy" + app:keyShadowAlpha="0.3" + app:ambientShadowBlur="@dimen/dream_overlay_clock_ambient_text_shadow_radius" + app:ambientShadowOffsetX="@dimen/dream_overlay_clock_ambient_text_shadow_dx" + app:ambientShadowOffsetY="@dimen/dream_overlay_clock_ambient_text_shadow_dy" + app:ambientShadowAlpha="0.3" +/> diff --git a/packages/SystemUI/shared/res/values/attrs.xml b/packages/SystemUI/shared/res/values/attrs.xml index f9d66ee583da..96a58405a764 100644 --- a/packages/SystemUI/shared/res/values/attrs.xml +++ b/packages/SystemUI/shared/res/values/attrs.xml @@ -25,4 +25,39 @@ <attr name="lockScreenWeight" format="integer" /> <attr name="chargeAnimationDelay" format="integer" /> </declare-styleable> + + <declare-styleable name="DoubleShadowAttrDeclare"> + <attr name="keyShadowBlur" format="dimension" /> + <attr name="keyShadowOffsetX" format="dimension" /> + <attr name="keyShadowOffsetY" format="dimension" /> + <attr name="keyShadowAlpha" format="float" /> + <attr name="ambientShadowBlur" format="dimension" /> + <attr name="ambientShadowOffsetX" format="dimension" /> + <attr name="ambientShadowOffsetY" format="dimension" /> + <attr name="ambientShadowAlpha" format="float" /> + </declare-styleable> + + <declare-styleable name="DoubleShadowTextClock"> + <attr name="keyShadowBlur" /> + <attr name="keyShadowOffsetX" /> + <attr name="keyShadowOffsetY" /> + <attr name="keyShadowAlpha" /> + <attr name="ambientShadowBlur" /> + <attr name="ambientShadowOffsetX" /> + <attr name="ambientShadowOffsetY" /> + <attr name="ambientShadowAlpha" /> + </declare-styleable> + + <declare-styleable name="DoubleShadowTextView"> + <attr name="keyShadowBlur" /> + <attr name="keyShadowOffsetX" /> + <attr name="keyShadowOffsetY" /> + <attr name="keyShadowAlpha" /> + <attr name="ambientShadowBlur" /> + <attr name="ambientShadowOffsetX" /> + <attr name="ambientShadowOffsetY" /> + <attr name="ambientShadowAlpha" /> + <attr name="drawableIconSize" format="dimension" /> + <attr name="drawableIconInsetSize" format="dimension" /> + </declare-styleable> </resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt new file mode 100644 index 000000000000..3748eba47be5 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt @@ -0,0 +1,124 @@ +/* + * 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.shared.shadow + +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.PixelFormat +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.RenderEffect +import android.graphics.RenderNode +import android.graphics.Shader +import android.graphics.drawable.Drawable +import android.graphics.drawable.InsetDrawable +import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo + +/** A component to draw an icon with two layers of shadows. */ +class DoubleShadowIconDrawable( + keyShadowInfo: ShadowInfo, + ambientShadowInfo: ShadowInfo, + iconDrawable: Drawable, + iconSize: Int, + val iconInsetSize: Int +) : Drawable() { + private val mAmbientShadowInfo: ShadowInfo + private val mCanvasSize: Int + private val mKeyShadowInfo: ShadowInfo + private val mIconDrawable: InsetDrawable + private val mDoubleShadowNode: RenderNode + + init { + mCanvasSize = iconSize + iconInsetSize * 2 + mKeyShadowInfo = keyShadowInfo + mAmbientShadowInfo = ambientShadowInfo + setBounds(0, 0, mCanvasSize, mCanvasSize) + mIconDrawable = InsetDrawable(iconDrawable, iconInsetSize) + mIconDrawable.setBounds(0, 0, mCanvasSize, mCanvasSize) + mDoubleShadowNode = createShadowRenderNode() + } + + private fun createShadowRenderNode(): RenderNode { + val renderNode = RenderNode("DoubleShadowNode") + renderNode.setPosition(0, 0, mCanvasSize, mCanvasSize) + // Create render effects + val ambientShadow = + createShadowRenderEffect( + mAmbientShadowInfo.blur, + mAmbientShadowInfo.offsetX, + mAmbientShadowInfo.offsetY, + mAmbientShadowInfo.alpha + ) + val keyShadow = + createShadowRenderEffect( + mKeyShadowInfo.blur, + mKeyShadowInfo.offsetX, + mKeyShadowInfo.offsetY, + mKeyShadowInfo.alpha + ) + val blend = RenderEffect.createBlendModeEffect(ambientShadow, keyShadow, BlendMode.DARKEN) + renderNode.setRenderEffect(blend) + return renderNode + } + + private fun createShadowRenderEffect( + radius: Float, + offsetX: Float, + offsetY: Float, + alpha: Float + ): RenderEffect { + return RenderEffect.createColorFilterEffect( + PorterDuffColorFilter(Color.argb(alpha, 0f, 0f, 0f), PorterDuff.Mode.MULTIPLY), + RenderEffect.createOffsetEffect( + offsetX, + offsetY, + RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP) + ) + ) + } + + override fun draw(canvas: Canvas) { + if (canvas.isHardwareAccelerated) { + if (!mDoubleShadowNode.hasDisplayList()) { + // Record render node if its display list is not recorded or discarded + // (which happens when it's no longer drawn by anything). + val recordingCanvas = mDoubleShadowNode.beginRecording() + mIconDrawable.draw(recordingCanvas) + mDoubleShadowNode.endRecording() + } + canvas.drawRenderNode(mDoubleShadowNode) + } + mIconDrawable.draw(canvas) + } + + override fun getOpacity(): Int { + return PixelFormat.TRANSPARENT + } + + override fun setAlpha(alpha: Int) { + mIconDrawable.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + mIconDrawable.colorFilter = colorFilter + } + + override fun setTint(color: Int) { + mIconDrawable.setTint(color) + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextClock.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextClock.kt new file mode 100644 index 000000000000..f2db129120e9 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextClock.kt @@ -0,0 +1,100 @@ +/* + * 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.shared.shadow + +import android.content.Context +import android.graphics.Canvas +import android.util.AttributeSet +import android.widget.TextClock +import com.android.systemui.shared.R +import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo +import com.android.systemui.shared.shadow.DoubleShadowTextHelper.applyShadows + +/** Extension of [TextClock] which draws two shadows on the text (ambient and key shadows) */ +class DoubleShadowTextClock +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : TextClock(context, attrs, defStyleAttr, defStyleRes) { + private val mAmbientShadowInfo: ShadowInfo + private val mKeyShadowInfo: ShadowInfo + + init { + val attributes = + context.obtainStyledAttributes( + attrs, + R.styleable.DoubleShadowTextClock, + defStyleAttr, + defStyleRes + ) + try { + val keyShadowBlur = + attributes.getDimensionPixelSize(R.styleable.DoubleShadowTextClock_keyShadowBlur, 0) + val keyShadowOffsetX = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextClock_keyShadowOffsetX, + 0 + ) + val keyShadowOffsetY = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextClock_keyShadowOffsetY, + 0 + ) + val keyShadowAlpha = + attributes.getFloat(R.styleable.DoubleShadowTextClock_keyShadowAlpha, 0f) + mKeyShadowInfo = + ShadowInfo( + keyShadowBlur.toFloat(), + keyShadowOffsetX.toFloat(), + keyShadowOffsetY.toFloat(), + keyShadowAlpha + ) + val ambientShadowBlur = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextClock_ambientShadowBlur, + 0 + ) + val ambientShadowOffsetX = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextClock_ambientShadowOffsetX, + 0 + ) + val ambientShadowOffsetY = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextClock_ambientShadowOffsetY, + 0 + ) + val ambientShadowAlpha = + attributes.getFloat(R.styleable.DoubleShadowTextClock_ambientShadowAlpha, 0f) + mAmbientShadowInfo = + ShadowInfo( + ambientShadowBlur.toFloat(), + ambientShadowOffsetX.toFloat(), + ambientShadowOffsetY.toFloat(), + ambientShadowAlpha + ) + } finally { + attributes.recycle() + } + } + + public override fun onDraw(canvas: Canvas) { + applyShadows(mKeyShadowInfo, mAmbientShadowInfo, this, canvas) { super.onDraw(canvas) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextHelper.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextHelper.kt index b1dc5a2e5dea..eaac93dc2555 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextHelper.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextHelper.kt @@ -14,31 +14,33 @@ * limitations under the License. */ -package com.android.systemui.dreams.complication +package com.android.systemui.shared.shadow import android.graphics.Canvas +import android.graphics.Color import android.widget.TextView -import androidx.annotation.ColorInt -class DoubleShadowTextHelper -constructor( - private val keyShadowInfo: ShadowInfo, - private val ambientShadowInfo: ShadowInfo, -) { +object DoubleShadowTextHelper { data class ShadowInfo( val blur: Float, val offsetX: Float = 0f, val offsetY: Float = 0f, - @ColorInt val color: Int + val alpha: Float ) - fun applyShadows(view: TextView, canvas: Canvas, onDrawCallback: () -> Unit) { + fun applyShadows( + keyShadowInfo: ShadowInfo, + ambientShadowInfo: ShadowInfo, + view: TextView, + canvas: Canvas, + onDrawCallback: () -> Unit + ) { // We enhance the shadow by drawing the shadow twice view.paint.setShadowLayer( ambientShadowInfo.blur, ambientShadowInfo.offsetX, ambientShadowInfo.offsetY, - ambientShadowInfo.color + Color.argb(ambientShadowInfo.alpha, 0f, 0f, 0f) ) onDrawCallback() canvas.save() @@ -53,7 +55,7 @@ constructor( keyShadowInfo.blur, keyShadowInfo.offsetX, keyShadowInfo.offsetY, - keyShadowInfo.color + Color.argb(keyShadowInfo.alpha, 0f, 0f, 0f) ) onDrawCallback() canvas.restore() diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt new file mode 100644 index 000000000000..25d272185bc0 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt @@ -0,0 +1,127 @@ +/* + * 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.shared.shadow + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.widget.TextView +import com.android.systemui.shared.R +import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo +import com.android.systemui.shared.shadow.DoubleShadowTextHelper.applyShadows + +/** Extension of [TextView] which draws two shadows on the text (ambient and key shadows} */ +class DoubleShadowTextView +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : TextView(context, attrs, defStyleAttr, defStyleRes) { + private val mKeyShadowInfo: ShadowInfo + private val mAmbientShadowInfo: ShadowInfo + + init { + val attributes = + context.obtainStyledAttributes( + attrs, + R.styleable.DoubleShadowTextView, + defStyleAttr, + defStyleRes + ) + val drawableSize: Int + val drawableInsetSize: Int + try { + val keyShadowBlur = + attributes.getDimensionPixelSize(R.styleable.DoubleShadowTextView_keyShadowBlur, 0) + val keyShadowOffsetX = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextView_keyShadowOffsetX, + 0 + ) + val keyShadowOffsetY = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextView_keyShadowOffsetY, + 0 + ) + val keyShadowAlpha = + attributes.getFloat(R.styleable.DoubleShadowTextView_keyShadowAlpha, 0f) + mKeyShadowInfo = + ShadowInfo( + keyShadowBlur.toFloat(), + keyShadowOffsetX.toFloat(), + keyShadowOffsetY.toFloat(), + keyShadowAlpha + ) + val ambientShadowBlur = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextView_ambientShadowBlur, + 0 + ) + val ambientShadowOffsetX = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextView_ambientShadowOffsetX, + 0 + ) + val ambientShadowOffsetY = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextView_ambientShadowOffsetY, + 0 + ) + val ambientShadowAlpha = + attributes.getFloat(R.styleable.DoubleShadowTextView_ambientShadowAlpha, 0f) + mAmbientShadowInfo = + ShadowInfo( + ambientShadowBlur.toFloat(), + ambientShadowOffsetX.toFloat(), + ambientShadowOffsetY.toFloat(), + ambientShadowAlpha + ) + drawableSize = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextView_drawableIconSize, + 0 + ) + drawableInsetSize = + attributes.getDimensionPixelSize( + R.styleable.DoubleShadowTextView_drawableIconInsetSize, + 0 + ) + } finally { + attributes.recycle() + } + + val drawables = arrayOf<Drawable?>(null, null, null, null) + for ((index, drawable) in compoundDrawablesRelative.withIndex()) { + if (drawable == null) continue + drawables[index] = + DoubleShadowIconDrawable( + mKeyShadowInfo, + mAmbientShadowInfo, + drawable, + drawableSize, + drawableInsetSize + ) + } + setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3]) + } + + public override fun onDraw(canvas: Canvas) { + applyShadows(mKeyShadowInfo, mAmbientShadowInfo, this, canvas) { super.onDraw(canvas) } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java index 2cc5ccdc3fa1..1e5c53de4446 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java @@ -24,6 +24,7 @@ import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT; +import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; import android.animation.Animator; @@ -106,6 +107,8 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView { return R.string.kg_prompt_reason_timeout_password; case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: return R.string.kg_prompt_reason_timeout_password; + case PROMPT_REASON_TRUSTAGENT_EXPIRED: + return R.string.kg_prompt_reason_timeout_password; case PROMPT_REASON_NONE: return 0; default: diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 987164557a7a..5b223242670c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -330,6 +330,9 @@ public class KeyguardPatternViewController case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern); break; + case PROMPT_REASON_TRUSTAGENT_EXPIRED: + mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern); + break; case PROMPT_REASON_NONE: break; default: diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index c46e33d9fd53..0a91150e6c39 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -22,6 +22,7 @@ import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT; +import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; import android.animation.Animator; @@ -123,6 +124,8 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView return R.string.kg_prompt_reason_timeout_pin; case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: return R.string.kg_prompt_reason_timeout_pin; + case PROMPT_REASON_TRUSTAGENT_EXPIRED: + return R.string.kg_prompt_reason_timeout_pin; case PROMPT_REASON_NONE: return 0; default: diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java index ac00e9453c97..9d0a8acf02b4 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java @@ -61,6 +61,12 @@ public interface KeyguardSecurityView { int PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT = 7; /** + * Some auth is required because the trustagent expired either from timeout or manually by + * the user + */ + int PROMPT_REASON_TRUSTAGENT_EXPIRED = 8; + + /** * Reset the view and prepare to take input. This should do things like clearing the * password or pattern and clear error messages. */ diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 7fc81231c90b..a5fdc68226e8 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -103,7 +103,6 @@ import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.policy.AccessibilityController; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; -import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -253,7 +252,6 @@ public class Dependency { @Inject Lazy<UserInfoController> mUserInfoController; @Inject Lazy<KeyguardStateController> mKeyguardMonitor; @Inject Lazy<KeyguardUpdateMonitor> mKeyguardUpdateMonitor; - @Inject Lazy<BatteryController> mBatteryController; @Inject Lazy<NightDisplayListener> mNightDisplayListener; @Inject Lazy<ReduceBrightColorsController> mReduceBrightColorsController; @Inject Lazy<ManagedProfileController> mManagedProfileController; @@ -404,8 +402,6 @@ public class Dependency { mProviders.put(UserInfoController.class, mUserInfoController::get); - mProviders.put(BatteryController.class, mBatteryController::get); - mProviders.put(NightDisplayListener.class, mNightDisplayListener::get); mProviders.put(ReduceBrightColorsController.class, mReduceBrightColorsController::get); diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 2e13903814a5..67b683ec643a 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -455,7 +455,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } } - boolean needToUpdateProviderViews = false; final String newUniqueId = mDisplayInfo.uniqueId; if (!Objects.equals(newUniqueId, mDisplayUniqueId)) { mDisplayUniqueId = newUniqueId; @@ -473,37 +472,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab setupDecorations(); return; } - - if (mScreenDecorHwcLayer != null) { - updateHwLayerRoundedCornerDrawable(); - updateHwLayerRoundedCornerExistAndSize(); - } - needToUpdateProviderViews = true; - } - - final float newRatio = getPhysicalPixelDisplaySizeRatio(); - if (mRoundedCornerResDelegate.getPhysicalPixelDisplaySizeRatio() != newRatio) { - mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(newRatio); - if (mScreenDecorHwcLayer != null) { - updateHwLayerRoundedCornerExistAndSize(); - } - needToUpdateProviderViews = true; - } - - if (needToUpdateProviderViews) { - updateOverlayProviderViews(null); - } else { - updateOverlayProviderViews(new Integer[] { - mFaceScanningViewId, - R.id.display_cutout, - R.id.display_cutout_left, - R.id.display_cutout_right, - R.id.display_cutout_bottom, - }); - } - - if (mScreenDecorHwcLayer != null) { - mScreenDecorHwcLayer.onDisplayChanged(newUniqueId); } } }; @@ -1069,6 +1037,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod))) { mRotation = newRotation; mDisplayMode = newMod; + mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio( + getPhysicalPixelDisplaySizeRatio()); if (mScreenDecorHwcLayer != null) { mScreenDecorHwcLayer.pendingConfigChange = false; mScreenDecorHwcLayer.updateRotation(mRotation); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 5610377367c4..1ceb6b3ca172 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -187,7 +187,10 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba public void onReceive(Context context, Intent intent) { if (mCurrentDialog != null && Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { - Log.w(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received"); + String reason = intent.getStringExtra("reason"); + reason = (reason != null) ? reason : "unknown"; + Log.d(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received, reason: " + reason); + mCurrentDialog.dismissWithoutCallback(true /* animate */); mCurrentDialog = null; @@ -370,18 +373,14 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba @Override public void onTryAgainPressed(long requestId) { - if (mReceiver == null) { - Log.e(TAG, "onTryAgainPressed: Receiver is null"); - return; - } - - if (requestId != mCurrentDialog.getRequestId()) { - Log.w(TAG, "requestId doesn't match, skip onTryAgainPressed"); + final IBiometricSysuiReceiver receiver = getCurrentReceiver(requestId); + if (receiver == null) { + Log.w(TAG, "Skip onTryAgainPressed"); return; } try { - mReceiver.onTryAgainPressed(); + receiver.onTryAgainPressed(); } catch (RemoteException e) { Log.e(TAG, "RemoteException when handling try again", e); } @@ -389,18 +388,14 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba @Override public void onDeviceCredentialPressed(long requestId) { - if (mReceiver == null) { - Log.e(TAG, "onDeviceCredentialPressed: Receiver is null"); - return; - } - - if (requestId != mCurrentDialog.getRequestId()) { - Log.w(TAG, "requestId doesn't match, skip onDeviceCredentialPressed"); + final IBiometricSysuiReceiver receiver = getCurrentReceiver(requestId); + if (receiver == null) { + Log.w(TAG, "Skip onDeviceCredentialPressed"); return; } try { - mReceiver.onDeviceCredentialPressed(); + receiver.onDeviceCredentialPressed(); } catch (RemoteException e) { Log.e(TAG, "RemoteException when handling credential button", e); } @@ -408,18 +403,14 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba @Override public void onSystemEvent(int event, long requestId) { - if (mReceiver == null) { - Log.e(TAG, "onSystemEvent(" + event + "): Receiver is null"); - return; - } - - if (requestId != mCurrentDialog.getRequestId()) { - Log.w(TAG, "requestId doesn't match, skip onSystemEvent"); + final IBiometricSysuiReceiver receiver = getCurrentReceiver(requestId); + if (receiver == null) { + Log.w(TAG, "Skip onSystemEvent"); return; } try { - mReceiver.onSystemEvent(event); + receiver.onSystemEvent(event); } catch (RemoteException e) { Log.e(TAG, "RemoteException when sending system event", e); } @@ -427,23 +418,46 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba @Override public void onDialogAnimatedIn(long requestId) { - if (mReceiver == null) { - Log.e(TAG, "onDialogAnimatedIn: Receiver is null"); - return; - } - - if (requestId != mCurrentDialog.getRequestId()) { - Log.w(TAG, "requestId doesn't match, skip onDialogAnimatedIn"); + final IBiometricSysuiReceiver receiver = getCurrentReceiver(requestId); + if (receiver == null) { + Log.w(TAG, "Skip onDialogAnimatedIn"); return; } try { - mReceiver.onDialogAnimatedIn(); + receiver.onDialogAnimatedIn(); } catch (RemoteException e) { Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e); } } + @Nullable + private IBiometricSysuiReceiver getCurrentReceiver(long requestId) { + if (!isRequestIdValid(requestId)) { + return null; + } + + if (mReceiver == null) { + Log.w(TAG, "getCurrentReceiver: Receiver is null"); + } + + return mReceiver; + } + + private boolean isRequestIdValid(long requestId) { + if (mCurrentDialog == null) { + Log.w(TAG, "shouldNotifyReceiver: dialog already gone"); + return false; + } + + if (requestId != mCurrentDialog.getRequestId()) { + Log.w(TAG, "shouldNotifyReceiver: requestId doesn't match"); + return false; + } + + return true; + } + @Override public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation, long requestId) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 36287f59d746..27e9af9b394a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -342,8 +342,11 @@ public class UdfpsController implements DozeReceiver { if (mOverlay != null && mOverlay.getRequestReason() != REASON_AUTH_KEYGUARD && Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { - Log.d(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received, mRequestReason: " - + mOverlay.getRequestReason()); + String reason = intent.getStringExtra("reason"); + reason = (reason != null) ? reason : "unknown"; + Log.d(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received, reason: " + reason + + ", mRequestReason: " + mOverlay.getRequestReason()); + mOverlay.cancel(); hideUdfpsOverlay(); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index fbfc94af683e..a99669970a34 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -35,6 +35,7 @@ import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; +import com.android.systemui.dump.DumpManager; import com.android.systemui.media.dagger.MediaModule; import com.android.systemui.navigationbar.gestural.GestureModule; import com.android.systemui.plugins.qs.QSFactory; @@ -126,6 +127,7 @@ public abstract class ReferenceSystemUIModule { PowerManager powerManager, BroadcastDispatcher broadcastDispatcher, DemoModeController demoModeController, + DumpManager dumpManager, @Main Handler mainHandler, @Background Handler bgHandler) { BatteryController bC = new BatteryControllerImpl( @@ -134,6 +136,7 @@ public abstract class ReferenceSystemUIModule { powerManager, broadcastDispatcher, demoModeController, + dumpManager, mainHandler, bgHandler); bC.init(); diff --git a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt index a25286438387..8b4aeefb6ed4 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt @@ -78,23 +78,18 @@ class RoundedCornerResDelegate( reloadMeasures() } - private fun reloadAll(newReloadToken: Int) { - if (reloadToken == newReloadToken) { - return - } - reloadToken = newReloadToken - reloadRes() - reloadMeasures() - } - fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) { if (displayUniqueId != newDisplayUniqueId) { displayUniqueId = newDisplayUniqueId newReloadToken ?.let { reloadToken = it } reloadRes() reloadMeasures() - } else { - newReloadToken?.let { reloadAll(it) } + } else if (newReloadToken != null) { + if (reloadToken == newReloadToken) { + return + } + reloadToken = newReloadToken + reloadMeasures() } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextClock.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextClock.java deleted file mode 100644 index 789ebc517271..000000000000 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextClock.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.dreams.complication; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.widget.TextClock; - -import com.android.systemui.R; -import com.android.systemui.dreams.complication.DoubleShadowTextHelper.ShadowInfo; - -import kotlin.Unit; - -/** - * Extension of {@link TextClock} which draws two shadows on the text (ambient and key shadows) - */ -public class DoubleShadowTextClock extends TextClock { - private final DoubleShadowTextHelper mShadowHelper; - - public DoubleShadowTextClock(Context context) { - this(context, null); - } - - public DoubleShadowTextClock(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public DoubleShadowTextClock(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - final Resources resources = context.getResources(); - final ShadowInfo keyShadowInfo = new ShadowInfo( - resources.getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_radius), - resources.getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_dx), - resources.getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_dy), - resources.getColor(R.color.dream_overlay_clock_key_text_shadow_color)); - - final ShadowInfo ambientShadowInfo = new ShadowInfo( - resources.getDimensionPixelSize( - R.dimen.dream_overlay_clock_ambient_text_shadow_radius), - resources.getDimensionPixelSize(R.dimen.dream_overlay_clock_ambient_text_shadow_dx), - resources.getDimensionPixelSize(R.dimen.dream_overlay_clock_ambient_text_shadow_dy), - resources.getColor(R.color.dream_overlay_clock_ambient_text_shadow_color)); - mShadowHelper = new DoubleShadowTextHelper(keyShadowInfo, ambientShadowInfo); - } - - @Override - public void onDraw(Canvas canvas) { - mShadowHelper.applyShadows(this, canvas, () -> { - super.onDraw(canvas); - return Unit.INSTANCE; - }); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextView.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextView.java deleted file mode 100644 index cf7e3127dedf..000000000000 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextView.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.dreams.complication; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.widget.TextView; - -import com.android.systemui.R; - -import kotlin.Unit; - -/** - * Extension of {@link TextView} which draws two shadows on the text (ambient and key shadows} - */ -public class DoubleShadowTextView extends TextView { - private final DoubleShadowTextHelper mShadowHelper; - - public DoubleShadowTextView(Context context) { - this(context, null); - } - - public DoubleShadowTextView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public DoubleShadowTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - final Resources resources = context.getResources(); - final DoubleShadowTextHelper.ShadowInfo - keyShadowInfo = new DoubleShadowTextHelper.ShadowInfo( - resources.getDimensionPixelSize( - R.dimen.dream_overlay_status_bar_key_text_shadow_radius), - resources.getDimensionPixelSize( - R.dimen.dream_overlay_status_bar_key_text_shadow_dx), - resources.getDimensionPixelSize( - R.dimen.dream_overlay_status_bar_key_text_shadow_dy), - resources.getColor(R.color.dream_overlay_status_bar_key_text_shadow_color)); - - final DoubleShadowTextHelper.ShadowInfo - ambientShadowInfo = new DoubleShadowTextHelper.ShadowInfo( - resources.getDimensionPixelSize( - R.dimen.dream_overlay_status_bar_ambient_text_shadow_radius), - resources.getDimensionPixelSize( - R.dimen.dream_overlay_status_bar_ambient_text_shadow_dx), - resources.getDimensionPixelSize( - R.dimen.dream_overlay_status_bar_ambient_text_shadow_dy), - resources.getColor(R.color.dream_overlay_status_bar_ambient_text_shadow_color)); - mShadowHelper = new DoubleShadowTextHelper(keyShadowInfo, ambientShadowInfo); - } - - @Override - public void onDraw(Canvas canvas) { - mShadowHelper.applyShadows(this, canvas, () -> { - super.onDraw(canvas); - return Unit.INSTANCE; - }); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 26db3ee4926f..8c4d17d072e2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -24,6 +24,7 @@ import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NAV_BAR_HANDLE_SHOW_OVER_LOCKSCREEN; import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD; import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_UNLOCK_ANIMATION; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT; @@ -802,6 +803,9 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } else if (trustAgentsEnabled && (strongAuth & SOME_AUTH_REQUIRED_AFTER_USER_REQUEST) != 0) { return KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; + } else if (trustAgentsEnabled + && (strongAuth & SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED) != 0) { + return KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; } else if (any && ((strongAuth & STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) != 0 || mUpdateMonitor.isFingerprintLockedOut())) { return KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 5612c22311fb..29e2c1cd8900 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -76,6 +76,14 @@ public class LogModule { return factory.create("NotifInterruptLog", 100); } + /** Provides a logging buffer for notification rendering events. */ + @Provides + @SysUISingleton + @NotificationRenderLog + public static LogBuffer provideNotificationRenderLogBuffer(LogBufferFactory factory) { + return factory.create("NotifRenderLog", 100); + } + /** Provides a logging buffer for all logs for lockscreen to shade transition events. */ @Provides @SysUISingleton diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/NonResizeableActivity.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java index 24275e002c7f..8c8753a07339 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/NonResizeableActivity.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,16 +14,20 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.systemui.log.dagger; -import android.app.Activity; -import android.os.Bundle; +import static java.lang.annotation.RetentionPolicy.RUNTIME; -public class NonResizeableActivity extends Activity { +import com.android.systemui.log.LogBuffer; - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - setContentView(R.layout.activity_non_resizeable); - } +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +/** A {@link LogBuffer} for notification rendering logging. */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface NotificationRenderLog { } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 654c15812988..b36f33b87d98 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -202,6 +202,7 @@ class MediaCarouselController @Inject constructor( * It will be called when the container is out of view. */ lateinit var updateUserVisibility: () -> Unit + lateinit var updateHostVisibility: () -> Unit private val isReorderingAllowed: Boolean get() = visualStabilityProvider.isReorderingAllowed @@ -225,7 +226,13 @@ class MediaCarouselController @Inject constructor( reorderAllPlayers(previousVisiblePlayerKey = null) } - keysNeedRemoval.forEach { removePlayer(it) } + keysNeedRemoval.forEach { + removePlayer(it) + } + if (keysNeedRemoval.size > 0) { + // Carousel visibility may need to be updated after late removals + updateHostVisibility() + } keysNeedRemoval.clear() // Update user visibility so that no extra impression will be logged when @@ -247,6 +254,7 @@ class MediaCarouselController @Inject constructor( receivedSmartspaceCardLatency: Int, isSsReactivated: Boolean ) { + debugLogger.logMediaLoaded(key) if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) { // Log card received if a new resumable media card is added MediaPlayerData.getMediaPlayer(key)?.let { @@ -315,7 +323,7 @@ class MediaCarouselController @Inject constructor( data: SmartspaceMediaData, shouldPrioritize: Boolean ) { - if (DEBUG) Log.d(TAG, "Loading Smartspace media update") + debugLogger.logRecommendationLoaded(key) // Log the case where the hidden media carousel with the existed inactive resume // media is shown by the Smartspace signal. if (data.isActive) { @@ -370,13 +378,21 @@ class MediaCarouselController @Inject constructor( } override fun onMediaDataRemoved(key: String) { + debugLogger.logMediaRemoved(key) removePlayer(key) } override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { - if (DEBUG) Log.d(TAG, "My Smartspace media removal request is received") + debugLogger.logRecommendationRemoved(key, immediately) if (immediately || isReorderingAllowed) { - onMediaDataRemoved(key) + removePlayer(key) + if (!immediately) { + // Although it wasn't requested, we were able to process the removal + // immediately since reordering is allowed. So, notify hosts to update + if (this@MediaCarouselController::updateHostVisibility.isInitialized) { + updateHostVisibility() + } + } } else { keysNeedRemoval.add(key) } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt index 04ebd5a71137..b1018f9544c0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt @@ -40,6 +40,37 @@ class MediaCarouselControllerLogger @Inject constructor( "Removing control panel for $str1 from map without calling #onDestroy" } ) + + fun logMediaLoaded(key: String) = buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "add player $str1" } + ) + + fun logMediaRemoved(key: String) = buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "removing player $str1" } + ) + + fun logRecommendationLoaded(key: String) = buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "add recommendation $str1" } + ) + + fun logRecommendationRemoved(key: String, immediately: Boolean) = buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = key + bool1 = immediately + }, + { "removing recommendation $str1, immediate=$bool1" } + ) } private const val TAG = "MediaCarouselCtlrLog" diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index c48271e0348a..896fb4765c57 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -1322,6 +1322,7 @@ class MediaDataManager( println("externalListeners: ${mediaDataFilter.listeners}") println("mediaEntries: $mediaEntries") println("useMediaResumption: $useMediaResumption") + println("allowMediaRecommendations: $allowMediaRecommendations") } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 6baf6e137ab4..e0b6d1f9de7b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -546,6 +546,11 @@ class MediaHierarchyManager @Inject constructor( mediaCarouselController.updateUserVisibility = { mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser() } + mediaCarouselController.updateHostVisibility = { + mediaHosts.forEach { + it?.updateViewVisibility() + } + } panelEventsEvents.registerListener(object : NotifPanelEvents.Listener { override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index de2b5c9a4739..864592238b73 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -167,7 +167,11 @@ class MediaHost constructor( } } - private fun updateViewVisibility() { + /** + * Updates this host's state based on the current media data's status, and invokes listeners if + * the visibility has changed + */ + fun updateViewVisibility() { state.visible = if (showsOnlyActiveMedia) { mediaDataManager.hasActiveMediaOrRecommendation() } else { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt index 8e4ca5a98778..731e348edca4 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt @@ -380,7 +380,7 @@ class MediaViewController @Inject constructor( type: TYPE ) = traceSection("MediaViewController#attach") { updateMediaViewControllerType(type) - logger.logMediaLocation("attach", currentStartLocation, currentEndLocation) + logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation) this.transitionLayout = transitionLayout layoutController.attach(transitionLayout) if (currentEndLocation == -1) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 7c4c64c20089..d605c1a42ec0 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -955,9 +955,9 @@ public class EdgeBackGestureHandler extends CurrentUserTracker updateDisabledForQuickstep(newConfig); } - if (DEBUG_MISSING_GESTURE) { - Log.d(DEBUG_MISSING_GESTURE_TAG, "Config changed: config=" + newConfig); - } + // TODO(b/243765256): Disable this logging once b/243765256 is fixed. + Log.d(DEBUG_MISSING_GESTURE_TAG, "Config changed: newConfig=" + newConfig + + " lastReportedConfig=" + mLastReportedConfig); mLastReportedConfig.updateFrom(newConfig); updateDisplaySize(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java index b6f6e933bf84..624def60276b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java @@ -51,8 +51,6 @@ import javax.inject.Inject; /** Quick settings tile: Hotspot **/ public class HotspotTile extends QSTileImpl<BooleanState> { - private final Icon mEnabledStatic = ResourceIcon.get(R.drawable.ic_hotspot); - private final HotspotController mHotspotController; private final DataSaverController mDataSaverController; @@ -129,9 +127,6 @@ public class HotspotTile extends QSTileImpl<BooleanState> { @Override protected void handleUpdateState(BooleanState state, Object arg) { final boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING; - if (state.slash == null) { - state.slash = new SlashState(); - } final int numConnectedDevices; final boolean isTransient = transientEnabling || mHotspotController.isHotspotTransient(); @@ -150,13 +145,14 @@ public class HotspotTile extends QSTileImpl<BooleanState> { isDataSaverEnabled = mDataSaverController.isDataSaverEnabled(); } - state.icon = mEnabledStatic; state.label = mContext.getString(R.string.quick_settings_hotspot_label); state.isTransient = isTransient; - state.slash.isSlashed = !state.value && !state.isTransient; if (state.isTransient) { state.icon = ResourceIcon.get( - com.android.internal.R.drawable.ic_hotspot_transient_animation); + R.drawable.qs_hotspot_icon_search); + } else { + state.icon = ResourceIcon.get(state.value + ? R.drawable.qs_hotspot_icon_on : R.drawable.qs_hotspot_icon_off); } state.expandedAccessibilityClassName = Switch.class.getName(); state.contentDescription = state.label; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java index f60e0661a235..92f6690a13e7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java @@ -81,8 +81,7 @@ public class UiModeNightTile extends QSTileImpl<QSTile.BooleanState> implements super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); mBatteryController = batteryController; - mUiModeManager = (UiModeManager) host.getUserContext().getSystemService( - Context.UI_MODE_SERVICE); + mUiModeManager = host.getUserContext().getSystemService(UiModeManager.class); mLocationController = locationController; configurationController.observe(getLifecycle(), this); batteryController.observe(getLifecycle(), this); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 1011a6d831e6..d7e86b6e2919 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -2230,7 +2230,8 @@ public final class NotificationPanelViewController extends PanelViewController { if (cancel) { collapse(false /* delayed */, 1.0f /* speedUpFactor */); } else { - maybeVibrateOnOpening(); + // Window never will receive touch events that typically trigger haptic on open. + maybeVibrateOnOpening(false /* openingWithTouch */); fling(velocity > 1f ? 1000f * velocity : 0, true /* expand */); } onTrackingStopped(false); diff --git a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java index c3f1e571ab87..b4ce95c434fc 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java @@ -96,6 +96,7 @@ public abstract class PanelViewController { private float mMinExpandHeight; private boolean mPanelUpdateWhenAnimatorEnds; private final boolean mVibrateOnOpening; + private boolean mHasVibratedOnOpen = false; protected boolean mIsLaunchAnimationRunning; private int mFixedDuration = NO_FIXED_DURATION; protected float mOverExpansion; @@ -353,8 +354,8 @@ public abstract class PanelViewController { private void startOpening(MotionEvent event) { updatePanelExpansionAndVisibility(); - maybeVibrateOnOpening(); - + // Reset at start so haptic can be triggered as soon as panel starts to open. + mHasVibratedOnOpen = false; //TODO: keyguard opens QS a different way; log that too? // Log the position of the swipe that opened the panel @@ -368,9 +369,18 @@ public abstract class PanelViewController { .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND); } - protected void maybeVibrateOnOpening() { + /** + * Maybe vibrate as panel is opened. + * + * @param openingWithTouch Whether the panel is being opened with touch. If the panel is instead + * being opened programmatically (such as by the open panel gesture), we always play haptic. + */ + protected void maybeVibrateOnOpening(boolean openingWithTouch) { if (mVibrateOnOpening) { - mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); + if (!openingWithTouch || !mHasVibratedOnOpen) { + mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); + mHasVibratedOnOpen = true; + } } } @@ -1371,6 +1381,9 @@ public abstract class PanelViewController { break; case MotionEvent.ACTION_MOVE: addMovement(event); + if (!isFullyCollapsed()) { + maybeVibrateOnOpening(true /* openingWithTouch */); + } float h = y - mInitialExpandY; // If the panel was collapsed when touching, we only need to check for the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt new file mode 100644 index 000000000000..fe03b2ad6a32 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt @@ -0,0 +1,72 @@ +/* + * 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.notification.logging + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel.INFO +import com.android.systemui.log.dagger.NotificationRenderLog +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.stack.NotificationSection +import javax.inject.Inject + +/** Handles logging for the {NotificationRoundnessManager}. */ +class NotificationRoundnessLogger +@Inject +constructor(@NotificationRenderLog val buffer: LogBuffer) { + + /** Called when the {NotificationRoundnessManager} updates the corners if the Notifications. */ + fun onCornersUpdated( + view: ExpandableView?, + isFirstInSection: Boolean, + isLastInSection: Boolean, + topChanged: Boolean, + bottomChanged: Boolean + ) { + buffer.log( + TAG_ROUNDNESS, + INFO, + { + str1 = (view as? ExpandableNotificationRow)?.entry?.key + bool1 = isFirstInSection + bool2 = isLastInSection + bool3 = topChanged + bool4 = bottomChanged + }, + { + "onCornersUpdated: " + + "entry=$str1 isFirstInSection=$bool1 isLastInSection=$bool2 " + + "topChanged=$bool3 bottomChanged=$bool4" + } + ) + } + + /** Called when we update the {NotificationRoundnessManager} with new sections. */ + fun onSectionCornersUpdated(sections: Array<NotificationSection?>, anyChanged: Boolean) { + buffer.log( + TAG_ROUNDNESS, + INFO, + { + int1 = sections.size + bool1 = anyChanged + }, + { "onSectionCornersUpdated: sections size=$int1 anyChanged=$bool1" } + ) + } +} + +private const val TAG_ROUNDNESS = "NotifRoundnessLogger" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java index b589d9ae1abf..2015c87aac2b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java @@ -19,12 +19,19 @@ package com.android.systemui.statusbar.notification.stack; import android.content.res.Resources; import android.util.MathUtils; +import androidx.annotation.NonNull; + +import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; +import java.io.PrintWriter; import java.util.HashSet; import javax.inject.Inject; @@ -33,12 +40,16 @@ import javax.inject.Inject; * A class that manages the roundness for notification views */ @SysUISingleton -public class NotificationRoundnessManager { +public class NotificationRoundnessManager implements Dumpable { + + private static final String TAG = "NotificationRoundnessManager"; private final ExpandableView[] mFirstInSectionViews; private final ExpandableView[] mLastInSectionViews; private final ExpandableView[] mTmpFirstInSectionViews; private final ExpandableView[] mTmpLastInSectionViews; + private final NotificationRoundnessLogger mNotifLogger; + private final DumpManager mDumpManager; private boolean mExpanded; private HashSet<ExpandableView> mAnimatedChildren; private Runnable mRoundingChangedCallback; @@ -53,12 +64,31 @@ public class NotificationRoundnessManager { @Inject NotificationRoundnessManager( - NotificationSectionsFeatureManager sectionsFeatureManager) { + NotificationSectionsFeatureManager sectionsFeatureManager, + NotificationRoundnessLogger notifLogger, + DumpManager dumpManager) { int numberOfSections = sectionsFeatureManager.getNumberOfBuckets(); mFirstInSectionViews = new ExpandableView[numberOfSections]; mLastInSectionViews = new ExpandableView[numberOfSections]; mTmpFirstInSectionViews = new ExpandableView[numberOfSections]; mTmpLastInSectionViews = new ExpandableView[numberOfSections]; + mNotifLogger = notifLogger; + mDumpManager = dumpManager; + + mDumpManager.registerDumpable(TAG, this); + } + + @Override + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + pw.println("mFirstInSectionViews: length=" + mFirstInSectionViews.length); + pw.println(dumpViews(mFirstInSectionViews)); + pw.println("mLastInSectionViews: length=" + mLastInSectionViews.length); + pw.println(dumpViews(mFirstInSectionViews)); + if (mTrackedHeadsUp != null) { + pw.println("trackedHeadsUp=" + mTrackedHeadsUp.getEntry()); + } + pw.println("roundForPulsingViews=" + mRoundForPulsingViews); + pw.println("isClearAllInProgress=" + mIsClearAllInProgress); } public void updateView(ExpandableView view, boolean animate) { @@ -95,6 +125,9 @@ public class NotificationRoundnessManager { view.setFirstInSection(isFirstInSection); view.setLastInSection(isLastInSection); + mNotifLogger.onCornersUpdated(view, isFirstInSection, + isLastInSection, topChanged, bottomChanged); + return (isFirstInSection || isLastInSection) && (topChanged || bottomChanged); } @@ -184,6 +217,7 @@ public class NotificationRoundnessManager { if (isLastInSection(view) && !top) { return 1.0f; } + if (view == mTrackedHeadsUp) { // If we're pushing up on a headsup the appear fraction is < 0 and it needs to still be // rounded. @@ -220,6 +254,8 @@ public class NotificationRoundnessManager { if (anyChanged) { mRoundingChangedCallback.run(); } + + mNotifLogger.onSectionCornersUpdated(sections, anyChanged); } private boolean handleRemovedOldViews(NotificationSection[] sections, @@ -296,4 +332,36 @@ public class NotificationRoundnessManager { public void setShouldRoundPulsingViews(boolean shouldRoundPulsingViews) { mRoundForPulsingViews = shouldRoundPulsingViews; } + + private String dumpViews(ExpandableView[] views) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < views.length; i++) { + if (views[i] == null) continue; + + sb.append("\t") + .append("[").append(i).append("] ") + .append("isPinned=").append(views[i].isPinned()).append(" ") + .append("isFirstInSection=").append(views[i].isFirstInSection()).append(" ") + .append("isLastInSection=").append(views[i].isLastInSection()).append(" "); + + if (views[i] instanceof ExpandableNotificationRow) { + sb.append("entry="); + dumpEntry(((ExpandableNotificationRow) views[i]).getEntry(), sb); + } + + sb.append("\n"); + } + return sb.toString(); + } + + private void dumpEntry(NotificationEntry entry, StringBuilder sb) { + sb.append("NotificationEntry{key=").append(entry.getKey()).append(" "); + + if (entry.getSection() != null) { + sb.append(" section=") + .append(entry.getSection().getLabel()); + } + + sb.append("}"); + } } 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 753e94015751..149ed0a71b91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java @@ -19,14 +19,17 @@ package com.android.systemui.statusbar.policy; import android.annotation.Nullable; import android.view.View; -import com.android.systemui.Dumpable; import com.android.systemui.demomode.DemoMode; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import java.io.PrintWriter; import java.lang.ref.WeakReference; -public interface BatteryController extends DemoMode, Dumpable, +/** + * Controller for battery related information, including the charge level, power save mode, + * and time remaining information + */ +public interface BatteryController extends DemoMode, CallbackController<BatteryStateChangeCallback> { /** * Prints the current state of the {@link BatteryController} to the given {@link PrintWriter}. 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 33ddf7eed006..c7ad76722929 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -38,11 +38,13 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.fuelgauge.BatterySaverUtils; import com.android.settingslib.fuelgauge.Estimate; import com.android.settingslib.utils.PowerUtil; +import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; +import com.android.systemui.dump.DumpManager; import com.android.systemui.power.EnhancedEstimates; import com.android.systemui.util.Assert; @@ -56,7 +58,8 @@ import java.util.concurrent.atomic.AtomicReference; * Default implementation of a {@link BatteryController}. This controller monitors for battery * level change events that are broadcasted by the system. */ -public class BatteryControllerImpl extends BroadcastReceiver implements BatteryController { +public class BatteryControllerImpl extends BroadcastReceiver implements BatteryController, + Dumpable { private static final String TAG = "BatteryController"; private static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST"; @@ -70,6 +73,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC private final ArrayList<EstimateFetchCompletion> mFetchCallbacks = new ArrayList<>(); private final PowerManager mPowerManager; private final DemoModeController mDemoModeController; + private final DumpManager mDumpManager; private final Handler mMainHandler; private final Handler mBgHandler; protected final Context mContext; @@ -101,6 +105,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC PowerManager powerManager, BroadcastDispatcher broadcastDispatcher, DemoModeController demoModeController, + DumpManager dumpManager, @Main Handler mainHandler, @Background Handler bgHandler) { mContext = context; @@ -110,6 +115,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC mEstimates = enhancedEstimates; mBroadcastDispatcher = broadcastDispatcher; mDemoModeController = demoModeController; + mDumpManager = dumpManager; } private void registerReceiver() { @@ -134,6 +140,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC } } mDemoModeController.addCallback(this); + mDumpManager.registerDumpable(TAG, this); updatePowerSave(); updateEstimateInBackground(); } diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java index 27746c024dad..00ed3d635fa1 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java @@ -36,6 +36,7 @@ import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; +import com.android.systemui.dump.DumpManager; import com.android.systemui.navigationbar.gestural.GestureModule; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -116,9 +117,12 @@ public abstract class TvSystemUIModule { static BatteryController provideBatteryController(Context context, EnhancedEstimates enhancedEstimates, PowerManager powerManager, BroadcastDispatcher broadcastDispatcher, DemoModeController demoModeController, + DumpManager dumpManager, @Main Handler mainHandler, @Background Handler bgHandler) { BatteryController bC = new BatteryControllerImpl(context, enhancedEstimates, powerManager, - broadcastDispatcher, demoModeController, mainHandler, bgHandler); + broadcastDispatcher, demoModeController, + dumpManager, + mainHandler, bgHandler); bC.init(); return bC; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index df10dfe9f160..5a26d05d7b37 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -1005,18 +1005,13 @@ public class ScreenDecorationsTest extends SysuiTestCase { assertEquals(new Size(3, 3), resDelegate.getTopRoundedSize()); assertEquals(new Size(4, 4), resDelegate.getBottomRoundedSize()); - setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, - getTestsDrawable(com.android.systemui.tests.R.drawable.rounded4px) - /* roundedTopDrawable */, - getTestsDrawable(com.android.systemui.tests.R.drawable.rounded5px) - /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* privacyDot */, false /* faceScanning*/); + doReturn(2f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio(); mDisplayInfo.rotation = Surface.ROTATION_270; mScreenDecorations.onConfigurationChanged(null); - assertEquals(new Size(4, 4), resDelegate.getTopRoundedSize()); - assertEquals(new Size(5, 5), resDelegate.getBottomRoundedSize()); + assertEquals(new Size(6, 6), resDelegate.getTopRoundedSize()); + assertEquals(new Size(8, 8), resDelegate.getBottomRoundedSize()); } @Test @@ -1293,51 +1288,6 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test - public void testOnDisplayChanged_hwcLayer() { - setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, - null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */); - final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport(); - decorationSupport.format = PixelFormat.R_8; - doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport(); - - // top cutout - mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP)); - - mScreenDecorations.start(); - - final ScreenDecorHwcLayer hwcLayer = mScreenDecorations.mScreenDecorHwcLayer; - spyOn(hwcLayer); - doReturn(mDisplay).when(hwcLayer).getDisplay(); - - mScreenDecorations.mDisplayListener.onDisplayChanged(1); - - verify(hwcLayer, times(1)).onDisplayChanged(any()); - } - - @Test - public void testOnDisplayChanged_nonHwcLayer() { - setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, - null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */); - - // top cutout - mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP)); - - mScreenDecorations.start(); - - final ScreenDecorations.DisplayCutoutView cutoutView = (ScreenDecorations.DisplayCutoutView) - mScreenDecorations.getOverlayView(R.id.display_cutout); - assertNotNull(cutoutView); - spyOn(cutoutView); - doReturn(mDisplay).when(cutoutView).getDisplay(); - - mScreenDecorations.mDisplayListener.onDisplayChanged(1); - - verify(cutoutView, times(1)).onDisplayChanged(any()); - } - - @Test public void testHasSameProvidersWithNullOverlays() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, diff --git a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt index f93336134900..93a1868b72f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt @@ -24,12 +24,11 @@ import androidx.annotation.DrawableRes import androidx.test.filters.SmallTest import com.android.internal.R as InternalR import com.android.systemui.R as SystemUIR -import com.android.systemui.tests.R import com.android.systemui.SysuiTestCase +import com.android.systemui.tests.R import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test - import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations @@ -102,14 +101,11 @@ class RoundedCornerResDelegateTest : SysuiTestCase() { assertEquals(Size(3, 3), roundedCornerResDelegate.topRoundedSize) assertEquals(Size(4, 4), roundedCornerResDelegate.bottomRoundedSize) - setupResources(radius = 100, - roundedTopDrawable = getTestsDrawable(R.drawable.rounded4px), - roundedBottomDrawable = getTestsDrawable(R.drawable.rounded5px)) - + roundedCornerResDelegate.physicalPixelDisplaySizeRatio = 2f roundedCornerResDelegate.updateDisplayUniqueId(null, 1) - assertEquals(Size(4, 4), roundedCornerResDelegate.topRoundedSize) - assertEquals(Size(5, 5), roundedCornerResDelegate.bottomRoundedSize) + assertEquals(Size(6, 6), roundedCornerResDelegate.topRoundedSize) + assertEquals(Size(8, 8), roundedCornerResDelegate.bottomRoundedSize) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt index 5a50a9f64487..5dd1cfcb76e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.animation.TransitionLayout @@ -78,6 +79,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var mediaViewController: MediaViewController @Mock lateinit var smartspaceMediaData: SmartspaceMediaData @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> + @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener> private val clock = FakeSystemClock() private lateinit var mediaCarouselController: MediaCarouselController @@ -102,6 +104,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { debugLogger ) verify(mediaDataManager).addListener(capture(listener)) + verify(visualStabilityProvider) + .addPersistentReorderingAllowedListener(capture(visualStabilityCallback)) whenever(mediaControlPanelFactory.get()).thenReturn(mediaPlayer) whenever(mediaPlayer.mediaViewController).thenReturn(mediaViewController) whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData) @@ -374,4 +378,28 @@ class MediaCarouselControllerTest : SysuiTestCase() { playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") assertEquals(playerIndex, 0) } + + @Test + fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() { + var result = false + mediaCarouselController.updateHostVisibility = { result = true } + + whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true) + listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) + + assertEquals(true, result) + } + + @Test + fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() { + var result = false + mediaCarouselController.updateHostVisibility = { result = true } + + whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false) + listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) + assertEquals(false, result) + + visualStabilityCallback.value.onReorderingAllowed() + assertEquals(true, result) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java index b86713d0890b..451e9119f297 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java @@ -39,6 +39,7 @@ import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSTileHost; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.HotspotController; @@ -122,4 +123,40 @@ public class HotspotTileTest extends SysuiTestCase { .isEqualTo(mContext.getString(R.string.wifitrackerlib_admin_restricted_network)); mockitoSession.finishMocking(); } + + @Test + public void testIcon_whenDisabled_isOffState() { + QSTile.BooleanState state = new QSTile.BooleanState(); + when(mHotspotController.isHotspotTransient()).thenReturn(false); + when(mHotspotController.isHotspotEnabled()).thenReturn(false); + + mTile.handleUpdateState(state, /* arg= */ null); + + assertThat(state.icon) + .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_hotspot_icon_off)); + } + + @Test + public void testIcon_whenTransient_isSearchState() { + QSTile.BooleanState state = new QSTile.BooleanState(); + when(mHotspotController.isHotspotTransient()).thenReturn(true); + when(mHotspotController.isHotspotEnabled()).thenReturn(true); + + mTile.handleUpdateState(state, /* arg= */ null); + + assertThat(state.icon) + .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_hotspot_icon_search)); + } + + @Test + public void testIcon_whenEnabled_isOnState() { + QSTile.BooleanState state = new QSTile.BooleanState(); + when(mHotspotController.isHotspotTransient()).thenReturn(false); + when(mHotspotController.isHotspotEnabled()).thenReturn(true); + + mTile.handleUpdateState(state, /* arg= */ null); + + assertThat(state.icon) + .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_hotspot_icon_on)); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt index ea70c263a121..0c070da1fcb9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt @@ -21,9 +21,9 @@ import android.content.Context import android.content.res.Configuration import android.content.res.Resources import android.os.Handler -import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.R @@ -51,28 +51,17 @@ import org.mockito.MockitoAnnotations @SmallTest class UiModeNightTileTest : SysuiTestCase() { - @Mock - private lateinit var mockContext: Context - @Mock - private lateinit var uiModeManager: UiModeManager - @Mock - private lateinit var resources: Resources - @Mock - private lateinit var qsLogger: QSLogger - @Mock - private lateinit var qsHost: QSTileHost - @Mock - private lateinit var metricsLogger: MetricsLogger - @Mock - private lateinit var statusBarStateController: StatusBarStateController - @Mock - private lateinit var activityStarter: ActivityStarter - @Mock - private lateinit var configurationController: ConfigurationController - @Mock - private lateinit var batteryController: BatteryController - @Mock - private lateinit var locationController: LocationController + @Mock private lateinit var mockContext: Context + @Mock private lateinit var uiModeManager: UiModeManager + @Mock private lateinit var resources: Resources + @Mock private lateinit var qsLogger: QSLogger + @Mock private lateinit var qsHost: QSTileHost + @Mock private lateinit var metricsLogger: MetricsLogger + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var batteryController: BatteryController + @Mock private lateinit var locationController: LocationController private val uiEventLogger = UiEventLoggerFake() private val falsingManager = FalsingManagerFake() @@ -85,7 +74,7 @@ class UiModeNightTileTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) configuration = Configuration() - mContext.addMockSystemService(Context.UI_MODE_SERVICE, uiModeManager) + mContext.addMockSystemService(UiModeManager::class.java, uiModeManager) `when`(qsHost.context).thenReturn(mockContext) `when`(qsHost.userContext).thenReturn(mContext) @@ -93,7 +82,8 @@ class UiModeNightTileTest : SysuiTestCase() { `when`(resources.configuration).thenReturn(configuration) `when`(qsHost.uiEventLogger).thenReturn(uiEventLogger) - tile = UiModeNightTile( + tile = + UiModeNightTile( qsHost, testableLooper.looper, Handler(testableLooper.looper), @@ -104,7 +94,8 @@ class UiModeNightTileTest : SysuiTestCase() { qsLogger, configurationController, batteryController, - locationController) + locationController + ) } @Test @@ -115,7 +106,7 @@ class UiModeNightTileTest : SysuiTestCase() { tile.handleUpdateState(state, /* arg= */ null) assertThat(state.icon) - .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_light_dark_theme_icon_on)) + .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_light_dark_theme_icon_on)) } @Test @@ -126,7 +117,7 @@ class UiModeNightTileTest : SysuiTestCase() { tile.handleUpdateState(state, /* arg= */ null) assertThat(state.icon) - .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_light_dark_theme_icon_off)) + .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_light_dark_theme_icon_off)) } private fun setNightModeOn() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java index d3c1dc9db218..a95a49c31adf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.stack; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -32,8 +34,10 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; @@ -57,6 +61,7 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { private Runnable mRoundnessCallback = mock(Runnable.class); private ExpandableNotificationRow mFirst; private ExpandableNotificationRow mSecond; + private NotificationRoundnessLogger mLogger = mock(NotificationRoundnessLogger.class); private float mSmallRadiusRatio; @Before @@ -66,7 +71,9 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { mSmallRadiusRatio = resources.getDimension(R.dimen.notification_corner_radius_small) / resources.getDimension(R.dimen.notification_corner_radius); mRoundnessManager = new NotificationRoundnessManager( - new NotificationSectionsFeatureManager(new DeviceConfigProxy(), mContext)); + new NotificationSectionsFeatureManager(new DeviceConfigProxy(), mContext), + mLogger, + mock(DumpManager.class)); allowTestableLooperAsMainThread(); NotificationTestHelper testHelper = new NotificationTestHelper( mContext, @@ -337,6 +344,20 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { Assert.assertTrue(mSecond.isLastInSection()); } + @Test + public void testLoggingOnRoundingUpdate() { + NotificationSection[] sections = new NotificationSection[]{ + createSection(mFirst, mSecond), + createSection(null, null) + }; + mRoundnessManager.updateRoundedChildren(sections); + verify(mLogger).onSectionCornersUpdated(sections, /*anyChanged=*/ true); + verify(mLogger, atLeast(1)).onCornersUpdated(eq(mFirst), anyBoolean(), + anyBoolean(), anyBoolean(), anyBoolean()); + verify(mLogger, atLeast(1)).onCornersUpdated(eq(mSecond), anyBoolean(), + anyBoolean(), anyBoolean(), anyBoolean()); + } + private NotificationSection createSection(ExpandableNotificationRow first, ExpandableNotificationRow last) { NotificationSection section = mock(NotificationSection.class); 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 fda80a2f9ed8..43d0fe9559b6 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 @@ -42,6 +42,7 @@ import com.android.settingslib.fuelgauge.BatterySaverUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.demomode.DemoModeController; +import com.android.systemui.dump.DumpManager; import com.android.systemui.power.EnhancedEstimates; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; @@ -80,6 +81,7 @@ public class BatteryControllerTest extends SysuiTestCase { mPowerManager, mBroadcastDispatcher, mDemoModeController, + mock(DumpManager.class), new Handler(), new Handler()); // Can throw if updateEstimate is called on the main thread diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING index 5c84a628c00d..ae65dcb4d714 100644 --- a/services/core/java/com/android/server/TEST_MAPPING +++ b/services/core/java/com/android/server/TEST_MAPPING @@ -1,6 +1,9 @@ { "presubmit": [ { + "name": "CtsLocationFineTestCases" + }, + { "name": "CtsLocationCoarseTestCases" }, { @@ -66,10 +69,5 @@ ], "file_patterns": ["ClipboardService\\.java"] } - ], - "postsubmit": [ - { - "name": "CtsLocationFineTestCases" - } ] } diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index f26d9f9f49e6..76cac934fdfe 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -24,6 +24,7 @@ import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE; import static android.net.vcn.VcnManager.VCN_STATUS_CODE_INACTIVE; import static android.net.vcn.VcnManager.VCN_STATUS_CODE_NOT_CONFIGURED; import static android.net.vcn.VcnManager.VCN_STATUS_CODE_SAFE_MODE; +import static android.telephony.SubscriptionManager.isValidSubscriptionId; import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionTrackerCallback; @@ -167,6 +168,10 @@ public class VcnManagementService extends IVcnManagementService.Stub { static final String VCN_CONFIG_FILE = new File(Environment.getDataSystemDirectory(), "vcn/configs.xml").getPath(); + // TODO(b/176956496): Directly use CarrierServiceBindHelper.UNBIND_DELAY_MILLIS + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final long CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS = TimeUnit.SECONDS.toMillis(30); + /* Binder context for this service */ @NonNull private final Context mContext; @NonNull private final Dependencies mDeps; @@ -360,15 +365,12 @@ public class VcnManagementService extends IVcnManagementService.Stub { /** Notifies the VcnManagementService that external dependencies can be set up. */ public void systemReady() { - // Always run on the handler thread to ensure consistency. - mHandler.post(() -> { - mNetworkProvider.register(); - mContext.getSystemService(ConnectivityManager.class) - .registerNetworkCallback( - new NetworkRequest.Builder().clearCapabilities().build(), - mTrackingNetworkCallback); - mTelephonySubscriptionTracker.register(); - }); + mNetworkProvider.register(); + mContext.getSystemService(ConnectivityManager.class) + .registerNetworkCallback( + new NetworkRequest.Builder().clearCapabilities().build(), + mTrackingNetworkCallback); + mTelephonySubscriptionTracker.register(); } private void enforcePrimaryUser() { @@ -509,15 +511,22 @@ public class VcnManagementService extends IVcnManagementService.Stub { if (!mVcns.containsKey(subGrp)) { startVcnLocked(subGrp, entry.getValue()); } + + // Cancel any scheduled teardowns for active subscriptions + mHandler.removeCallbacksAndMessages(mVcns.get(subGrp)); } } - // Schedule teardown of any VCN instances that have lost carrier privileges + // Schedule teardown of any VCN instances that have lost carrier privileges (after a + // delay) for (Entry<ParcelUuid, Vcn> entry : mVcns.entrySet()) { final ParcelUuid subGrp = entry.getKey(); final VcnConfig config = mConfigs.get(subGrp); final boolean isActiveSubGrp = isActiveSubGroup(subGrp, snapshot); + final boolean isValidActiveDataSubIdNotInVcnSubGrp = + isValidSubscriptionId(snapshot.getActiveDataSubscriptionId()) + && !isActiveSubGroup(subGrp, snapshot); // TODO(b/193687515): Support multiple VCNs active at the same time if (config == null @@ -527,12 +536,31 @@ public class VcnManagementService extends IVcnManagementService.Stub { final ParcelUuid uuidToTeardown = subGrp; final Vcn instanceToTeardown = entry.getValue(); - stopVcnLocked(uuidToTeardown); - - // TODO(b/181789060): invoke asynchronously after Vcn notifies - // through VcnCallback - notifyAllPermissionedStatusCallbacksLocked( - uuidToTeardown, VCN_STATUS_CODE_INACTIVE); + // TODO(b/193687515): Support multiple VCNs active at the same time + // If directly switching to a subscription not in the current group, + // teardown immediately to prevent other subscription's network from being + // outscored by the VCN. Otherwise, teardown after a delay to ensure that + // SIM profile switches do not trigger the VCN to cycle. + final long teardownDelayMs = + isValidActiveDataSubIdNotInVcnSubGrp + ? 0 + : CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS; + mHandler.postDelayed(() -> { + synchronized (mLock) { + // Guard against case where this is run after a old instance was + // torn down, and a new instance was started. Verify to ensure + // correct instance is torn down. This could happen as a result of a + // Carrier App manually removing/adding a VcnConfig. + if (mVcns.get(uuidToTeardown) == instanceToTeardown) { + stopVcnLocked(uuidToTeardown); + + // TODO(b/181789060): invoke asynchronously after Vcn notifies + // through VcnCallback + notifyAllPermissionedStatusCallbacksLocked( + uuidToTeardown, VCN_STATUS_CODE_INACTIVE); + } + } + }, instanceToTeardown, teardownDelayMs); } else { // If this VCN's status has not changed, update it with the new snapshot entry.getValue().updateSubscriptionSnapshot(mLastSnapshot); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index d84b0e83646c..86918aa8ed7d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -668,10 +668,17 @@ public class ActivityManagerService extends IActivityManager.Stub // we still create this new offload queue, but never ever put anything on it. final boolean mEnableOffloadQueue; - final BroadcastQueue mFgBroadcastQueue; - final BroadcastQueue mBgBroadcastQueue; - final BroadcastQueue mBgOffloadBroadcastQueue; - final BroadcastQueue mFgOffloadBroadcastQueue; + /** + * Flag indicating if we should use {@link BroadcastQueueModernImpl} instead + * of the default {@link BroadcastQueueImpl}. + */ + final boolean mEnableModernQueue; + + static final int BROADCAST_QUEUE_FG = 0; + static final int BROADCAST_QUEUE_BG = 1; + static final int BROADCAST_QUEUE_BG_OFFLOAD = 2; + static final int BROADCAST_QUEUE_FG_OFFLOAD = 3; + // Convenient for easy iteration over the queues. Foreground is first // so that dispatch of foreground broadcasts gets precedence. final BroadcastQueue[] mBroadcastQueues; @@ -693,12 +700,16 @@ public class ActivityManagerService extends IActivityManager.Stub } BroadcastQueue broadcastQueueForFlags(int flags, Object cookie) { + if (mEnableModernQueue) { + return mBroadcastQueues[0]; + } + if (isOnFgOffloadQueue(flags)) { if (DEBUG_BROADCAST_BACKGROUND) { Slog.i(TAG_BROADCAST, "Broadcast intent " + cookie + " on foreground offload queue"); } - return mFgOffloadBroadcastQueue; + return mBroadcastQueues[BROADCAST_QUEUE_FG_OFFLOAD]; } if (isOnBgOffloadQueue(flags)) { @@ -706,14 +717,15 @@ public class ActivityManagerService extends IActivityManager.Stub Slog.i(TAG_BROADCAST, "Broadcast intent " + cookie + " on background offload queue"); } - return mBgOffloadBroadcastQueue; + return mBroadcastQueues[BROADCAST_QUEUE_BG_OFFLOAD]; } final boolean isFg = (flags & Intent.FLAG_RECEIVER_FOREGROUND) != 0; if (DEBUG_BROADCAST_BACKGROUND) Slog.i(TAG_BROADCAST, "Broadcast intent " + cookie + " on " + (isFg ? "foreground" : "background") + " queue"); - return (isFg) ? mFgBroadcastQueue : mBgBroadcastQueue; + return (isFg) ? mBroadcastQueues[BROADCAST_QUEUE_FG] + : mBroadcastQueues[BROADCAST_QUEUE_BG]; } private volatile int mDeviceOwnerUid = INVALID_UID; @@ -2366,9 +2378,8 @@ public class ActivityManagerService extends IActivityManager.Stub mPendingStartActivityUids = new PendingStartActivityUids(); mUseFifoUiScheduling = false; mEnableOffloadQueue = false; + mEnableModernQueue = false; mBroadcastQueues = new BroadcastQueue[0]; - mFgBroadcastQueue = mBgBroadcastQueue = mBgOffloadBroadcastQueue = - mFgOffloadBroadcastQueue = null; mComponentAliasResolver = new ComponentAliasResolver(this); } @@ -2424,20 +2435,23 @@ public class ActivityManagerService extends IActivityManager.Stub mEnableOffloadQueue = SystemProperties.getBoolean( "persist.device_config.activity_manager_native_boot.offload_queue_enabled", true); + mEnableModernQueue = SystemProperties.getBoolean( + "persist.device_config.activity_manager_native_boot.modern_queue_enabled", false); - mBroadcastQueues = new BroadcastQueue[4]; - mFgBroadcastQueue = new BroadcastQueueImpl(this, mHandler, - "foreground", foreConstants, false, ProcessList.SCHED_GROUP_DEFAULT); - mBgBroadcastQueue = new BroadcastQueueImpl(this, mHandler, - "background", backConstants, true, ProcessList.SCHED_GROUP_BACKGROUND); - mBgOffloadBroadcastQueue = new BroadcastQueueImpl(this, mHandler, - "offload_bg", offloadConstants, true, ProcessList.SCHED_GROUP_BACKGROUND); - mFgOffloadBroadcastQueue = new BroadcastQueueImpl(this, mHandler, - "offload_fg", foreConstants, true, ProcessList.SCHED_GROUP_BACKGROUND); - mBroadcastQueues[0] = mFgBroadcastQueue; - mBroadcastQueues[1] = mBgBroadcastQueue; - mBroadcastQueues[2] = mBgOffloadBroadcastQueue; - mBroadcastQueues[3] = mFgOffloadBroadcastQueue; + if (mEnableModernQueue) { + mBroadcastQueues = new BroadcastQueue[1]; + mBroadcastQueues[0] = new BroadcastQueueModernImpl(this, mHandler, foreConstants); + } else { + mBroadcastQueues = new BroadcastQueue[4]; + mBroadcastQueues[BROADCAST_QUEUE_FG] = new BroadcastQueueImpl(this, mHandler, + "foreground", foreConstants, false, ProcessList.SCHED_GROUP_DEFAULT); + mBroadcastQueues[BROADCAST_QUEUE_BG] = new BroadcastQueueImpl(this, mHandler, + "background", backConstants, true, ProcessList.SCHED_GROUP_BACKGROUND); + mBroadcastQueues[BROADCAST_QUEUE_BG_OFFLOAD] = new BroadcastQueueImpl(this, mHandler, + "offload_bg", offloadConstants, true, ProcessList.SCHED_GROUP_BACKGROUND); + mBroadcastQueues[BROADCAST_QUEUE_FG_OFFLOAD] = new BroadcastQueueImpl(this, mHandler, + "offload_fg", foreConstants, true, ProcessList.SCHED_GROUP_BACKGROUND); + } mServices = new ActiveServices(this); mCpHelper = new ContentProviderHelper(this, true); @@ -8913,6 +8927,7 @@ public class ActivityManagerService extends IActivityManager.Stub * @param incrementalMetrics metrics for apps installed on Incremental. * @param errorId a unique id to append to the dropbox headers. */ + @SuppressWarnings("DoNotCall") // Ignore warning for synchronous to call to worker.run() public void addErrorToDropBox(String eventType, ProcessRecord process, String processName, String activityShortComponentName, String parentShortComponentName, ProcessRecord parentProcess, diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java index 3299ee3f65d9..ceff67e168ab 100644 --- a/services/core/java/com/android/server/am/AppProfiler.java +++ b/services/core/java/com/android/server/am/AppProfiler.java @@ -44,6 +44,7 @@ import static com.android.server.am.ActivityManagerService.appendMemInfo; import static com.android.server.am.ActivityManagerService.getKsmInfo; import static com.android.server.am.ActivityManagerService.stringifyKBSize; import static com.android.server.am.LowMemDetector.ADJ_MEM_FACTOR_NOTHING; +import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_NONE; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH; import static com.android.server.wm.ActivityTaskManagerService.DUMP_ACTIVITIES_CMD; @@ -1047,17 +1048,7 @@ public class AppProfiler { } trimMemoryUiHiddenIfNecessaryLSP(app); if (curProcState >= ActivityManager.PROCESS_STATE_HOME && !app.isKilledByAm()) { - if (trimMemoryLevel < curLevel[0] && (thread = app.getThread()) != null) { - try { - if (DEBUG_SWITCH || DEBUG_OOM_ADJ) { - Slog.v(TAG_OOM_ADJ, - "Trimming memory of " + app.processName - + " to " + curLevel[0]); - } - thread.scheduleTrimMemory(curLevel[0]); - } catch (RemoteException e) { - } - } + scheduleTrimMemoryLSP(app, curLevel[0], "Trimming memory of "); profile.setTrimMemoryLevel(curLevel[0]); step[0]++; if (step[0] >= actualFactor) { @@ -1073,31 +1064,11 @@ public class AppProfiler { } } else if (curProcState == ActivityManager.PROCESS_STATE_HEAVY_WEIGHT && !app.isKilledByAm()) { - if (trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND - && (thread = app.getThread()) != null) { - try { - if (DEBUG_SWITCH || DEBUG_OOM_ADJ) { - Slog.v(TAG_OOM_ADJ, - "Trimming memory of heavy-weight " + app.processName - + " to " + ComponentCallbacks2.TRIM_MEMORY_BACKGROUND); - } - thread.scheduleTrimMemory( - ComponentCallbacks2.TRIM_MEMORY_BACKGROUND); - } catch (RemoteException e) { - } - } + scheduleTrimMemoryLSP(app, ComponentCallbacks2.TRIM_MEMORY_BACKGROUND, + "Trimming memory of heavy-weight "); profile.setTrimMemoryLevel(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND); } else { - if (trimMemoryLevel < fgTrimLevel && (thread = app.getThread()) != null) { - try { - if (DEBUG_SWITCH || DEBUG_OOM_ADJ) { - Slog.v(TAG_OOM_ADJ, "Trimming memory of fg " + app.processName - + " to " + fgTrimLevel); - } - thread.scheduleTrimMemory(fgTrimLevel); - } catch (RemoteException e) { - } - } + scheduleTrimMemoryLSP(app, fgTrimLevel, "Trimming memory of fg "); profile.setTrimMemoryLevel(fgTrimLevel); } }); @@ -1128,19 +1099,25 @@ public class AppProfiler { // If this application is now in the background and it // had done UI, then give it the special trim level to // have it free UI resources. - final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN; - IApplicationThread thread; - if (app.mProfile.getTrimMemoryLevel() < level && (thread = app.getThread()) != null) { - try { - if (DEBUG_SWITCH || DEBUG_OOM_ADJ) { - Slog.v(TAG_OOM_ADJ, "Trimming memory of bg-ui " - + app.processName + " to " + level); - } - thread.scheduleTrimMemory(level); - } catch (RemoteException e) { + scheduleTrimMemoryLSP(app, ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN, + "Trimming memory of bg-ui "); + app.mProfile.setPendingUiClean(false); + } + } + + @GuardedBy({"mService", "mProcLock"}) + private void scheduleTrimMemoryLSP(ProcessRecord app, int level, String msg) { + IApplicationThread thread; + if (app.mProfile.getTrimMemoryLevel() < level && (thread = app.getThread()) != null) { + try { + if (DEBUG_SWITCH || DEBUG_OOM_ADJ) { + Slog.v(TAG_OOM_ADJ, msg + app.processName + " to " + level); } + mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(app, + OOM_ADJ_REASON_NONE); + thread.scheduleTrimMemory(level); + } catch (RemoteException e) { } - app.mProfile.setPendingUiClean(false); } } diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java new file mode 100644 index 000000000000..c18c65eec773 --- /dev/null +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -0,0 +1,298 @@ +/* + * 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.am; + +import static com.android.server.am.BroadcastQueue.checkState; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UptimeMillisLong; +import android.os.UserHandle; +import android.util.IndentingPrintWriter; + +import com.android.internal.os.SomeArgs; + +import java.util.ArrayDeque; + +/** + * Queue of pending {@link BroadcastRecord} entries intended for delivery to a + * specific process. + * <p> + * Each queue has a concept of being "runnable at" a particular time in the + * future, which supports arbitrarily pausing or delaying delivery on a + * per-process basis. + * <p> + * Internally each queue consists of a pending broadcasts which are waiting to + * be dispatched, and a single active broadcast which is currently being + * dispatched. + */ +class BroadcastProcessQueue implements Comparable<BroadcastProcessQueue> { + /** + * Default delay to apply to background broadcasts, giving a chance for + * debouncing of rapidly changing events. + */ + // TODO: shift hard-coded defaults to BroadcastConstants + private static final long DELAY_DEFAULT_MILLIS = 10_000; + + /** + * Default delay to apply to broadcasts targeting cached applications. + */ + // TODO: shift hard-coded defaults to BroadcastConstants + private static final long DELAY_CACHED_MILLIS = 30_000; + + final @NonNull String processName; + final int uid; + + /** + * Linked list connection to another process under this {@link #uid} which + * has a different {@link #processName}. + */ + @Nullable BroadcastProcessQueue next; + + /** + * Currently known details about the target process; typically undefined + * when the process isn't actively running. + */ + @Nullable ProcessRecord app; + + /** + * Ordered collection of broadcasts that are waiting to be dispatched to + * this process, as a pair of {@link BroadcastRecord} and the index into + * {@link BroadcastRecord#receivers} that represents the receiver. + */ + private final ArrayDeque<SomeArgs> mPending = new ArrayDeque<>(); + + /** + * Broadcast actively being dispatched to this process. + */ + private @Nullable BroadcastRecord mActive; + + /** + * Receiver actively being dispatched to in this process. This is an index + * into the {@link BroadcastRecord#receivers} list of {@link #mActive}. + */ + private int mActiveIndex; + + private int mCountForeground; + private int mCountOrdered; + private int mCountAlarm; + + private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE; + private boolean mRunnableAtInvalidated; + + private boolean mProcessCached; + + public BroadcastProcessQueue(@NonNull String processName, int uid) { + this.processName = processName; + this.uid = uid; + } + + /** + * Enqueue the given broadcast to be dispatched to this process at some + * future point in time. The target receiver is indicated by the given index + * into {@link BroadcastRecord#receivers}. + */ + public void enqueueBroadcast(@NonNull BroadcastRecord record, int recordIndex) { + // Detect situations where the incoming broadcast should cause us to + // recalculate when we'll be runnable + if (mPending.isEmpty()) { + invalidateRunnableAt(); + } + if (record.isForeground()) { + mCountForeground++; + invalidateRunnableAt(); + } + if (record.ordered) { + mCountOrdered++; + invalidateRunnableAt(); + } + if (record.alarm) { + mCountAlarm++; + invalidateRunnableAt(); + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = record; + args.argi1 = recordIndex; + mPending.addLast(args); + } + + /** + * Update if this process is in the "cached" state, typically signaling that + * broadcast dispatch should be paused or delayed. + */ + public void setProcessCached(boolean cached) { + if (mProcessCached != cached) { + mProcessCached = cached; + invalidateRunnableAt(); + } + } + + /** + * Return if we know of an actively running "warm" process for this queue. + */ + public boolean isProcessWarm() { + return (app != null) && (app.getThread() != null) && !app.isKilled(); + } + + public int getPreferredSchedulingGroupLocked() { + if (mCountForeground > 0 || mCountOrdered > 0 || mCountAlarm > 0) { + // We have an important broadcast somewhere down the queue, so + // boost priority until we drain them all + return ProcessList.SCHED_GROUP_DEFAULT; + } else if ((mActive != null) + && (mActive.isForeground() || mActive.ordered || mActive.alarm)) { + // We have an important broadcast right now, so boost priority + return ProcessList.SCHED_GROUP_DEFAULT; + } else { + return ProcessList.SCHED_GROUP_BACKGROUND; + } + } + + /** + * Set the currently active broadcast to the next pending broadcast. + */ + public void makeActiveNextPending() { + // TODO: what if the next broadcast isn't runnable yet? + checkState(isRunnable(), "isRunnable"); + final SomeArgs next = mPending.removeFirst(); + mActive = (BroadcastRecord) next.arg1; + mActiveIndex = next.argi1; + next.recycle(); + if (mActive.isForeground()) { + mCountForeground--; + } + if (mActive.ordered) { + mCountOrdered--; + } + if (mActive.alarm) { + mCountAlarm--; + } + invalidateRunnableAt(); + } + + /** + * Set the currently running broadcast to be idle. + */ + public void makeActiveIdle() { + mActive = null; + mActiveIndex = 0; + } + + public void setActiveDeliveryState(int deliveryState) { + checkState(isActive(), "isActive"); + mActive.setDeliveryState(mActiveIndex, deliveryState); + } + + public @NonNull BroadcastRecord getActive() { + checkState(isActive(), "isActive"); + return mActive; + } + + public @NonNull Object getActiveReceiver() { + checkState(isActive(), "isActive"); + return mActive.receivers.get(mActiveIndex); + } + + public boolean isActive() { + return mActive != null; + } + + public boolean isRunnable() { + if (mRunnableAtInvalidated) updateRunnableAt(); + return mRunnableAt != Long.MAX_VALUE; + } + + /** + * Return time at which this process is considered runnable. This is + * typically the time at which the next pending broadcast was first + * enqueued, but it also reflects any pauses or delays that should be + * applied to the process. + * <p> + * Returns {@link Long#MAX_VALUE} when this queue isn't currently runnable, + * typically when the queue is empty or when paused. + */ + public @UptimeMillisLong long getRunnableAt() { + if (mRunnableAtInvalidated) updateRunnableAt(); + return mRunnableAt; + } + + private void invalidateRunnableAt() { + mRunnableAtInvalidated = true; + } + + /** + * Update {@link #getRunnableAt()} if it's currently invalidated. + */ + private void updateRunnableAt() { + final SomeArgs next = mPending.peekFirst(); + if (next != null) { + final long runnableAt = ((BroadcastRecord) next.arg1).enqueueTime; + if (mCountForeground > 0) { + mRunnableAt = runnableAt; + } else if (mCountOrdered > 0) { + mRunnableAt = runnableAt; + } else if (mCountAlarm > 0) { + mRunnableAt = runnableAt; + } else if (mProcessCached) { + mRunnableAt = runnableAt + DELAY_CACHED_MILLIS; + } else { + mRunnableAt = runnableAt + DELAY_DEFAULT_MILLIS; + } + } else { + mRunnableAt = Long.MAX_VALUE; + } + } + + @Override + public int compareTo(BroadcastProcessQueue o) { + if (mRunnableAtInvalidated) updateRunnableAt(); + if (o.mRunnableAtInvalidated) o.updateRunnableAt(); + return Long.compare(mRunnableAt, o.mRunnableAt); + } + + @Override + public String toString() { + return "BroadcastProcessQueue{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + processName + "/" + UserHandle.formatUid(uid) + "}"; + } + + public String toShortString() { + return processName + "/" + UserHandle.formatUid(uid); + } + + public void dumpLocked(@NonNull IndentingPrintWriter pw) { + if ((mActive == null) && mPending.isEmpty()) return; + + pw.println(toShortString()); + pw.increaseIndent(); + if (mActive != null) { + pw.print("🏃 "); + pw.print(mActive.toShortString()); + pw.print(' '); + pw.println(mActive.receivers.get(mActiveIndex)); + } + for (SomeArgs args : mPending) { + final BroadcastRecord r = (BroadcastRecord) args.arg1; + pw.print("\u3000 "); + pw.print(r.toShortString()); + pw.print(' '); + pw.println(r.receivers.get(args.argi1)); + } + pw.decreaseIndent(); + } +} diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java index 6814509922b1..e6c446aa40ad 100644 --- a/services/core/java/com/android/server/am/BroadcastQueue.java +++ b/services/core/java/com/android/server/am/BroadcastQueue.java @@ -22,6 +22,7 @@ import android.content.ContentResolver; import android.content.Intent; import android.os.Bundle; import android.os.Handler; +import android.util.Slog; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; @@ -59,6 +60,16 @@ public abstract class BroadcastQueue { mConstants.startObserving(mHandler, resolver); } + static void checkState(boolean state, String msg) { + if (!state) { + Slog.wtf(TAG, msg, new Throwable()); + } + } + + static void logv(String msg) { + Slog.v(TAG, msg); + } + @Override public String toString() { return mQueueName; @@ -74,6 +85,7 @@ public abstract class BroadcastQueue { * otherwise {@link ProcessList#SCHED_GROUP_UNDEFINED} if this queue * has no opinion. */ + @GuardedBy("mService") public abstract int getPreferredSchedulingGroupLocked(@NonNull ProcessRecord app); /** diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index 8c55b14b7e20..169f857995ce 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -1408,7 +1408,7 @@ public class BroadcastQueueImpl extends BroadcastQueue { info.activityInfo.applicationInfo, true, r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND, new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST, r.curComponent, - r.intent.getAction(), getHostingRecordTriggerType(r)), + r.intent.getAction(), r.getHostingRecordTriggerType()), isActivityCapable ? ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE : ZYGOTE_POLICY_FLAG_EMPTY, (r.intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false); if (r.curApp == null) { @@ -1431,17 +1431,6 @@ public class BroadcastQueueImpl extends BroadcastQueue { mPendingBroadcastRecvIndex = recIdx; } - private String getHostingRecordTriggerType(BroadcastRecord r) { - if (r.alarm) { - return HostingRecord.TRIGGER_TYPE_ALARM; - } else if (r.pushMessage) { - return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE; - } else if (r.pushMessageOverQuota) { - return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA; - } - return HostingRecord.TRIGGER_TYPE_UNKNOWN; - } - @Nullable private String getTargetPackage(BroadcastRecord r) { if (r.intent == null) { diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java new file mode 100644 index 000000000000..b5e7b86e3129 --- /dev/null +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -0,0 +1,649 @@ +/* + * 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.am; + +import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY; +import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE; + +import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST; +import static com.android.server.am.BroadcastRecord.getReceiverProcessName; +import static com.android.server.am.BroadcastRecord.getReceiverUid; +import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_START_RECEIVER; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.IApplicationThread; +import android.app.RemoteServiceException.CannotDeliverBroadcastException; +import android.app.UidObserver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.IndentingPrintWriter; +import android.util.Slog; +import android.util.SparseArray; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +/** + * Alternative {@link BroadcastQueue} implementation which pivots broadcasts to + * be dispatched on a per-process basis. + * <p> + * Each process now has its own broadcast queue represented by a + * {@link BroadcastProcessQueue} instance. Each queue has a concept of being + * "runnable at" a particular time in the future, which supports arbitrarily + * pausing or delaying delivery on a per-process basis. + */ +class BroadcastQueueModernImpl extends BroadcastQueue { + BroadcastQueueModernImpl(ActivityManagerService service, Handler handler, + BroadcastConstants constants) { + this(service, handler, constants, new BroadcastSkipPolicy(service), + new BroadcastHistory()); + } + + BroadcastQueueModernImpl(ActivityManagerService service, Handler handler, + BroadcastConstants constants, BroadcastSkipPolicy skipPolicy, + BroadcastHistory history) { + super(service, handler, "modern", constants, skipPolicy, history); + mLocalHandler = new Handler(handler.getLooper(), mLocalCallback); + } + + // TODO: add support for ordered broadcasts + // TODO: add support for replacing pending broadcasts + // TODO: add support for merging pending broadcasts + + // TODO: add trace points for debugging broadcast flows + // TODO: record broadcast state change timing statistics + // TODO: record historical broadcast statistics + + // TODO: pause queues for apps involved in backup/restore + // TODO: pause queues when background services are running + // TODO: pause queues when processes are frozen + + // TODO: clean up queues for removed apps + + /** + * Maximum number of process queues to dispatch broadcasts to + * simultaneously. + */ + // TODO: shift hard-coded defaults to BroadcastConstants + private static final int MAX_RUNNING_PROCESS_QUEUES = 4; + + /** + * Map from UID to per-process broadcast queues. If a UID hosts more than + * one process, each additional process is stored as a linked list using + * {@link BroadcastProcessQueue#next}. + * + * @see #getProcessQueue + * @see #getOrCreateProcessQueue + */ + @GuardedBy("mService") + private final SparseArray<BroadcastProcessQueue> mProcessQueues = new SparseArray<>(); + + /** + * Collection of queues which are "runnable". They're sorted by + * {@link BroadcastProcessQueue#getRunnableAt()} so that we prefer + * dispatching of longer-waiting broadcasts first. + */ + @GuardedBy("mService") + private final ArrayList<BroadcastProcessQueue> mRunnable = new ArrayList<>(); + + /** + * Collection of queues which are "running". This will never be larger than + * {@link #MAX_RUNNING_PROCESS_QUEUES}. + */ + @GuardedBy("mService") + private final ArrayList<BroadcastProcessQueue> mRunning = new ArrayList<>(); + + /** + * Single queue which is "running" but is awaiting a cold start to be + * completed via {@link #onApplicationAttachedLocked}. To optimize for + * system health we only request one cold start at a time. + */ + @GuardedBy("mService") + private @Nullable BroadcastProcessQueue mRunningColdStart; + + /** + * Collection of latches waiting for queue to go idle. + */ + @GuardedBy("mService") + private final ArrayList<CountDownLatch> mWaitingForIdle = new ArrayList<>(); + + private static final int MSG_UPDATE_RUNNING_LIST = 1; + + private void enqueueUpdateRunningList() { + mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST); + mLocalHandler.sendEmptyMessage(MSG_UPDATE_RUNNING_LIST); + } + + private final Handler mLocalHandler; + + private final Handler.Callback mLocalCallback = (msg) -> { + switch (msg.what) { + case MSG_UPDATE_RUNNING_LIST: { + synchronized (mService) { + updateRunningList(); + } + return true; + } + } + return false; + }; + + /** + * Consider updating the list of "runnable" queues, specifically with + * relation to the given queue. + * <p> + * Typically called when {@link BroadcastProcessQueue#getRunnableAt()} might + * have changed, since that influences the order in which we'll promote a + * "runnable" queue to be "running." + */ + @GuardedBy("mService") + private void updateRunnableList(@NonNull BroadcastProcessQueue queue) { + if (mRunning.contains(queue)) { + // Already running; they'll be reinserted into the runnable list + // once they finish running, so no need to update them now + return; + } + + // TODO: better optimize by using insertion sort data structure + mRunnable.remove(queue); + if (queue.isRunnable()) { + mRunnable.add(queue); + } + mRunnable.sort(null); + } + + /** + * Consider updating the list of "running" queues. + * <p> + * This method can promote "runnable" queues to become "running", subject to + * a maximum of {@link #MAX_RUNNING_PROCESS_QUEUES} warm processes and only + * one pending cold-start. + */ + @GuardedBy("mService") + private void updateRunningList() { + int avail = MAX_RUNNING_PROCESS_QUEUES - mRunning.size(); + if (avail == 0) return; + + // If someone is waiting to go idle, everything is runnable now + final boolean waitingForIdle = !mWaitingForIdle.isEmpty(); + + // We're doing an update now, so remove any future update requests; + // we'll repost below if needed + mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST); + + boolean updateOomAdj = false; + final long now = SystemClock.uptimeMillis(); + for (int i = 0; i < mRunnable.size() && avail > 0; i++) { + final BroadcastProcessQueue queue = mRunnable.get(i); + final long runnableAt = queue.getRunnableAt(); + + // If queues beyond this point aren't ready to run yet, schedule + // another pass when they'll be runnable + if (runnableAt > now && !waitingForIdle) { + mLocalHandler.sendEmptyMessageAtTime(MSG_UPDATE_RUNNING_LIST, runnableAt); + break; + } + + // We might not have heard about a newly running process yet, so + // consider refreshing if we think we're cold + updateWarmProcess(queue); + + final boolean processWarm = queue.isProcessWarm(); + if (!processWarm) { + // We only offer to run one cold-start at a time to preserve + // system resources; below we either claim that single slot or + // skip to look for another warm process + if (mRunningColdStart == null) { + mRunningColdStart = queue; + } else { + continue; + } + } + + if (DEBUG_BROADCAST) logv("Promoting " + queue + + " from runnable to running; process is " + queue.app); + + // Allocate this available permit and start running! + mRunnable.remove(i); + mRunning.add(queue); + avail--; + i--; + + queue.makeActiveNextPending(); + + // If we're already warm, schedule it; otherwise we'll wait for the + // cold start to circle back around + if (processWarm) { + scheduleReceiverWarmLocked(queue); + } else { + scheduleReceiverColdLocked(queue); + } + + mService.enqueueOomAdjTargetLocked(queue.app); + updateOomAdj = true; + } + + if (updateOomAdj) { + mService.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_RECEIVER); + } + + if (waitingForIdle && isIdleLocked()) { + mWaitingForIdle.forEach((latch) -> latch.countDown()); + mWaitingForIdle.clear(); + } + } + + @Override + public boolean onApplicationAttachedLocked(@NonNull ProcessRecord app) { + boolean didSomething = false; + if ((mRunningColdStart != null) && (mRunningColdStart.app == app)) { + // We've been waiting for this app to cold start, and it's ready + // now; dispatch its next broadcast and clear the slot + scheduleReceiverWarmLocked(mRunningColdStart); + mRunningColdStart = null; + + // We might be willing to kick off another cold start + enqueueUpdateRunningList(); + didSomething = true; + } + return didSomething; + } + + @Override + public boolean onApplicationTimeoutLocked(@NonNull ProcessRecord app) { + return onApplicationCleanupLocked(app); + } + + @Override + public boolean onApplicationProblemLocked(@NonNull ProcessRecord app) { + return onApplicationCleanupLocked(app); + } + + @Override + public boolean onApplicationCleanupLocked(@NonNull ProcessRecord app) { + boolean didSomething = false; + if ((mRunningColdStart != null) && (mRunningColdStart.app == app)) { + // We've been waiting for this app to cold start, and it had + // trouble; clear the slot and fail delivery below + mRunningColdStart = null; + + // We might be willing to kick off another cold start + enqueueUpdateRunningList(); + didSomething = true; + } + + final BroadcastProcessQueue queue = getProcessQueue(app); + if (queue != null) { + queue.app = null; + + // If queue was running a broadcast, fail it + if (queue.isActive()) { + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + didSomething = true; + } + } + + return didSomething; + } + + @Override + public int getPreferredSchedulingGroupLocked(@NonNull ProcessRecord app) { + final BroadcastProcessQueue queue = getProcessQueue(app); + if ((queue != null) && mRunning.contains(queue)) { + return queue.getPreferredSchedulingGroupLocked(); + } + return ProcessList.SCHED_GROUP_UNDEFINED; + } + + @Override + public void enqueueBroadcastLocked(@NonNull BroadcastRecord r) { + // TODO: handle empty receivers to deliver result immediately + if (r.receivers == null) return; + + r.enqueueTime = SystemClock.uptimeMillis(); + r.enqueueRealTime = SystemClock.elapsedRealtime(); + r.enqueueClockTime = System.currentTimeMillis(); + + for (int i = 0; i < r.receivers.size(); i++) { + final Object receiver = r.receivers.get(i); + final BroadcastProcessQueue queue = getOrCreateProcessQueue( + getReceiverProcessName(receiver), getReceiverUid(receiver)); + queue.enqueueBroadcast(r, i); + updateRunnableList(queue); + enqueueUpdateRunningList(); + } + } + + private void scheduleReceiverColdLocked(@NonNull BroadcastProcessQueue queue) { + checkState(queue.isActive(), "isActive"); + + final BroadcastRecord r = queue.getActive(); + final Object receiver = queue.getActiveReceiver(); + + final ApplicationInfo info = ((ResolveInfo) receiver).activityInfo.applicationInfo; + final ComponentName component = ((ResolveInfo) receiver).activityInfo.getComponentName(); + + final int intentFlags = r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND; + final HostingRecord hostingRecord = new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST, + component, r.intent.getAction(), r.getHostingRecordTriggerType()); + final boolean isActivityCapable = (r.options != null + && r.options.getTemporaryAppAllowlistDuration() > 0); + final int zygotePolicyFlags = isActivityCapable ? ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE + : ZYGOTE_POLICY_FLAG_EMPTY; + final boolean allowWhileBooting = (r.intent.getFlags() + & Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0; + + if (DEBUG_BROADCAST) logv("Scheduling " + r + " to cold " + queue); + queue.app = mService.startProcessLocked(queue.processName, info, true, intentFlags, + hostingRecord, zygotePolicyFlags, allowWhileBooting, false); + if (queue.app == null) { + mRunningColdStart = null; + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + } + } + + private void scheduleReceiverWarmLocked(@NonNull BroadcastProcessQueue queue) { + checkState(queue.isActive(), "isActive"); + + final ProcessRecord app = queue.app; + final BroadcastRecord r = queue.getActive(); + final Object receiver = queue.getActiveReceiver(); + + // TODO: schedule ANR timeout trigger event + // TODO: apply temp allowlist exemptions + // TODO: apply background activity launch exemptions + + if (mSkipPolicy.shouldSkip(r, receiver)) { + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + return; + } + + final Intent receiverIntent = r.getReceiverIntent(receiver); + if (receiverIntent == null) { + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + return; + } + + if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app); + final IApplicationThread thread = app.getThread(); + if (thread != null) { + try { + queue.setActiveDeliveryState(BroadcastRecord.DELIVERY_SCHEDULED); + if (receiver instanceof BroadcastFilter) { + thread.scheduleRegisteredReceiver( + ((BroadcastFilter) receiver).receiverList.receiver, receiverIntent, + r.resultCode, r.resultData, r.resultExtras, r.ordered, r.initialSticky, + r.userId, app.mState.getReportedProcState()); + + // TODO: consider making registered receivers of unordered + // broadcasts report results to detect ANRs + if (!r.ordered) { + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED); + } + } else { + thread.scheduleReceiver(receiverIntent, ((ResolveInfo) receiver).activityInfo, + null, r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId, + app.mState.getReportedProcState()); + } + } catch (RemoteException e) { + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + synchronized (app.mService) { + app.scheduleCrashLocked(TAG, CannotDeliverBroadcastException.TYPE_ID, null); + } + } + } else { + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + } + } + + @Override + public boolean finishReceiverLocked(@NonNull ProcessRecord app, int resultCode, + @Nullable String resultData, @Nullable Bundle resultExtras, boolean resultAbort, + boolean waitForServices) { + final BroadcastProcessQueue queue = getProcessQueue(app); + return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED); + } + + private boolean finishReceiverLocked(@NonNull BroadcastProcessQueue queue, int deliveryState) { + checkState(queue.isActive(), "isActive"); + + if (deliveryState != BroadcastRecord.DELIVERY_DELIVERED) { + Slog.w(TAG, "Failed delivery of " + queue.getActive() + " to " + queue); + } + + queue.setActiveDeliveryState(deliveryState); + + // TODO: cancel any outstanding ANR timeout + // TODO: limit number of broadcasts in a row to avoid starvation + // TODO: if we're the last receiver of this broadcast, record to history + + if (queue.isRunnable() && queue.isProcessWarm()) { + // We're on a roll; move onto the next broadcast for this process + queue.makeActiveNextPending(); + scheduleReceiverWarmLocked(queue); + return true; + } else { + // We've drained running broadcasts; maybe move back to runnable + queue.makeActiveIdle(); + mRunning.remove(queue); + // App is no longer running a broadcast, so update its OOM + // adjust during our next pass; no need for an immediate update + mService.enqueueOomAdjTargetLocked(queue.app); + updateRunnableList(queue); + enqueueUpdateRunningList(); + return false; + } + } + + @Override + public boolean cleanupDisabledPackageReceiversLocked(String packageName, + Set<String> filterByClasses, int userId, boolean doit) { + // TODO: implement + return false; + } + + @Override + void start(@NonNull ContentResolver resolver) { + super.start(resolver); + + mService.registerUidObserver(new UidObserver() { + @Override + public void onUidCachedChanged(int uid, boolean cached) { + synchronized (mService) { + BroadcastProcessQueue leaf = mProcessQueues.get(uid); + while (leaf != null) { + leaf.setProcessCached(cached); + updateRunnableList(leaf); + leaf = leaf.next; + } + enqueueUpdateRunningList(); + } + } + }, ActivityManager.UID_OBSERVER_CACHED, 0, "android"); + } + + @Override + public boolean isIdleLocked() { + return mRunnable.isEmpty() && mRunning.isEmpty(); + } + + @Override + public void waitForIdle(@Nullable PrintWriter pw) { + final CountDownLatch latch = new CountDownLatch(1); + synchronized (mService) { + mWaitingForIdle.add(latch); + } + enqueueUpdateRunningList(); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void waitForBarrier(@Nullable PrintWriter pw) { + // TODO: implement + throw new UnsupportedOperationException(); + } + + @Override + public String describeStateLocked() { + return mRunnable.size() + " runnable, " + mRunning.size() + " running"; + } + + @Override + public boolean isDelayBehindServices() { + // TODO: implement + return false; + } + + @Override + public void backgroundServicesFinishedLocked(int userId) { + // TODO: implement + } + + private void updateWarmProcess(@NonNull BroadcastProcessQueue queue) { + if (!queue.isProcessWarm()) { + queue.app = mService.getProcessRecordLocked(queue.processName, queue.uid); + } + } + + private @NonNull BroadcastProcessQueue getOrCreateProcessQueue(@NonNull ProcessRecord app) { + return getOrCreateProcessQueue(app.processName, app.info.uid); + } + + private @NonNull BroadcastProcessQueue getOrCreateProcessQueue(@NonNull String processName, + int uid) { + BroadcastProcessQueue leaf = mProcessQueues.get(uid); + while (leaf != null) { + if (Objects.equals(leaf.processName, processName)) { + return leaf; + } else if (leaf.next == null) { + break; + } + leaf = leaf.next; + } + + BroadcastProcessQueue created = new BroadcastProcessQueue(processName, uid); + created.app = mService.getProcessRecordLocked(processName, uid); + + if (leaf == null) { + mProcessQueues.put(uid, created); + } else { + leaf.next = created; + } + return created; + } + + private @Nullable BroadcastProcessQueue getProcessQueue(@NonNull ProcessRecord app) { + return getProcessQueue(app.processName, app.info.uid); + } + + private @Nullable BroadcastProcessQueue getProcessQueue(@NonNull String processName, int uid) { + BroadcastProcessQueue leaf = mProcessQueues.get(uid); + while (leaf != null) { + if (Objects.equals(leaf.processName, processName)) { + return leaf; + } + leaf = leaf.next; + } + return null; + } + + @Override + public void dumpDebug(@NonNull ProtoOutputStream proto, long fieldId) { + long token = proto.start(fieldId); + proto.write(BroadcastQueueProto.QUEUE_NAME, mQueueName); + mHistory.dumpDebug(proto); + proto.end(token); + } + + @Override + public boolean dumpLocked(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, + @NonNull String[] args, int opti, boolean dumpAll, @Nullable String dumpPackage, + boolean needSep) { + final long now = SystemClock.uptimeMillis(); + final IndentingPrintWriter ipw = new IndentingPrintWriter(pw); + ipw.increaseIndent(); + + ipw.println(); + ipw.println("📋 Per-process queues:"); + ipw.increaseIndent(); + for (int i = 0; i < mProcessQueues.size(); i++) { + BroadcastProcessQueue leaf = mProcessQueues.valueAt(i); + while (leaf != null) { + leaf.dumpLocked(ipw); + leaf = leaf.next; + } + } + ipw.decreaseIndent(); + + ipw.println(); + ipw.println("🧍 Runnable:"); + ipw.increaseIndent(); + if (mRunnable.isEmpty()) { + ipw.println("(none)"); + } else { + for (BroadcastProcessQueue queue : mRunnable) { + TimeUtils.formatDuration(queue.getRunnableAt(), now, ipw); + ipw.print(' '); + ipw.println(queue.toShortString()); + } + } + ipw.decreaseIndent(); + + ipw.println(); + ipw.println("🏃 Running:"); + ipw.increaseIndent(); + if (mRunning.isEmpty()) { + ipw.println("(none)"); + } else { + for (BroadcastProcessQueue queue : mRunning) { + if (queue == mRunningColdStart) { + ipw.print("🥶 "); + } else { + ipw.print("\u3000 "); + } + ipw.println(queue.toShortString()); + } + } + ipw.decreaseIndent(); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + needSep = mHistory.dumpLocked(ipw, dumpPackage, mQueueName, sdf, dumpAll, needSep); + return needSep; + } +} diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 817831cb003b..96fd362d256c 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -84,6 +84,7 @@ final class BroadcastRecord extends Binder { final BroadcastOptions options; // BroadcastOptions supplied by caller final List receivers; // contains BroadcastFilter and ResolveInfo final int[] delivery; // delivery state of each receiver + final long[] scheduledTime; // uptimeMillis when each receiver was scheduled final long[] duration; // duration a receiver took to process broadcast IIntentReceiver resultTo; // who receives final result if non-null boolean deferred; @@ -127,10 +128,18 @@ final class BroadcastRecord extends Binder { static final int CALL_DONE_RECEIVE = 3; static final int WAITING_SERVICES = 4; + /** Initial state: waiting to run in future */ static final int DELIVERY_PENDING = 0; + /** Terminal state: finished successfully */ static final int DELIVERY_DELIVERED = 1; + /** Terminal state: skipped due to internal policy */ static final int DELIVERY_SKIPPED = 2; + /** Terminal state: timed out during attempted delivery */ static final int DELIVERY_TIMEOUT = 3; + /** Intermediate state: currently executing */ + static final int DELIVERY_SCHEDULED = 4; + /** Terminal state: failure to dispatch */ + static final int DELIVERY_FAILURE = 5; ProcessRecord curApp; // hosting application of current receiver. ComponentName curComponent; // the receiver class that is currently running. @@ -253,6 +262,8 @@ final class BroadcastRecord extends Binder { case DELIVERY_DELIVERED: pw.print("Deliver"); break; case DELIVERY_SKIPPED: pw.print("Skipped"); break; case DELIVERY_TIMEOUT: pw.print("Timeout"); break; + case DELIVERY_SCHEDULED: pw.print("Schedul"); break; + case DELIVERY_FAILURE: pw.print("Failure"); break; default: pw.print("???????"); break; } pw.print(" "); TimeUtils.formatDuration(duration[i], pw); @@ -300,6 +311,7 @@ final class BroadcastRecord extends Binder { options = _options; receivers = _receivers; delivery = new int[_receivers != null ? _receivers.size() : 0]; + scheduledTime = new long[delivery.length]; duration = new long[delivery.length]; resultTo = _resultTo; resultCode = _resultCode; @@ -346,6 +358,7 @@ final class BroadcastRecord extends Binder { options = from.options; receivers = from.receivers; delivery = from.delivery; + scheduledTime = from.scheduledTime; duration = from.duration; resultTo = from.resultTo; enqueueTime = from.enqueueTime; @@ -497,7 +510,77 @@ final class BroadcastRecord extends Binder { return ret; } - int getReceiverUid(Object receiver) { + /** + * Update the delivery state of the given {@link #receivers} index. + * Automatically updates any time measurements related to state changes. + */ + void setDeliveryState(int index, int deliveryState) { + delivery[index] = deliveryState; + + switch (deliveryState) { + case DELIVERY_DELIVERED: + duration[index] = SystemClock.uptimeMillis() - scheduledTime[index]; + break; + case DELIVERY_SCHEDULED: + scheduledTime[index] = SystemClock.uptimeMillis(); + break; + } + } + + boolean isForeground() { + return (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) != 0; + } + + boolean isReplacePending() { + return (intent.getFlags() & Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0; + } + + @NonNull String getHostingRecordTriggerType() { + if (alarm) { + return HostingRecord.TRIGGER_TYPE_ALARM; + } else if (pushMessage) { + return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE; + } else if (pushMessageOverQuota) { + return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA; + } + return HostingRecord.TRIGGER_TYPE_UNKNOWN; + } + + /** + * Return an instance of {@link #intent} specialized for the given receiver. + * For example, this returns a new specialized instance if the extras need + * to be filtered, or a {@link ResolveInfo} needs to be configured. + * + * @return a specialized intent, otherwise {@code null} to indicate that the + * broadcast should not be delivered to this receiver, typically due + * to it being filtered away by {@link #filterExtrasForReceiver}. + */ + @Nullable Intent getReceiverIntent(@NonNull Object receiver) { + Intent newIntent = null; + if (filterExtrasForReceiver != null) { + final Bundle extras = intent.getExtras(); + if (extras != null) { + final int receiverUid = getReceiverUid(receiver); + final Bundle filteredExtras = filterExtrasForReceiver.apply(receiverUid, extras); + if (filteredExtras == null) { + // Completely filtered; skip the broadcast! + return null; + } else { + newIntent = new Intent(intent); + newIntent.replaceExtras(filteredExtras); + } + } + } + if (receiver instanceof ResolveInfo) { + if (newIntent == null) { + newIntent = new Intent(intent); + } + newIntent.setComponent(((ResolveInfo) receiver).activityInfo.getComponentName()); + } + return (newIntent != null) ? newIntent : intent; + } + + static int getReceiverUid(@NonNull Object receiver) { if (receiver instanceof BroadcastFilter) { return ((BroadcastFilter) receiver).owningUid; } else /* if (receiver instanceof ResolveInfo) */ { @@ -505,6 +588,14 @@ final class BroadcastRecord extends Binder { } } + static String getReceiverProcessName(@NonNull Object receiver) { + if (receiver instanceof BroadcastFilter) { + return ((BroadcastFilter) receiver).receiverList.app.processName; + } else /* if (receiver instanceof ResolveInfo) */ { + return ((ResolveInfo) receiver).activityInfo.processName; + } + } + public BroadcastRecord maybeStripForHistory() { if (!intent.canStripForHistory()) { return this; @@ -561,6 +652,10 @@ final class BroadcastRecord extends Binder { + " u" + userId + " " + intent.getAction() + "}"; } + public String toShortString() { + return intent.getAction() + "/u" + userId; + } + public void dumpDebug(ProtoOutputStream proto, long fieldId) { long token = proto.start(fieldId); proto.write(BroadcastRecordProto.USER_ID, userId); diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index 9569848cbe37..e9b503007b5e 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -16,7 +16,6 @@ package com.android.server.am; -import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; import static com.android.server.am.ActivityManagerService.checkComponentPermission; import static com.android.server.am.BroadcastQueue.TAG; @@ -35,7 +34,6 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; -import android.os.Bundle; import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; @@ -59,6 +57,18 @@ public class BroadcastSkipPolicy { /** * Determine if the given {@link BroadcastRecord} is eligible to be sent to + * the given {@link BroadcastFilter} or {@link ResolveInfo}. + */ + public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull Object target) { + if (target instanceof BroadcastFilter) { + return shouldSkip(r, (BroadcastFilter) target); + } else { + return shouldSkip(r, (ResolveInfo) target); + } + } + + /** + * Determine if the given {@link BroadcastRecord} is eligible to be sent to * the given {@link ResolveInfo}. */ public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull ResolveInfo info) { diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 6775c9952ddd..7c7b01c96509 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -1641,7 +1641,7 @@ class UserController implements Handler.Callback { } mInjector.updateUserConfiguration(); updateCurrentProfileIds(); - mInjector.getWindowManager().setCurrentUser(userId, getCurrentProfileIds()); + mInjector.getWindowManager().setCurrentUser(userId); mInjector.reportCurWakefulnessUsageEvent(); // Once the internal notion of the active user has switched, we lock the device // with the option to show the user switcher on the keyguard. @@ -1655,7 +1655,6 @@ class UserController implements Handler.Callback { } else { final Integer currentUserIdInt = mCurrentUserId; updateCurrentProfileIds(); - mInjector.getWindowManager().setCurrentProfileIds(getCurrentProfileIds()); synchronized (mLock) { mUserLru.remove(currentUserIdInt); mUserLru.add(currentUserIdInt); diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 676dc196f20c..6a53978175af 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -3210,41 +3210,44 @@ public class Vpn { return; } - if (mSession != null && mMobikeEnabled) { - Log.d( - TAG, - "IKE Session has mobility. Delay handleSessionLost for losing network " - + network - + " on session with token " - + mCurrentToken); - - final int token = mCurrentToken; - // Delay the teardown in case a new network will be available soon. For example, - // during handover between two WiFi networks, Android will disconnect from the - // first WiFi and then connects to the second WiFi. - mScheduledHandleNetworkLostFuture = - mExecutor.schedule( - () -> { - if (isActiveToken(token)) { - handleSessionLost(null /* exception */, network); - } else { - Log.d( - TAG, - "Scheduled handleSessionLost fired for " - + "obsolete token " - + token); + Log.d(TAG, "Schedule a delay handleSessionLost for losing network " + + network + + " on session with token " + + mCurrentToken); + + final int token = mCurrentToken; + // Delay the teardown in case a new network will be available soon. For example, + // during handover between two WiFi networks, Android will disconnect from the + // first WiFi and then connects to the second WiFi. + mScheduledHandleNetworkLostFuture = + mExecutor.schedule( + () -> { + if (isActiveToken(token)) { + handleSessionLost(new IkeNetworkLostException(network), + network); + + synchronized (Vpn.this) { + // Ignore stale runner. + if (mVpnRunner != this) return; + + updateState(DetailedState.DISCONNECTED, + "Network lost"); } + } else { + Log.d( + TAG, + "Scheduled handleSessionLost fired for " + + "obsolete token " + + token); + } + + // Reset mScheduledHandleNetworkLostFuture since it's + // already run on executor thread. + mScheduledHandleNetworkLostFuture = null; + }, + NETWORK_LOST_TIMEOUT_MS, + TimeUnit.MILLISECONDS); - // Reset mScheduledHandleNetworkLostFuture since it's - // already run on executor thread. - mScheduledHandleNetworkLostFuture = null; - }, - NETWORK_LOST_TIMEOUT_MS, - TimeUnit.MILLISECONDS); - } else { - Log.d(TAG, "Call handleSessionLost for losing network " + network); - handleSessionLost(null /* exception */, network); - } } private void cancelHandleNetworkLostTimeout() { @@ -4185,8 +4188,6 @@ public class Vpn { */ @NonNull public synchronized List<String> getAppExclusionList(@NonNull String packageName) { - enforceNotRestrictedUser(); - final long oldId = Binder.clearCallingIdentity(); try { final byte[] bytes = getVpnProfileStore().get(getVpnAppExcludedForPackage(packageName)); diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index 8e00ccf68fb1..44c8e18a22cf 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java @@ -21,8 +21,11 @@ import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STA import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE; import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS; +import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_BASE_STATE; +import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_EMULATED_STATE; import static com.android.server.devicestate.OverrideRequestController.STATUS_ACTIVE; import static com.android.server.devicestate.OverrideRequestController.STATUS_CANCELED; +import static com.android.server.devicestate.OverrideRequestController.STATUS_UNKNOWN; import android.annotation.IntDef; import android.annotation.IntRange; @@ -106,6 +109,8 @@ public final class DeviceStateManagerService extends SystemService { private final BinderService mBinderService; @NonNull private final OverrideRequestController mOverrideRequestController; + @NonNull + private final DeviceStateProviderListener mDeviceStateProviderListener; @VisibleForTesting @NonNull public ActivityTaskManagerInternal mActivityTaskManagerInternal; @@ -139,6 +144,12 @@ public final class DeviceStateManagerService extends SystemService { @NonNull private Optional<OverrideRequest> mActiveOverride = Optional.empty(); + // The current active base state override request. When set the device state specified here will + // replace the value in mBaseState. + @GuardedBy("mLock") + @NonNull + private Optional<OverrideRequest> mActiveBaseStateOverride = Optional.empty(); + // List of processes registered to receive notifications about changes to device state and // request status indexed by process id. @GuardedBy("mLock") @@ -177,7 +188,8 @@ public final class DeviceStateManagerService extends SystemService { mOverrideRequestController = new OverrideRequestController( this::onOverrideRequestStatusChangedLocked); mDeviceStatePolicy = policy; - mDeviceStatePolicy.getDeviceStateProvider().setListener(new DeviceStateProviderListener()); + mDeviceStateProviderListener = new DeviceStateProviderListener(); + mDeviceStatePolicy.getDeviceStateProvider().setListener(mDeviceStateProviderListener); mBinderService = new BinderService(); mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); } @@ -257,6 +269,21 @@ public final class DeviceStateManagerService extends SystemService { } } + /** + * Returns the current override base state, or {@link Optional#empty()} if no override state is + * requested. If an override base state is present, the returned state will be the same as + * the base state returned from {@link #getBaseState()}. + */ + @NonNull + Optional<DeviceState> getOverrideBaseState() { + synchronized (mLock) { + if (mActiveBaseStateOverride.isPresent()) { + return getStateLocked(mActiveBaseStateOverride.get().getRequestedState()); + } + return Optional.empty(); + } + } + /** Returns the list of currently supported device states. */ DeviceState[] getSupportedStates() { synchronized (mLock) { @@ -366,6 +393,7 @@ public final class DeviceStateManagerService extends SystemService { } final DeviceState baseState = baseStateOptional.get(); + if (mBaseState.isPresent() && mBaseState.get().equals(baseState)) { // Base state hasn't changed. Nothing to do. return; @@ -375,7 +403,7 @@ public final class DeviceStateManagerService extends SystemService { if (baseState.hasFlag(FLAG_CANCEL_OVERRIDE_REQUESTS)) { mOverrideRequestController.cancelOverrideRequest(); } - mOverrideRequestController.handleBaseStateChanged(); + mOverrideRequestController.handleBaseStateChanged(identifier); updatePendingStateLocked(); if (!mPendingState.isPresent()) { @@ -528,16 +556,41 @@ public final class DeviceStateManagerService extends SystemService { } } + @GuardedBy("mLock") private void onOverrideRequestStatusChangedLocked(@NonNull OverrideRequest request, @OverrideRequestController.RequestStatus int status) { - if (status == STATUS_ACTIVE) { - mActiveOverride = Optional.of(request); - } else if (status == STATUS_CANCELED) { - if (mActiveOverride.isPresent() && mActiveOverride.get() == request) { - mActiveOverride = Optional.empty(); + if (request.getRequestType() == OVERRIDE_REQUEST_TYPE_BASE_STATE) { + switch (status) { + case STATUS_ACTIVE: + enableBaseStateRequestLocked(request); + return; + case STATUS_CANCELED: + if (mActiveBaseStateOverride.isPresent() + && mActiveBaseStateOverride.get() == request) { + mActiveBaseStateOverride = Optional.empty(); + } + break; + case STATUS_UNKNOWN: // same as default + default: + throw new IllegalArgumentException("Unknown request status: " + status); + } + } else if (request.getRequestType() == OVERRIDE_REQUEST_TYPE_EMULATED_STATE) { + switch (status) { + case STATUS_ACTIVE: + mActiveOverride = Optional.of(request); + break; + case STATUS_CANCELED: + if (mActiveOverride.isPresent() && mActiveOverride.get() == request) { + mActiveOverride = Optional.empty(); + } + break; + case STATUS_UNKNOWN: // same as default + default: + throw new IllegalArgumentException("Unknown request status: " + status); } } else { - throw new IllegalArgumentException("Unknown request status: " + status); + throw new IllegalArgumentException( + "Unknown OverrideRest type: " + request.getRequestType()); } boolean updatedPendingState = updatePendingStateLocked(); @@ -564,6 +617,18 @@ public final class DeviceStateManagerService extends SystemService { mHandler.post(this::notifyPolicyIfNeeded); } + /** + * Sets the new base state of the device and notifies the process that made the base state + * override request that the request is now active. + */ + @GuardedBy("mLock") + private void enableBaseStateRequestLocked(OverrideRequest request) { + setBaseState(request.getRequestedState()); + mActiveBaseStateOverride = Optional.of(request); + ProcessRecord processRecord = mProcessRecords.get(request.getPid()); + processRecord.notifyRequestActiveAsync(request.getToken()); + } + private void registerProcess(int pid, IDeviceStateManagerCallback callback) { synchronized (mLock) { if (mProcessRecords.contains(pid)) { @@ -606,7 +671,8 @@ public final class DeviceStateManagerService extends SystemService { + " has no registered callback."); } - if (mOverrideRequestController.hasRequest(token)) { + if (mOverrideRequestController.hasRequest(token, + OVERRIDE_REQUEST_TYPE_EMULATED_STATE)) { throw new IllegalStateException("Request has already been made for the supplied" + " token: " + token); } @@ -617,7 +683,8 @@ public final class DeviceStateManagerService extends SystemService { + " is not supported."); } - OverrideRequest request = new OverrideRequest(token, callingPid, state, flags); + OverrideRequest request = new OverrideRequest(token, callingPid, state, flags, + OVERRIDE_REQUEST_TYPE_EMULATED_STATE); mOverrideRequestController.addRequest(request); } } @@ -633,6 +700,44 @@ public final class DeviceStateManagerService extends SystemService { } } + private void requestBaseStateOverrideInternal(int state, int flags, int callingPid, + @NonNull IBinder token) { + synchronized (mLock) { + final Optional<DeviceState> deviceState = getStateLocked(state); + if (!deviceState.isPresent()) { + throw new IllegalArgumentException("Requested state: " + state + + " is not supported."); + } + + final ProcessRecord processRecord = mProcessRecords.get(callingPid); + if (processRecord == null) { + throw new IllegalStateException("Process " + callingPid + + " has no registered callback."); + } + + if (mOverrideRequestController.hasRequest(token, + OVERRIDE_REQUEST_TYPE_BASE_STATE)) { + throw new IllegalStateException("Request has already been made for the supplied" + + " token: " + token); + } + + OverrideRequest request = new OverrideRequest(token, callingPid, state, flags, + OVERRIDE_REQUEST_TYPE_BASE_STATE); + mOverrideRequestController.addBaseStateRequest(request); + } + } + + private void cancelBaseStateOverrideInternal(int callingPid) { + synchronized (mLock) { + final ProcessRecord processRecord = mProcessRecords.get(callingPid); + if (processRecord == null) { + throw new IllegalStateException("Process " + callingPid + + " has no registered callback."); + } + setBaseState(mDeviceStateProviderListener.mCurrentBaseState); + } + } + private void dumpInternal(PrintWriter pw) { pw.println("DEVICE STATE MANAGER (dumpsys device_state)"); @@ -729,6 +834,8 @@ public final class DeviceStateManagerService extends SystemService { } private final class DeviceStateProviderListener implements DeviceStateProvider.Listener { + @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int mCurrentBaseState; + @Override public void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates) { if (newDeviceStates.length == 0) { @@ -743,7 +850,7 @@ public final class DeviceStateManagerService extends SystemService { if (identifier < MINIMUM_DEVICE_STATE || identifier > MAXIMUM_DEVICE_STATE) { throw new IllegalArgumentException("Invalid identifier: " + identifier); } - + mCurrentBaseState = identifier; setBaseState(identifier); } } @@ -895,6 +1002,38 @@ public final class DeviceStateManagerService extends SystemService { } @Override // Binder call + public void requestBaseStateOverride(IBinder token, int state, int flags) { + final int callingPid = Binder.getCallingPid(); + getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE, + "Permission required to control base state of device."); + + if (token == null) { + throw new IllegalArgumentException("Request token must not be null."); + } + + final long callingIdentity = Binder.clearCallingIdentity(); + try { + requestBaseStateOverrideInternal(state, flags, callingPid, token); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + + @Override // Binder call + public void cancelBaseStateOverride() { + final int callingPid = Binder.getCallingPid(); + getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE, + "Permission required to control base state of device."); + + final long callingIdentity = Binder.clearCallingIdentity(); + try { + cancelBaseStateOverrideInternal(callingPid); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + + @Override // Binder call public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver result) { new DeviceStateManagerShellCommand(DeviceStateManagerService.this) diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java index 659ee75de453..8c6068d89296 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java @@ -16,11 +16,8 @@ package com.android.server.devicestate; -import static android.Manifest.permission.CONTROL_DEVICE_STATE; - import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.Context; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateRequest; import android.os.Binder; @@ -39,6 +36,8 @@ import java.util.stream.Collectors; public class DeviceStateManagerShellCommand extends ShellCommand { @Nullable private static DeviceStateRequest sLastRequest; + @Nullable + private static DeviceStateRequest sLastBaseStateRequest; private final DeviceStateManagerService mService; private final DeviceStateManager mClient; @@ -58,6 +57,8 @@ public class DeviceStateManagerShellCommand extends ShellCommand { switch (cmd) { case "state": return runState(pw); + case "base-state": + return runBaseState(pw); case "print-state": return runPrintState(pw); case "print-states": @@ -89,10 +90,6 @@ public class DeviceStateManagerShellCommand extends ShellCommand { return 0; } - final Context context = mService.getContext(); - context.enforceCallingOrSelfPermission( - CONTROL_DEVICE_STATE, - "Permission required to request device state."); final long callingIdentity = Binder.clearCallingIdentity(); try { if ("reset".equals(nextArg)) { @@ -127,6 +124,47 @@ public class DeviceStateManagerShellCommand extends ShellCommand { return 0; } + private int runBaseState(PrintWriter pw) { + final String nextArg = getNextArg(); + if (nextArg == null) { + printAllStates(pw); + return 0; + } + + final long callingIdentity = Binder.clearCallingIdentity(); + try { + if ("reset".equals(nextArg)) { + if (sLastBaseStateRequest != null) { + mClient.cancelBaseStateOverride(); + sLastBaseStateRequest = null; + } + } else { + int requestedState = Integer.parseInt(nextArg); + DeviceStateRequest request = DeviceStateRequest.newBuilder(requestedState).build(); + + mClient.requestBaseStateOverride(request, null, null); + + sLastBaseStateRequest = request; + } + } catch (NumberFormatException e) { + getErrPrintWriter().println("Error: requested state should be an integer"); + return -1; + } catch (IllegalArgumentException e) { + getErrPrintWriter().println("Error: " + e.getMessage()); + getErrPrintWriter().println("-------------------"); + getErrPrintWriter().println("Run:"); + getErrPrintWriter().println(""); + getErrPrintWriter().println(" print-states"); + getErrPrintWriter().println(""); + getErrPrintWriter().println("to get the list of currently supported device states"); + return -1; + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + + return 0; + } + private int runPrintState(PrintWriter pw) { Optional<DeviceState> deviceState = mService.getCommittedState(); if (deviceState.isPresent()) { diff --git a/services/core/java/com/android/server/devicestate/OverrideRequest.java b/services/core/java/com/android/server/devicestate/OverrideRequest.java index 35a4c844c710..325e384cb8ad 100644 --- a/services/core/java/com/android/server/devicestate/OverrideRequest.java +++ b/services/core/java/com/android/server/devicestate/OverrideRequest.java @@ -16,9 +16,13 @@ package com.android.server.devicestate; +import android.annotation.IntDef; import android.hardware.devicestate.DeviceStateRequest; import android.os.IBinder; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A request to override the state managed by {@link DeviceStateManagerService}. * @@ -30,13 +34,47 @@ final class OverrideRequest { private final int mRequestedState; @DeviceStateRequest.RequestFlags private final int mFlags; + @OverrideRequestType + private final int mRequestType; + + /** + * Denotes that the request is meant to override the emulated state of the device. This will + * not change the base (physical) state of the device. + * + * This request type should be used if you are looking to emulate a device state for a feature + * but want the system to be aware of the physical state of the device. + */ + public static final int OVERRIDE_REQUEST_TYPE_EMULATED_STATE = 0; + + /** + * Denotes that the request is meant to override the base (physical) state of the device. + * Overriding the base state may not change the emulated state of the device if there is also an + * override request active for that property. + * + * This request type should only be used for testing, where you want to simulate the physical + * state of the device changing. + */ + public static final int OVERRIDE_REQUEST_TYPE_BASE_STATE = 1; + + /** + * Flags for signifying the type of {@link OverrideRequest}. + * + * @hide + */ + @IntDef(flag = true, prefix = { "REQUEST_TYPE_" }, value = { + OVERRIDE_REQUEST_TYPE_BASE_STATE, + OVERRIDE_REQUEST_TYPE_EMULATED_STATE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface OverrideRequestType {} OverrideRequest(IBinder token, int pid, int requestedState, - @DeviceStateRequest.RequestFlags int flags) { + @DeviceStateRequest.RequestFlags int flags, @OverrideRequestType int requestType) { mToken = token; mPid = pid; mRequestedState = requestedState; mFlags = flags; + mRequestType = requestType; } IBinder getToken() { @@ -55,4 +93,9 @@ final class OverrideRequest { int getFlags() { return mFlags; } + + @OverrideRequestType + int getRequestType() { + return mRequestType; + } } diff --git a/services/core/java/com/android/server/devicestate/OverrideRequestController.java b/services/core/java/com/android/server/devicestate/OverrideRequestController.java index 01f5a09323cf..e39c7324d7bd 100644 --- a/services/core/java/com/android/server/devicestate/OverrideRequestController.java +++ b/services/core/java/com/android/server/devicestate/OverrideRequestController.java @@ -75,6 +75,8 @@ final class OverrideRequestController { // Handle to the current override request, null if none. private OverrideRequest mRequest; + // Handle to the current base state override request, null if none. + private OverrideRequest mBaseStateRequest; private boolean mStickyRequestsAllowed; // The current request has outlived their process. @@ -111,13 +113,23 @@ final class OverrideRequestController { } } + void addBaseStateRequest(@NonNull OverrideRequest request) { + OverrideRequest previousRequest = mBaseStateRequest; + mBaseStateRequest = request; + mListener.onStatusChanged(request, STATUS_ACTIVE); + + if (previousRequest != null) { + cancelRequestLocked(previousRequest); + } + } + /** * Cancels the request with the specified {@code token} and notifies the listener of all changes * to request status as a result of this operation. */ void cancelRequest(@NonNull OverrideRequest request) { // Either don't have a current request or attempting to cancel an already cancelled request - if (!hasRequest(request.getToken())) { + if (!hasRequest(request.getToken(), request.getRequestType())) { return; } cancelCurrentRequestLocked(); @@ -144,11 +156,24 @@ final class OverrideRequestController { } /** + * Cancels the current base state override request, this could be due to the physical + * configuration of the device changing. + */ + void cancelBaseStateOverrideRequest() { + cancelCurrentBaseStateRequestLocked(); + } + + /** * Returns {@code true} if this controller is current managing a request with the specified * {@code token}, {@code false} otherwise. */ - boolean hasRequest(@NonNull IBinder token) { - return mRequest != null && token == mRequest.getToken(); + boolean hasRequest(@NonNull IBinder token, + @OverrideRequest.OverrideRequestType int requestType) { + if (requestType == OverrideRequest.OVERRIDE_REQUEST_TYPE_BASE_STATE) { + return mBaseStateRequest != null && token == mBaseStateRequest.getToken(); + } else { + return mRequest != null && token == mRequest.getToken(); + } } /** @@ -157,11 +182,11 @@ final class OverrideRequestController { * operation. */ void handleProcessDied(int pid) { - if (mRequest == null) { - return; + if (mBaseStateRequest != null && mBaseStateRequest.getPid() == pid) { + cancelCurrentBaseStateRequestLocked(); } - if (mRequest.getPid() == pid) { + if (mRequest != null && mRequest.getPid() == pid) { if (mStickyRequestsAllowed) { // Do not cancel the requests now because sticky requests are allowed. These // requests will be cancelled on a call to cancelStickyRequests(). @@ -176,7 +201,10 @@ final class OverrideRequestController { * Notifies the controller that the base state has changed. The controller will notify the * listener of all changes to request status as a result of this change. */ - void handleBaseStateChanged() { + void handleBaseStateChanged(int state) { + if (mBaseStateRequest != null && state != mBaseStateRequest.getRequestedState()) { + cancelBaseStateOverrideRequest(); + } if (mRequest == null) { return; } @@ -192,11 +220,12 @@ final class OverrideRequestController { * notify the listener of all changes to request status as a result of this change. */ void handleNewSupportedStates(int[] newSupportedStates) { - if (mRequest == null) { - return; + if (mBaseStateRequest != null && !contains(newSupportedStates, + mBaseStateRequest.getRequestedState())) { + cancelCurrentBaseStateRequestLocked(); } - if (!contains(newSupportedStates, mRequest.getRequestedState())) { + if (mRequest != null && !contains(newSupportedStates, mRequest.getRequestedState())) { cancelCurrentRequestLocked(); } } @@ -228,10 +257,23 @@ final class OverrideRequestController { return; } mStickyRequest = false; - mListener.onStatusChanged(mRequest, STATUS_CANCELED); + cancelRequestLocked(mRequest); mRequest = null; } + /** + * Handles cancelling {@code mBaseStateRequest}. + * Notifies the listener of the canceled status as well. + */ + private void cancelCurrentBaseStateRequestLocked() { + if (mBaseStateRequest == null) { + Slog.w(TAG, "Attempted to cancel a null OverrideRequest"); + return; + } + cancelRequestLocked(mBaseStateRequest); + mBaseStateRequest = null; + } + private static boolean contains(int[] array, int value) { for (int i = 0; i < array.length; i++) { if (array[i] == value) { diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java index 2f67dddc1bf5..372bc8ad94a1 100644 --- a/services/core/java/com/android/server/display/ColorFade.java +++ b/services/core/java/com/android/server/display/ColorFade.java @@ -31,7 +31,6 @@ import android.opengl.EGLDisplay; import android.opengl.EGLSurface; import android.opengl.GLES11Ext; import android.opengl.GLES20; -import android.os.IBinder; import android.util.Slog; import android.view.Display; import android.view.DisplayInfo; @@ -170,15 +169,7 @@ final class ColorFade { mDisplayWidth = displayInfo.getNaturalWidth(); mDisplayHeight = displayInfo.getNaturalHeight(); - final IBinder token = SurfaceControl.getInternalDisplayToken(); - if (token == null) { - Slog.e(TAG, - "Failed to take screenshot because internal display is disconnected"); - return false; - } - final boolean isWideColor = SurfaceControl.getDynamicDisplayInfo(token).activeColorMode - == Display.COLOR_MODE_DISPLAY_P3; - + final boolean isWideColor = displayInfo.colorMode == Display.COLOR_MODE_DISPLAY_P3; // Set mPrepared here so if initialization fails, resources can be cleaned up. mPrepared = true; diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java index e9640cf6b4d5..a060f076d4fb 100644 --- a/services/core/java/com/android/server/display/DisplayControl.java +++ b/services/core/java/com/android/server/display/DisplayControl.java @@ -16,6 +16,9 @@ package com.android.server.display; +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.os.IBinder; import java.util.Objects; @@ -26,6 +29,7 @@ import java.util.Objects; public class DisplayControl { private static native IBinder nativeCreateDisplay(String name, boolean secure); private static native void nativeDestroyDisplay(IBinder displayToken); + private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes); /** * Create a display in SurfaceFlinger. @@ -52,4 +56,11 @@ public class DisplayControl { nativeDestroyDisplay(displayToken); } + /** + * Overrides HDR modes for a display device. + */ + @RequiresPermission(Manifest.permission.ACCESS_SURFACE_FLINGER) + public static void overrideHdrTypes(@NonNull IBinder displayToken, @NonNull int[] modes) { + nativeOverrideHdrTypes(displayToken, modes); + } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 3a037edf12eb..2cde526c6dbf 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2562,10 +2562,10 @@ public final class DisplayManagerService extends SystemService { final BrightnessSetting brightnessSetting = new BrightnessSetting(mPersistentDataStore, display, mSyncRoot); - final DisplayPowerController displayPowerController; + final DisplayPowerControllerInterface displayPowerController; if (SystemProperties.getInt(PROP_USE_NEW_DISPLAY_POWER_CONTROLLER, 0) == 1) { - displayPowerController = new DisplayPowerController( + displayPowerController = new DisplayPowerController2( mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler, mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting, () -> handleBrightnessChange(display)); @@ -3000,6 +3000,19 @@ public final class DisplayManagerService extends SystemService { } } + @Override + public void overrideHdrTypes(int displayId, int[] modes) { + IBinder displayToken; + synchronized (mSyncRoot) { + displayToken = getDisplayToken(displayId); + if (displayToken == null) { + throw new IllegalArgumentException("Invalid display: " + displayId); + } + } + + DisplayControl.overrideHdrTypes(displayToken, modes); + } + @Override // Binder call public void setAreUserDisabledHdrTypesAllowed(boolean areUserDisabledHdrTypesAllowed) { mContext.enforceCallingOrSelfPermission( @@ -3884,6 +3897,19 @@ public final class DisplayManagerService extends SystemService { return displayIdToMirror; } } + + @Override + public SurfaceControl.DisplayPrimaries getDisplayNativePrimaries(int displayId) { + IBinder displayToken; + synchronized (mSyncRoot) { + displayToken = getDisplayToken(displayId); + if (displayToken == null) { + throw new IllegalArgumentException("Invalid displayId=" + displayId); + } + } + + return SurfaceControl.getDisplayNativePrimaries(displayToken); + } } class DesiredDisplayModeSpecsObserver diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 58a80e3f977b..61225d7dee80 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -798,8 +798,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call * Notified when the display is changed. We use this to apply any changes that might be needed * when displays get swapped on foldable devices. For example, different brightness properties * of each display need to be properly reflected in AutomaticBrightnessController. + * + * Make sure DisplayManagerService.mSyncRoot is held when this is called */ - @GuardedBy("DisplayManagerService.mSyncRoot") @Override public void onDisplayChanged() { final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); @@ -988,8 +989,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call com.android.internal.R.array.config_screenBrighteningThresholds); int[] screenDarkeningThresholds = resources.getIntArray( com.android.internal.R.array.config_screenDarkeningThresholds); - int[] screenThresholdLevels = resources.getIntArray( - com.android.internal.R.array.config_screenThresholdLevels); + float[] screenThresholdLevels = BrightnessMappingStrategy.getFloatArray(resources + .obtainTypedArray(com.android.internal.R.array.config_screenThresholdLevels)); float screenDarkeningMinThreshold = mDisplayDeviceConfig.getScreenDarkeningMinThreshold(); float screenBrighteningMinThreshold = @@ -1896,7 +1897,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } }, () -> { - sendUpdatePowerStateLocked(); + sendUpdatePowerState(); postBrightnessChangeRunnable(); // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern. if (mAutomaticBrightnessController != null) { @@ -1912,7 +1913,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call ddConfig != null ? ddConfig.getBrightnessThrottlingData() : null; return new BrightnessThrottler(mHandler, data, () -> { - sendUpdatePowerStateLocked(); + sendUpdatePowerState(); postBrightnessChangeRunnable(); }, mUniqueDisplayId); } diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java new file mode 100644 index 000000000000..dc7db10123db --- /dev/null +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -0,0 +1,3071 @@ +/* + * 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.display; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.display.AmbientBrightnessDayStats; +import android.hardware.display.BrightnessChangeEvent; +import android.hardware.display.BrightnessConfiguration; +import android.hardware.display.BrightnessInfo; +import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks; +import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest; +import android.metrics.LogMaker; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.Trace; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.FloatProperty; +import android.util.Log; +import android.util.MathUtils; +import android.util.MutableFloat; +import android.util.MutableInt; +import android.util.Slog; +import android.util.TimeUtils; +import android.view.Display; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IBatteryStats; +import com.android.internal.display.BrightnessSynchronizer; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.RingBuffer; +import com.android.server.LocalServices; +import com.android.server.am.BatteryStatsService; +import com.android.server.display.RampAnimator.DualRampAnimator; +import com.android.server.display.brightness.BrightnessEvent; +import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal; +import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener; +import com.android.server.display.utils.SensorUtils; +import com.android.server.display.whitebalance.DisplayWhiteBalanceController; +import com.android.server.display.whitebalance.DisplayWhiteBalanceFactory; +import com.android.server.display.whitebalance.DisplayWhiteBalanceSettings; +import com.android.server.policy.WindowManagerPolicy; + +import java.io.PrintWriter; + +/** + * Controls the power state of the display. + * + * Handles the proximity sensor, light sensor, and animations between states + * including the screen off animation. + * + * This component acts independently of the rest of the power manager service. + * In particular, it does not share any state and it only communicates + * via asynchronous callbacks to inform the power manager that something has + * changed. + * + * Everything this class does internally is serialized on its handler although + * it may be accessed by other threads from the outside. + * + * Note that the power manager service guarantees that it will hold a suspend + * blocker as long as the display is not ready. So most of the work done here + * does not need to worry about holding a suspend blocker unless it happens + * independently of the display ready signal. + * + * For debugging, you can make the color fade and brightness animations run + * slower by changing the "animator duration scale" option in Development Settings. + */ +final class DisplayPowerController2 implements AutomaticBrightnessController.Callbacks, + DisplayWhiteBalanceController.Callbacks, DisplayPowerControllerInterface { + private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked"; + private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked"; + + private static final boolean DEBUG = false; + private static final boolean DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT = false; + + // If true, uses the color fade on animation. + // We might want to turn this off if we cannot get a guarantee that the screen + // actually turns on and starts showing new content after the call to set the + // screen state returns. Playing the animation can also be somewhat slow. + private static final boolean USE_COLOR_FADE_ON_ANIMATION = false; + + private static final float SCREEN_ANIMATION_RATE_MINIMUM = 0.0f; + + private static final int COLOR_FADE_ON_ANIMATION_DURATION_MILLIS = 250; + private static final int COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS = 400; + + private static final int MSG_UPDATE_POWER_STATE = 1; + private static final int MSG_PROXIMITY_SENSOR_DEBOUNCED = 2; + private static final int MSG_SCREEN_ON_UNBLOCKED = 3; + private static final int MSG_SCREEN_OFF_UNBLOCKED = 4; + private static final int MSG_CONFIGURE_BRIGHTNESS = 5; + private static final int MSG_SET_TEMPORARY_BRIGHTNESS = 6; + private static final int MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT = 7; + private static final int MSG_IGNORE_PROXIMITY = 8; + private static final int MSG_STOP = 9; + private static final int MSG_UPDATE_BRIGHTNESS = 10; + private static final int MSG_UPDATE_RBC = 11; + private static final int MSG_BRIGHTNESS_RAMP_DONE = 12; + private static final int MSG_STATSD_HBM_BRIGHTNESS = 13; + + private static final int PROXIMITY_UNKNOWN = -1; + private static final int PROXIMITY_NEGATIVE = 0; + private static final int PROXIMITY_POSITIVE = 1; + + // Proximity sensor debounce delay in milliseconds for positive or negative transitions. + private static final int PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY = 0; + private static final int PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY = 250; + + private static final int BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS = 500; + + // Trigger proximity if distance is less than 5 cm. + private static final float TYPICAL_PROXIMITY_THRESHOLD = 5.0f; + + // State machine constants for tracking initial brightness ramp skipping when enabled. + private static final int RAMP_STATE_SKIP_NONE = 0; + private static final int RAMP_STATE_SKIP_INITIAL = 1; + private static final int RAMP_STATE_SKIP_AUTOBRIGHT = 2; + + private static final int REPORTED_TO_POLICY_UNREPORTED = -1; + private static final int REPORTED_TO_POLICY_SCREEN_OFF = 0; + private static final int REPORTED_TO_POLICY_SCREEN_TURNING_ON = 1; + private static final int REPORTED_TO_POLICY_SCREEN_ON = 2; + private static final int REPORTED_TO_POLICY_SCREEN_TURNING_OFF = 3; + + private static final int RINGBUFFER_MAX = 100; + + private final String mTag; + + private final Object mLock = new Object(); + + private final Context mContext; + + // Our handler. + private final DisplayControllerHandler mHandler; + + // Asynchronous callbacks into the power manager service. + // Only invoked from the handler thread while no locks are held. + private final DisplayPowerCallbacks mCallbacks; + + // Battery stats. + @Nullable + private final IBatteryStats mBatteryStats; + + // The sensor manager. + private final SensorManager mSensorManager; + + // The window manager policy. + private final WindowManagerPolicy mWindowManagerPolicy; + + // The display blanker. + private final DisplayBlanker mBlanker; + + // The LogicalDisplay tied to this DisplayPowerController2. + private final LogicalDisplay mLogicalDisplay; + + // The ID of the LogicalDisplay tied to this DisplayPowerController2. + private final int mDisplayId; + + // The unique ID of the primary display device currently tied to this logical display + private String mUniqueDisplayId; + + // Tracker for brightness changes. + @Nullable + private final BrightnessTracker mBrightnessTracker; + + // Tracker for brightness settings changes. + private final SettingsObserver mSettingsObserver; + + // The proximity sensor, or null if not available or needed. + private Sensor mProximitySensor; + + // The doze screen brightness. + private final float mScreenBrightnessDozeConfig; + + // The dim screen brightness. + private final float mScreenBrightnessDimConfig; + + // The minimum dim amount to use if the screen brightness is already below + // mScreenBrightnessDimConfig. + private final float mScreenBrightnessMinimumDimAmount; + + private final float mScreenBrightnessDefault; + + // The minimum allowed brightness while in VR. + private final float mScreenBrightnessForVrRangeMinimum; + + // The maximum allowed brightness while in VR. + private final float mScreenBrightnessForVrRangeMaximum; + + // The default screen brightness for VR. + private final float mScreenBrightnessForVrDefault; + + // True if auto-brightness should be used. + private boolean mUseSoftwareAutoBrightnessConfig; + + // True if should use light sensor to automatically determine doze screen brightness. + private final boolean mAllowAutoBrightnessWhileDozingConfig; + + // Whether or not the color fade on screen on / off is enabled. + private final boolean mColorFadeEnabled; + + @GuardedBy("mCachedBrightnessInfo") + private final CachedBrightnessInfo mCachedBrightnessInfo = new CachedBrightnessInfo(); + + private DisplayDevice mDisplayDevice; + + // True if we should fade the screen while turning it off, false if we should play + // a stylish color fade animation instead. + private final boolean mColorFadeFadesConfig; + + // True if we need to fake a transition to off when coming out of a doze state. + // Some display hardware will blank itself when coming out of doze in order to hide + // artifacts. For these displays we fake a transition into OFF so that policy can appropriately + // blank itself and begin an appropriate power on animation. + private final boolean mDisplayBlanksAfterDozeConfig; + + // True if there are only buckets of brightness values when the display is in the doze state, + // rather than a full range of values. If this is true, then we'll avoid animating the screen + // brightness since it'd likely be multiple jarring brightness transitions instead of just one + // to reach the final state. + private final boolean mBrightnessBucketsInDozeConfig; + + private final Clock mClock; + private final Injector mInjector; + + // Maximum time a ramp animation can take. + private long mBrightnessRampIncreaseMaxTimeMillis; + private long mBrightnessRampDecreaseMaxTimeMillis; + + // The pending power request. + // Initially null until the first call to requestPowerState. + @GuardedBy("mLock") + private DisplayPowerRequest mPendingRequestLocked; + + // True if a request has been made to wait for the proximity sensor to go negative. + @GuardedBy("mLock") + private boolean mPendingWaitForNegativeProximityLocked; + + // True if the pending power request or wait for negative proximity flag + // has been changed since the last update occurred. + @GuardedBy("mLock") + private boolean mPendingRequestChangedLocked; + + // Set to true when the important parts of the pending power request have been applied. + // The important parts are mainly the screen state. Brightness changes may occur + // concurrently. + @GuardedBy("mLock") + private boolean mDisplayReadyLocked; + + // Set to true if a power state update is required. + @GuardedBy("mLock") + private boolean mPendingUpdatePowerStateLocked; + + /* The following state must only be accessed by the handler thread. */ + + // The currently requested power state. + // The power controller will progressively update its internal state to match + // the requested power state. Initially null until the first update. + private DisplayPowerRequest mPowerRequest; + + // The current power state. + // Must only be accessed on the handler thread. + private DisplayPowerState mPowerState; + + // True if the device should wait for negative proximity sensor before + // waking up the screen. This is set to false as soon as a negative + // proximity sensor measurement is observed or when the device is forced to + // go to sleep by the user. While true, the screen remains off. + private boolean mWaitingForNegativeProximity; + + // True if the device should not take into account the proximity sensor + // until either the proximity sensor state changes, or there is no longer a + // request to listen to proximity sensor. + private boolean mIgnoreProximityUntilChanged; + + // The actual proximity sensor threshold value. + private float mProximityThreshold; + + // Set to true if the proximity sensor listener has been registered + // with the sensor manager. + private boolean mProximitySensorEnabled; + + // The debounced proximity sensor state. + private int mProximity = PROXIMITY_UNKNOWN; + + // The raw non-debounced proximity sensor state. + private int mPendingProximity = PROXIMITY_UNKNOWN; + private long mPendingProximityDebounceTime = -1; // -1 if fully debounced + + // True if the screen was turned off because of the proximity sensor. + // When the screen turns on again, we report user activity to the power manager. + private boolean mScreenOffBecauseOfProximity; + + // The currently active screen on unblocker. This field is non-null whenever + // we are waiting for a callback to release it and unblock the screen. + private ScreenOnUnblocker mPendingScreenOnUnblocker; + private ScreenOffUnblocker mPendingScreenOffUnblocker; + + // True if we were in the process of turning off the screen. + // This allows us to recover more gracefully from situations where we abort + // turning off the screen. + private boolean mPendingScreenOff; + + // True if we have unfinished business and are holding a suspend blocker. + private boolean mUnfinishedBusiness; + + // The elapsed real time when the screen on was blocked. + private long mScreenOnBlockStartRealTime; + private long mScreenOffBlockStartRealTime; + + // Screen state we reported to policy. Must be one of REPORTED_TO_POLICY_* fields. + private int mReportedScreenStateToPolicy = REPORTED_TO_POLICY_UNREPORTED; + + // If the last recorded screen state was dozing or not. + private boolean mDozing; + + // Remembers whether certain kinds of brightness adjustments + // were recently applied so that we can decide how to transition. + private boolean mAppliedAutoBrightness; + private boolean mAppliedDimming; + private boolean mAppliedLowPower; + private boolean mAppliedScreenBrightnessOverride; + private boolean mAppliedTemporaryBrightness; + private boolean mAppliedTemporaryAutoBrightnessAdjustment; + private boolean mAppliedBrightnessBoost; + private boolean mAppliedThrottling; + + // Reason for which the brightness was last changed. See {@link BrightnessReason} for more + // information. + // At the time of this writing, this value is changed within updatePowerState() only, which is + // limited to the thread used by DisplayControllerHandler. + private final BrightnessReason mBrightnessReason = new BrightnessReason(); + private final BrightnessReason mBrightnessReasonTemp = new BrightnessReason(); + + // Brightness animation ramp rates in brightness units per second + private float mBrightnessRampRateFastDecrease; + private float mBrightnessRampRateFastIncrease; + private float mBrightnessRampRateSlowDecrease; + private float mBrightnessRampRateSlowIncrease; + + // Report HBM brightness change to StatsD + private int mDisplayStatsId; + private float mLastStatsBrightness = PowerManager.BRIGHTNESS_MIN; + + // Whether or not to skip the initial brightness ramps into STATE_ON. + private final boolean mSkipScreenOnBrightnessRamp; + + // Display white balance components. + @Nullable + private final DisplayWhiteBalanceSettings mDisplayWhiteBalanceSettings; + @Nullable + private final DisplayWhiteBalanceController mDisplayWhiteBalanceController; + + @Nullable + private final ColorDisplayServiceInternal mCdsi; + private float[] mNitsRange; + + private final HighBrightnessModeController mHbmController; + + private final BrightnessThrottler mBrightnessThrottler; + + private final BrightnessSetting mBrightnessSetting; + + private final Runnable mOnBrightnessChangeRunnable; + + private final BrightnessEvent mLastBrightnessEvent; + private final BrightnessEvent mTempBrightnessEvent; + + // Keeps a record of brightness changes for dumpsys. + private RingBuffer<BrightnessEvent> mBrightnessEventRingBuffer; + + // A record of state for skipping brightness ramps. + private int mSkipRampState = RAMP_STATE_SKIP_NONE; + + // The first autobrightness value set when entering RAMP_STATE_SKIP_INITIAL. + private float mInitialAutoBrightness; + + // The controller for the automatic brightness level. + @Nullable + private AutomaticBrightnessController mAutomaticBrightnessController; + + private Sensor mLightSensor; + + // The mappers between ambient lux, display backlight values, and display brightness. + // We will switch between the idle mapper and active mapper in AutomaticBrightnessController. + // Mapper used for active (normal) screen brightness mode + @Nullable + private BrightnessMappingStrategy mInteractiveModeBrightnessMapper; + // Mapper used for idle screen brightness mode + @Nullable + private BrightnessMappingStrategy mIdleModeBrightnessMapper; + + // The current brightness configuration. + @Nullable + private BrightnessConfiguration mBrightnessConfiguration; + + // The last brightness that was set by the user and not temporary. Set to + // PowerManager.BRIGHTNESS_INVALID_FLOAT when a brightness has yet to be recorded. + private float mLastUserSetScreenBrightness = Float.NaN; + + // The screen brightness setting has changed but not taken effect yet. If this is different + // from the current screen brightness setting then this is coming from something other than us + // and should be considered a user interaction. + private float mPendingScreenBrightnessSetting; + + // The last observed screen brightness setting, either set by us or by the settings app on + // behalf of the user. + private float mCurrentScreenBrightnessSetting; + + // The temporary screen brightness. Typically set when a user is interacting with the + // brightness slider but hasn't settled on a choice yet. Set to + // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary brightness set. + private float mTemporaryScreenBrightness; + + // The current screen brightness while in VR mode. + private float mScreenBrightnessForVr; + + // The last auto brightness adjustment that was set by the user and not temporary. Set to + // Float.NaN when an auto-brightness adjustment hasn't been recorded yet. + private float mAutoBrightnessAdjustment; + + // The pending auto brightness adjustment that will take effect on the next power state update. + private float mPendingAutoBrightnessAdjustment; + + // The temporary auto brightness adjustment. Typically set when a user is interacting with the + // adjustment slider but hasn't settled on a choice yet. Set to + // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary adjustment set. + private float mTemporaryAutoBrightnessAdjustment; + + private boolean mIsRbcActive; + + // Whether there's a callback to tell listeners the display has changed scheduled to run. When + // true it implies a wakelock is being held to guarantee the update happens before we collapse + // into suspend and so needs to be cleaned up if the thread is exiting. + // Should only be accessed on the Handler thread. + private boolean mOnStateChangedPending; + + // Count of proximity messages currently on this DPC's Handler. Used to keep track of how many + // suspend blocker acquisitions are pending when shutting down this DPC. + // Should only be accessed on the Handler thread. + private int mOnProximityPositiveMessages; + private int mOnProximityNegativeMessages; + + // Animators. + private ObjectAnimator mColorFadeOnAnimator; + private ObjectAnimator mColorFadeOffAnimator; + private DualRampAnimator<DisplayPowerState> mScreenBrightnessRampAnimator; + private BrightnessSetting.BrightnessSettingListener mBrightnessSettingListener; + + // True if this DisplayPowerController2 has been stopped and should no longer be running. + private boolean mStopped; + + private DisplayDeviceConfig mDisplayDeviceConfig; + + // Identifiers for suspend blocker acuisition requests + private final String mSuspendBlockerIdUnfinishedBusiness; + private final String mSuspendBlockerIdOnStateChanged; + private final String mSuspendBlockerIdProxPositive; + private final String mSuspendBlockerIdProxNegative; + private final String mSuspendBlockerIdProxDebounce; + + /** + * Creates the display power controller. + */ + DisplayPowerController2(Context context, Injector injector, + DisplayPowerCallbacks callbacks, Handler handler, + SensorManager sensorManager, DisplayBlanker blanker, LogicalDisplay logicalDisplay, + BrightnessTracker brightnessTracker, BrightnessSetting brightnessSetting, + Runnable onBrightnessChangeRunnable) { + + mInjector = injector != null ? injector : new Injector(); + mClock = mInjector.getClock(); + mLogicalDisplay = logicalDisplay; + mDisplayId = mLogicalDisplay.getDisplayIdLocked(); + mTag = "DisplayPowerController2[" + mDisplayId + "]"; + mSuspendBlockerIdUnfinishedBusiness = getSuspendBlockerUnfinishedBusinessId(mDisplayId); + mSuspendBlockerIdOnStateChanged = getSuspendBlockerOnStateChangedId(mDisplayId); + mSuspendBlockerIdProxPositive = getSuspendBlockerProxPositiveId(mDisplayId); + mSuspendBlockerIdProxNegative = getSuspendBlockerProxNegativeId(mDisplayId); + mSuspendBlockerIdProxDebounce = getSuspendBlockerProxDebounceId(mDisplayId); + + mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); + mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(); + mDisplayStatsId = mUniqueDisplayId.hashCode(); + mHandler = new DisplayControllerHandler(handler.getLooper()); + mLastBrightnessEvent = new BrightnessEvent(mDisplayId); + mTempBrightnessEvent = new BrightnessEvent(mDisplayId); + + if (mDisplayId == Display.DEFAULT_DISPLAY) { + mBatteryStats = BatteryStatsService.getService(); + } else { + mBatteryStats = null; + } + + mSettingsObserver = new SettingsObserver(mHandler); + mCallbacks = callbacks; + mSensorManager = sensorManager; + mWindowManagerPolicy = LocalServices.getService(WindowManagerPolicy.class); + mBlanker = blanker; + mContext = context; + mBrightnessTracker = brightnessTracker; + // TODO: b/186428377 update brightness setting when display changes + mBrightnessSetting = brightnessSetting; + mOnBrightnessChangeRunnable = onBrightnessChangeRunnable; + + PowerManager pm = context.getSystemService(PowerManager.class); + + final Resources resources = context.getResources(); + + // DOZE AND DIM SETTINGS + mScreenBrightnessDozeConfig = clampAbsoluteBrightness( + pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE)); + mScreenBrightnessDimConfig = clampAbsoluteBrightness( + pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM)); + mScreenBrightnessMinimumDimAmount = resources.getFloat( + R.dimen.config_screenBrightnessMinimumDimAmountFloat); + + + // NORMAL SCREEN SETTINGS + mScreenBrightnessDefault = clampAbsoluteBrightness( + mLogicalDisplay.getDisplayInfoLocked().brightnessDefault); + + // VR SETTINGS + mScreenBrightnessForVrDefault = clampAbsoluteBrightness( + pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DEFAULT_VR)); + mScreenBrightnessForVrRangeMaximum = clampAbsoluteBrightness( + pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM_VR)); + mScreenBrightnessForVrRangeMinimum = clampAbsoluteBrightness( + pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM_VR)); + + // Check the setting, but also verify that it is the default display. Only the default + // display has an automatic brightness controller running. + // TODO: b/179021925 - Fix to work with multiple displays + mUseSoftwareAutoBrightnessConfig = resources.getBoolean( + R.bool.config_automatic_brightness_available) + && mDisplayId == Display.DEFAULT_DISPLAY; + + mAllowAutoBrightnessWhileDozingConfig = resources.getBoolean( + R.bool.config_allowAutoBrightnessWhileDozing); + + mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked() + .getDisplayDeviceConfig(); + + loadBrightnessRampRates(); + mSkipScreenOnBrightnessRamp = resources.getBoolean( + R.bool.config_skipScreenOnBrightnessRamp); + + mHbmController = createHbmControllerLocked(); + + mBrightnessThrottler = createBrightnessThrottlerLocked(); + + // Seed the cached brightness + saveBrightnessInfo(getScreenBrightnessSetting()); + + DisplayWhiteBalanceSettings displayWhiteBalanceSettings = null; + DisplayWhiteBalanceController displayWhiteBalanceController = null; + if (mDisplayId == Display.DEFAULT_DISPLAY) { + try { + displayWhiteBalanceSettings = new DisplayWhiteBalanceSettings(mContext, mHandler); + displayWhiteBalanceController = DisplayWhiteBalanceFactory.create(mHandler, + mSensorManager, resources); + displayWhiteBalanceSettings.setCallbacks(this); + displayWhiteBalanceController.setCallbacks(this); + } catch (Exception e) { + Slog.e(mTag, "failed to set up display white-balance: " + e); + } + } + mDisplayWhiteBalanceSettings = displayWhiteBalanceSettings; + mDisplayWhiteBalanceController = displayWhiteBalanceController; + + loadNitsRange(resources); + + if (mDisplayId == Display.DEFAULT_DISPLAY) { + mCdsi = LocalServices.getService(ColorDisplayServiceInternal.class); + boolean active = mCdsi.setReduceBrightColorsListener(new ReduceBrightColorsListener() { + @Override + public void onReduceBrightColorsActivationChanged(boolean activated, + boolean userInitiated) { + applyReduceBrightColorsSplineAdjustment(); + + } + + @Override + public void onReduceBrightColorsStrengthChanged(int strength) { + applyReduceBrightColorsSplineAdjustment(); + } + }); + if (active) { + applyReduceBrightColorsSplineAdjustment(); + } + } else { + mCdsi = null; + } + + setUpAutoBrightness(resources, handler); + + mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic(); + mColorFadeFadesConfig = resources.getBoolean( + R.bool.config_animateScreenLights); + + mDisplayBlanksAfterDozeConfig = resources.getBoolean( + R.bool.config_displayBlanksAfterDoze); + + mBrightnessBucketsInDozeConfig = resources.getBoolean( + R.bool.config_displayBrightnessBucketsInDoze); + + loadProximitySensor(); + + mCurrentScreenBrightnessSetting = getScreenBrightnessSetting(); + mScreenBrightnessForVr = getScreenBrightnessForVrSetting(); + mAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting(); + mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mTemporaryAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mPendingAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT; + + } + + private void applyReduceBrightColorsSplineAdjustment() { + mHandler.obtainMessage(MSG_UPDATE_RBC).sendToTarget(); + sendUpdatePowerState(); + } + + private void handleRbcChanged() { + if (mAutomaticBrightnessController == null) { + return; + } + if ((!mAutomaticBrightnessController.isInIdleMode() + && mInteractiveModeBrightnessMapper == null) + || (mAutomaticBrightnessController.isInIdleMode() + && mIdleModeBrightnessMapper == null)) { + Log.w(mTag, "No brightness mapping available to recalculate splines for this mode"); + return; + } + + float[] adjustedNits = new float[mNitsRange.length]; + for (int i = 0; i < mNitsRange.length; i++) { + adjustedNits[i] = mCdsi.getReduceBrightColorsAdjustedBrightnessNits(mNitsRange[i]); + } + mIsRbcActive = mCdsi.isReduceBrightColorsActivated(); + mAutomaticBrightnessController.recalculateSplines(mIsRbcActive, adjustedNits); + } + + /** + * Returns true if the proximity sensor screen-off function is available. + */ + @Override + public boolean isProximitySensorAvailable() { + return mProximitySensor != null; + } + + /** + * Get the {@link BrightnessChangeEvent}s for the specified user. + * + * @param userId userId to fetch data for + * @param includePackage if false will null out the package name in events + */ + @Nullable + @Override + public ParceledListSlice<BrightnessChangeEvent> getBrightnessEvents( + @UserIdInt int userId, boolean includePackage) { + if (mBrightnessTracker == null) { + return null; + } + return mBrightnessTracker.getEvents(userId, includePackage); + } + + @Override + public void onSwitchUser(@UserIdInt int newUserId) { + handleSettingsChange(true /* userSwitch */); + if (mBrightnessTracker != null) { + mBrightnessTracker.onSwitchUser(newUserId); + } + } + + @Nullable + @Override + public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats( + @UserIdInt int userId) { + if (mBrightnessTracker == null) { + return null; + } + return mBrightnessTracker.getAmbientBrightnessStats(userId); + } + + /** + * Persist the brightness slider events and ambient brightness stats to disk. + */ + @Override + public void persistBrightnessTrackerState() { + if (mBrightnessTracker != null) { + mBrightnessTracker.persistBrightnessTrackerState(); + } + } + + /** + * Requests a new power state. + * The controller makes a copy of the provided object and then + * begins adjusting the power state to match what was requested. + * + * @param request The requested power state. + * @param waitForNegativeProximity If true, issues a request to wait for + * negative proximity before turning the screen back on, + * assuming the screen + * was turned off by the proximity sensor. + * @return True if display is ready, false if there are important changes that must + * be made asynchronously (such as turning the screen on), in which case the caller + * should grab a wake lock, watch for {@link DisplayPowerCallbacks#onStateChanged()} + * then try the request again later until the state converges. + */ + public boolean requestPowerState(DisplayPowerRequest request, + boolean waitForNegativeProximity) { + if (DEBUG) { + Slog.d(mTag, "requestPowerState: " + + request + ", waitForNegativeProximity=" + waitForNegativeProximity); + } + + synchronized (mLock) { + if (mStopped) { + return true; + } + + boolean changed = false; + + if (waitForNegativeProximity + && !mPendingWaitForNegativeProximityLocked) { + mPendingWaitForNegativeProximityLocked = true; + changed = true; + } + + if (mPendingRequestLocked == null) { + mPendingRequestLocked = new DisplayPowerRequest(request); + changed = true; + } else if (!mPendingRequestLocked.equals(request)) { + mPendingRequestLocked.copyFrom(request); + changed = true; + } + + if (changed) { + mDisplayReadyLocked = false; + if (!mPendingRequestChangedLocked) { + mPendingRequestChangedLocked = true; + sendUpdatePowerStateLocked(); + } + } + + return mDisplayReadyLocked; + } + } + + @Override + public BrightnessConfiguration getDefaultBrightnessConfiguration() { + if (mAutomaticBrightnessController == null) { + return null; + } + return mAutomaticBrightnessController.getDefaultConfig(); + } + + /** + * Notified when the display is changed. We use this to apply any changes that might be needed + * when displays get swapped on foldable devices. For example, different brightness properties + * of each display need to be properly reflected in AutomaticBrightnessController. + * + * Make sure DisplayManagerService.mSyncRoot lock is held when this is called + */ + @Override + public void onDisplayChanged() { + final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); + if (device == null) { + Slog.wtf(mTag, "Display Device is null in DisplayPowerController2 for display: " + + mLogicalDisplay.getDisplayIdLocked()); + return; + } + + final String uniqueId = device.getUniqueId(); + final DisplayDeviceConfig config = device.getDisplayDeviceConfig(); + final IBinder token = device.getDisplayTokenLocked(); + final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); + mHandler.post(() -> { + if (mDisplayDevice != device) { + mDisplayDevice = device; + mUniqueDisplayId = uniqueId; + mDisplayStatsId = mUniqueDisplayId.hashCode(); + mDisplayDeviceConfig = config; + loadFromDisplayDeviceConfig(token, info); + updatePowerState(); + } + }); + } + + /** + * Called when the displays are preparing to transition from one device state to another. + * This process involves turning off some displays so we need updatePowerState() to run and + * calculate the new state. + */ + @Override + public void onDeviceStateTransition() { + sendUpdatePowerState(); + } + + /** + * Unregisters all listeners and interrupts all running threads; halting future work. + * + * This method should be called when the DisplayPowerController2 is no longer in use; i.e. when + * the {@link #mDisplayId display} has been removed. + */ + @Override + public void stop() { + synchronized (mLock) { + mStopped = true; + Message msg = mHandler.obtainMessage(MSG_STOP); + mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()); + + if (mDisplayWhiteBalanceController != null) { + mDisplayWhiteBalanceController.setEnabled(false); + } + + if (mAutomaticBrightnessController != null) { + mAutomaticBrightnessController.stop(); + } + + if (mBrightnessSetting != null) { + mBrightnessSetting.unregisterListener(mBrightnessSettingListener); + } + + mContext.getContentResolver().unregisterContentObserver(mSettingsObserver); + } + } + + private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info) { + // All properties that depend on the associated DisplayDevice and the DDC must be + // updated here. + loadBrightnessRampRates(); + loadProximitySensor(); + loadNitsRange(mContext.getResources()); + setUpAutoBrightness(mContext.getResources(), mHandler); + reloadReduceBrightColours(); + if (mScreenBrightnessRampAnimator != null) { + mScreenBrightnessRampAnimator.setAnimationTimeLimits( + mBrightnessRampIncreaseMaxTimeMillis, + mBrightnessRampDecreaseMaxTimeMillis); + } + mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId, + mDisplayDeviceConfig.getHighBrightnessModeData(), + new HighBrightnessModeController.HdrBrightnessDeviceConfig() { + @Override + public float getHdrBrightnessFromSdr(float sdrBrightness) { + return mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness); + } + }); + mBrightnessThrottler.resetThrottlingData( + mDisplayDeviceConfig.getBrightnessThrottlingData(), mUniqueDisplayId); + } + + private void sendUpdatePowerState() { + synchronized (mLock) { + sendUpdatePowerStateLocked(); + } + } + + @GuardedBy("mLock") + private void sendUpdatePowerStateLocked() { + if (!mStopped && !mPendingUpdatePowerStateLocked) { + mPendingUpdatePowerStateLocked = true; + Message msg = mHandler.obtainMessage(MSG_UPDATE_POWER_STATE); + mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()); + } + } + + private void initialize(int displayState) { + mPowerState = mInjector.getDisplayPowerState(mBlanker, + mColorFadeEnabled ? new ColorFade(mDisplayId) : null, mDisplayId, displayState); + + if (mColorFadeEnabled) { + mColorFadeOnAnimator = ObjectAnimator.ofFloat( + mPowerState, DisplayPowerState.COLOR_FADE_LEVEL, 0.0f, 1.0f); + mColorFadeOnAnimator.setDuration(COLOR_FADE_ON_ANIMATION_DURATION_MILLIS); + mColorFadeOnAnimator.addListener(mAnimatorListener); + + mColorFadeOffAnimator = ObjectAnimator.ofFloat( + mPowerState, DisplayPowerState.COLOR_FADE_LEVEL, 1.0f, 0.0f); + mColorFadeOffAnimator.setDuration(COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS); + mColorFadeOffAnimator.addListener(mAnimatorListener); + } + + mScreenBrightnessRampAnimator = mInjector.getDualRampAnimator(mPowerState, + DisplayPowerState.SCREEN_BRIGHTNESS_FLOAT, + DisplayPowerState.SCREEN_SDR_BRIGHTNESS_FLOAT); + mScreenBrightnessRampAnimator.setAnimationTimeLimits( + mBrightnessRampIncreaseMaxTimeMillis, + mBrightnessRampDecreaseMaxTimeMillis); + mScreenBrightnessRampAnimator.setListener(mRampAnimatorListener); + + noteScreenState(mPowerState.getScreenState()); + noteScreenBrightness(mPowerState.getScreenBrightness()); + + // Initialize all of the brightness tracking state + final float brightness = convertToNits(mPowerState.getScreenBrightness()); + if (brightness >= PowerManager.BRIGHTNESS_MIN) { + mBrightnessTracker.start(brightness); + } + mBrightnessSettingListener = brightnessValue -> { + Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue); + mHandler.sendMessage(msg); + }; + + mBrightnessSetting.registerListener(mBrightnessSettingListener); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_FOR_VR_FLOAT), + false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ), + false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL); + } + + private void setUpAutoBrightness(Resources resources, Handler handler) { + if (!mUseSoftwareAutoBrightnessConfig) { + return; + } + + final boolean isIdleScreenBrightnessEnabled = resources.getBoolean( + R.bool.config_enableIdleScreenBrightnessMode); + mInteractiveModeBrightnessMapper = BrightnessMappingStrategy.create(resources, + mDisplayDeviceConfig, mDisplayWhiteBalanceController); + if (isIdleScreenBrightnessEnabled) { + mIdleModeBrightnessMapper = BrightnessMappingStrategy.createForIdleMode(resources, + mDisplayDeviceConfig, mDisplayWhiteBalanceController); + } + + if (mInteractiveModeBrightnessMapper != null) { + final float dozeScaleFactor = resources.getFraction( + R.fraction.config_screenAutoBrightnessDozeScaleFactor, + 1, 1); + + int[] ambientBrighteningThresholds = resources.getIntArray( + R.array.config_ambientBrighteningThresholds); + int[] ambientDarkeningThresholds = resources.getIntArray( + R.array.config_ambientDarkeningThresholds); + int[] ambientThresholdLevels = resources.getIntArray( + R.array.config_ambientThresholdLevels); + float ambientDarkeningMinThreshold = + mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(); + float ambientBrighteningMinThreshold = + mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(); + HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels( + ambientBrighteningThresholds, ambientDarkeningThresholds, + ambientThresholdLevels, ambientDarkeningMinThreshold, + ambientBrighteningMinThreshold); + + int[] screenBrighteningThresholds = resources.getIntArray( + R.array.config_screenBrighteningThresholds); + int[] screenDarkeningThresholds = resources.getIntArray( + R.array.config_screenDarkeningThresholds); + int[] screenThresholdLevels = resources.getIntArray( + R.array.config_screenThresholdLevels); + float screenDarkeningMinThreshold = + mDisplayDeviceConfig.getScreenDarkeningMinThreshold(); + float screenBrighteningMinThreshold = + mDisplayDeviceConfig.getScreenBrighteningMinThreshold(); + HysteresisLevels screenBrightnessThresholds = new HysteresisLevels( + screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels, + screenDarkeningMinThreshold, screenBrighteningMinThreshold); + + // Idle screen thresholds + float screenDarkeningMinThresholdIdle = + mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(); + float screenBrighteningMinThresholdIdle = + mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(); + HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels( + screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels, + screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle); + + // Idle ambient thresholds + float ambientDarkeningMinThresholdIdle = + mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(); + float ambientBrighteningMinThresholdIdle = + mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(); + HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels( + ambientBrighteningThresholds, ambientDarkeningThresholds, + ambientThresholdLevels, ambientDarkeningMinThresholdIdle, + ambientBrighteningMinThresholdIdle); + + long brighteningLightDebounce = mDisplayDeviceConfig + .getAutoBrightnessBrighteningLightDebounce(); + long darkeningLightDebounce = mDisplayDeviceConfig + .getAutoBrightnessDarkeningLightDebounce(); + boolean autoBrightnessResetAmbientLuxAfterWarmUp = resources.getBoolean( + R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp); + + int lightSensorWarmUpTimeConfig = resources.getInteger( + R.integer.config_lightSensorWarmupTime); + int lightSensorRate = resources.getInteger( + R.integer.config_autoBrightnessLightSensorRate); + int initialLightSensorRate = resources.getInteger( + R.integer.config_autoBrightnessInitialLightSensorRate); + if (initialLightSensorRate == -1) { + initialLightSensorRate = lightSensorRate; + } else if (initialLightSensorRate > lightSensorRate) { + Slog.w(mTag, "Expected config_autoBrightnessInitialLightSensorRate (" + + initialLightSensorRate + ") to be less than or equal to " + + "config_autoBrightnessLightSensorRate (" + lightSensorRate + ")."); + } + + loadAmbientLightSensor(); + if (mBrightnessTracker != null) { + mBrightnessTracker.setLightSensor(mLightSensor); + } + + if (mAutomaticBrightnessController != null) { + mAutomaticBrightnessController.stop(); + } + mAutomaticBrightnessController = new AutomaticBrightnessController(this, + handler.getLooper(), mSensorManager, mLightSensor, + mInteractiveModeBrightnessMapper, lightSensorWarmUpTimeConfig, + PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, dozeScaleFactor, + lightSensorRate, initialLightSensorRate, brighteningLightDebounce, + darkeningLightDebounce, autoBrightnessResetAmbientLuxAfterWarmUp, + ambientBrightnessThresholds, screenBrightnessThresholds, + ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, mContext, + mHbmController, mBrightnessThrottler, mIdleModeBrightnessMapper, + mDisplayDeviceConfig.getAmbientHorizonShort(), + mDisplayDeviceConfig.getAmbientHorizonLong()); + + mBrightnessEventRingBuffer = + new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX); + } else { + mUseSoftwareAutoBrightnessConfig = false; + } + } + + private void loadBrightnessRampRates() { + mBrightnessRampRateFastDecrease = mDisplayDeviceConfig.getBrightnessRampFastDecrease(); + mBrightnessRampRateFastIncrease = mDisplayDeviceConfig.getBrightnessRampFastIncrease(); + mBrightnessRampRateSlowDecrease = mDisplayDeviceConfig.getBrightnessRampSlowDecrease(); + mBrightnessRampRateSlowIncrease = mDisplayDeviceConfig.getBrightnessRampSlowIncrease(); + mBrightnessRampDecreaseMaxTimeMillis = + mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(); + mBrightnessRampIncreaseMaxTimeMillis = + mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(); + } + + private void loadNitsRange(Resources resources) { + if (mDisplayDeviceConfig != null && mDisplayDeviceConfig.getNits() != null) { + mNitsRange = mDisplayDeviceConfig.getNits(); + } else { + Slog.w(mTag, "Screen brightness nits configuration is unavailable; falling back"); + mNitsRange = BrightnessMappingStrategy.getFloatArray(resources + .obtainTypedArray(R.array.config_screenBrightnessNits)); + } + } + + private void reloadReduceBrightColours() { + if (mCdsi != null && mCdsi.isReduceBrightColorsActivated()) { + applyReduceBrightColorsSplineAdjustment(); + } + } + + @Override + public void setAutomaticScreenBrightnessMode(boolean isIdle) { + if (mAutomaticBrightnessController != null) { + if (isIdle) { + mAutomaticBrightnessController.switchToIdleMode(); + } else { + mAutomaticBrightnessController.switchToInteractiveScreenBrightnessMode(); + } + } + if (mDisplayWhiteBalanceController != null) { + mDisplayWhiteBalanceController.setStrongModeEnabled(isIdle); + } + } + + private final Animator.AnimatorListener mAnimatorListener = new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + sendUpdatePowerState(); + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + @Override + public void onAnimationCancel(Animator animation) { + } + }; + + private final RampAnimator.Listener mRampAnimatorListener = new RampAnimator.Listener() { + @Override + public void onAnimationEnd() { + sendUpdatePowerState(); + Message msg = mHandler.obtainMessage(MSG_BRIGHTNESS_RAMP_DONE); + mHandler.sendMessage(msg); + } + }; + + /** Clean up all resources that are accessed via the {@link #mHandler} thread. */ + private void cleanupHandlerThreadAfterStop() { + setProximitySensorEnabled(false); + mHbmController.stop(); + mBrightnessThrottler.stop(); + mHandler.removeCallbacksAndMessages(null); + + // Release any outstanding wakelocks we're still holding because of pending messages. + if (mUnfinishedBusiness) { + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness); + mUnfinishedBusiness = false; + } + if (mOnStateChangedPending) { + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged); + mOnStateChangedPending = false; + } + for (int i = 0; i < mOnProximityPositiveMessages; i++) { + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive); + } + mOnProximityPositiveMessages = 0; + for (int i = 0; i < mOnProximityNegativeMessages; i++) { + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative); + } + mOnProximityNegativeMessages = 0; + + final float brightness = mPowerState != null + ? mPowerState.getScreenBrightness() + : PowerManager.BRIGHTNESS_MIN; + reportStats(brightness); + + if (mPowerState != null) { + mPowerState.stop(); + mPowerState = null; + } + } + + private void updatePowerState() { + if (DEBUG) { + Trace.beginSection("DisplayPowerController#updatePowerState"); + } + updatePowerStateInternal(); + if (DEBUG) { + Trace.endSection(); + } + } + + private void updatePowerStateInternal() { + // Update the power state request. + final boolean mustNotify; + final int previousPolicy; + boolean mustInitialize = false; + int brightnessAdjustmentFlags = 0; + mBrightnessReasonTemp.set(null); + mTempBrightnessEvent.reset(); + synchronized (mLock) { + if (mStopped) { + return; + } + mPendingUpdatePowerStateLocked = false; + if (mPendingRequestLocked == null) { + return; // wait until first actual power request + } + + if (mPowerRequest == null) { + mPowerRequest = new DisplayPowerRequest(mPendingRequestLocked); + updatePendingProximityRequestsLocked(); + mPendingRequestChangedLocked = false; + mustInitialize = true; + // Assume we're on and bright until told otherwise, since that's the state we turn + // on in. + previousPolicy = DisplayPowerRequest.POLICY_BRIGHT; + } else if (mPendingRequestChangedLocked) { + previousPolicy = mPowerRequest.policy; + mPowerRequest.copyFrom(mPendingRequestLocked); + updatePendingProximityRequestsLocked(); + mPendingRequestChangedLocked = false; + mDisplayReadyLocked = false; + } else { + previousPolicy = mPowerRequest.policy; + } + + mustNotify = !mDisplayReadyLocked; + } + + // Compute the basic display state using the policy. + // We might override this below based on other factors. + // Initialise brightness as invalid. + int state; + float brightnessState = PowerManager.BRIGHTNESS_INVALID_FLOAT; + boolean performScreenOffTransition = false; + switch (mPowerRequest.policy) { + case DisplayPowerRequest.POLICY_OFF: + state = Display.STATE_OFF; + performScreenOffTransition = true; + break; + case DisplayPowerRequest.POLICY_DOZE: + if (mPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) { + state = mPowerRequest.dozeScreenState; + } else { + state = Display.STATE_DOZE; + } + if (!mAllowAutoBrightnessWhileDozingConfig) { + brightnessState = mPowerRequest.dozeScreenBrightness; + mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE); + } + break; + case DisplayPowerRequest.POLICY_VR: + state = Display.STATE_VR; + break; + case DisplayPowerRequest.POLICY_DIM: + case DisplayPowerRequest.POLICY_BRIGHT: + default: + state = Display.STATE_ON; + break; + } + assert (state != Display.STATE_UNKNOWN); + + boolean skipRampBecauseOfProximityChangeToNegative = false; + // Apply the proximity sensor. + if (mProximitySensor != null) { + if (mPowerRequest.useProximitySensor && state != Display.STATE_OFF) { + // At this point the policy says that the screen should be on, but we've been + // asked to listen to the prox sensor to adjust the display state, so lets make + // sure the sensor is on. + setProximitySensorEnabled(true); + if (!mScreenOffBecauseOfProximity + && mProximity == PROXIMITY_POSITIVE + && !mIgnoreProximityUntilChanged) { + // Prox sensor already reporting "near" so we should turn off the screen. + // Also checked that we aren't currently set to ignore the proximity sensor + // temporarily. + mScreenOffBecauseOfProximity = true; + sendOnProximityPositiveWithWakelock(); + } + } else if (mWaitingForNegativeProximity + && mScreenOffBecauseOfProximity + && mProximity == PROXIMITY_POSITIVE + && state != Display.STATE_OFF) { + // The policy says that we should have the screen on, but it's off due to the prox + // and we've been asked to wait until the screen is far from the user to turn it + // back on. Let keep the prox sensor on so we can tell when it's far again. + setProximitySensorEnabled(true); + } else { + // We haven't been asked to use the prox sensor and we're not waiting on the screen + // to turn back on...so lets shut down the prox sensor. + setProximitySensorEnabled(false); + mWaitingForNegativeProximity = false; + } + + if (mScreenOffBecauseOfProximity + && (mProximity != PROXIMITY_POSITIVE || mIgnoreProximityUntilChanged)) { + // The screen *was* off due to prox being near, but now it's "far" so lets turn + // the screen back on. Also turn it back on if we've been asked to ignore the + // prox sensor temporarily. + mScreenOffBecauseOfProximity = false; + skipRampBecauseOfProximityChangeToNegative = true; + sendOnProximityNegativeWithWakelock(); + } + } else { + mWaitingForNegativeProximity = false; + mIgnoreProximityUntilChanged = false; + } + + if (!mLogicalDisplay.isEnabled() + || mLogicalDisplay.getPhase() == LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION + || mScreenOffBecauseOfProximity) { + state = Display.STATE_OFF; + } + + // Initialize things the first time the power state is changed. + if (mustInitialize) { + initialize(state); + } + + // Animate the screen state change unless already animating. + // The transition may be deferred, so after this point we will use the + // actual state instead of the desired one. + final int oldState = mPowerState.getScreenState(); + animateScreenStateChange(state, performScreenOffTransition); + state = mPowerState.getScreenState(); + + if (state == Display.STATE_OFF) { + brightnessState = PowerManager.BRIGHTNESS_OFF_FLOAT; + mBrightnessReasonTemp.setReason(BrightnessReason.REASON_SCREEN_OFF); + } + + // Always use the VR brightness when in the VR state. + if (state == Display.STATE_VR) { + brightnessState = mScreenBrightnessForVr; + mBrightnessReasonTemp.setReason(BrightnessReason.REASON_VR); + } + + if ((Float.isNaN(brightnessState)) + && isValidBrightnessValue(mPowerRequest.screenBrightnessOverride)) { + brightnessState = mPowerRequest.screenBrightnessOverride; + mBrightnessReasonTemp.setReason(BrightnessReason.REASON_OVERRIDE); + mAppliedScreenBrightnessOverride = true; + } else { + mAppliedScreenBrightnessOverride = false; + } + + final boolean autoBrightnessEnabledInDoze = + mAllowAutoBrightnessWhileDozingConfig && Display.isDozeState(state); + final boolean autoBrightnessEnabled = mPowerRequest.useAutoBrightness + && (state == Display.STATE_ON || autoBrightnessEnabledInDoze) + && Float.isNaN(brightnessState) + && mAutomaticBrightnessController != null; + final boolean autoBrightnessDisabledDueToDisplayOff = mPowerRequest.useAutoBrightness + && !(state == Display.STATE_ON || autoBrightnessEnabledInDoze); + final int autoBrightnessState = autoBrightnessEnabled + ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED + : autoBrightnessDisabledDueToDisplayOff + ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE + : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED; + + final boolean userSetBrightnessChanged = updateUserSetScreenBrightness(); + + // Use the temporary screen brightness if there isn't an override, either from + // WindowManager or based on the display state. + if (isValidBrightnessValue(mTemporaryScreenBrightness)) { + brightnessState = mTemporaryScreenBrightness; + mAppliedTemporaryBrightness = true; + mBrightnessReasonTemp.setReason(BrightnessReason.REASON_TEMPORARY); + } else { + mAppliedTemporaryBrightness = false; + } + + final boolean autoBrightnessAdjustmentChanged = updateAutoBrightnessAdjustment(); + + // Use the autobrightness adjustment override if set. + final float autoBrightnessAdjustment; + if (!Float.isNaN(mTemporaryAutoBrightnessAdjustment)) { + autoBrightnessAdjustment = mTemporaryAutoBrightnessAdjustment; + brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO_TEMP; + mAppliedTemporaryAutoBrightnessAdjustment = true; + } else { + autoBrightnessAdjustment = mAutoBrightnessAdjustment; + brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO; + mAppliedTemporaryAutoBrightnessAdjustment = false; + } + // Apply brightness boost. + // We do this here after deciding whether auto-brightness is enabled so that we don't + // disable the light sensor during this temporary state. That way when boost ends we will + // be able to resume normal auto-brightness behavior without any delay. + if (mPowerRequest.boostScreenBrightness + && brightnessState != PowerManager.BRIGHTNESS_OFF_FLOAT) { + brightnessState = PowerManager.BRIGHTNESS_MAX; + mBrightnessReasonTemp.setReason(BrightnessReason.REASON_BOOST); + mAppliedBrightnessBoost = true; + } else { + mAppliedBrightnessBoost = false; + } + + // If the brightness is already set then it's been overridden by something other than the + // user, or is a temporary adjustment. + boolean userInitiatedChange = (Float.isNaN(brightnessState)) + && (autoBrightnessAdjustmentChanged || userSetBrightnessChanged); + boolean hadUserBrightnessPoint = false; + // Configure auto-brightness. + if (mAutomaticBrightnessController != null) { + hadUserBrightnessPoint = mAutomaticBrightnessController.hasUserDataPoints(); + mAutomaticBrightnessController.configure(autoBrightnessState, + mBrightnessConfiguration, + mLastUserSetScreenBrightness, + userSetBrightnessChanged, autoBrightnessAdjustment, + autoBrightnessAdjustmentChanged, mPowerRequest.policy); + } + + if (mBrightnessTracker != null) { + mBrightnessTracker.setBrightnessConfiguration(mBrightnessConfiguration); + } + + boolean updateScreenBrightnessSetting = false; + + // Apply auto-brightness. + boolean slowChange = false; + if (Float.isNaN(brightnessState)) { + float newAutoBrightnessAdjustment = autoBrightnessAdjustment; + if (autoBrightnessEnabled) { + brightnessState = mAutomaticBrightnessController.getAutomaticScreenBrightness( + mTempBrightnessEvent); + newAutoBrightnessAdjustment = + mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment(); + } + if (isValidBrightnessValue(brightnessState) + || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT) { + // Use current auto-brightness value and slowly adjust to changes. + brightnessState = clampScreenBrightness(brightnessState); + if (mAppliedAutoBrightness && !autoBrightnessAdjustmentChanged) { + slowChange = true; // slowly adapt to auto-brightness + } + updateScreenBrightnessSetting = mCurrentScreenBrightnessSetting != brightnessState; + mAppliedAutoBrightness = true; + mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC); + } else { + mAppliedAutoBrightness = false; + } + if (autoBrightnessAdjustment != newAutoBrightnessAdjustment) { + // If the autobrightness controller has decided to change the adjustment value + // used, make sure that's reflected in settings. + putAutoBrightnessAdjustmentSetting(newAutoBrightnessAdjustment); + } else { + // Adjustment values resulted in no change + brightnessAdjustmentFlags = 0; + } + } else { + // Any non-auto-brightness values such as override or temporary should still be subject + // to clamping so that they don't go beyond the current max as specified by HBM + // Controller. + brightnessState = clampScreenBrightness(brightnessState); + mAppliedAutoBrightness = false; + brightnessAdjustmentFlags = 0; + } + + // Use default brightness when dozing unless overridden. + if ((Float.isNaN(brightnessState)) + && Display.isDozeState(state)) { + brightnessState = clampScreenBrightness(mScreenBrightnessDozeConfig); + mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT); + } + + // Apply manual brightness. + if (Float.isNaN(brightnessState)) { + brightnessState = clampScreenBrightness(mCurrentScreenBrightnessSetting); + if (brightnessState != mCurrentScreenBrightnessSetting) { + // The manually chosen screen brightness is outside of the currently allowed + // range (i.e., high-brightness-mode), make sure we tell the rest of the system + // by updating the setting. + updateScreenBrightnessSetting = true; + } + mBrightnessReasonTemp.setReason(BrightnessReason.REASON_MANUAL); + } + + // Now that a desired brightness has been calculated, apply brightness throttling. The + // dimming and low power transformations that follow can only dim brightness further. + // + // We didn't do this earlier through brightness clamping because we need to know both + // unthrottled (unclamped/ideal) and throttled brightness levels for subsequent operations. + // Note throttling effectively changes the allowed brightness range, so, similarly to HBM, + // we broadcast this change through setting. + final float unthrottledBrightnessState = brightnessState; + if (mBrightnessThrottler.isThrottled()) { + mTempBrightnessEvent.setThermalMax(mBrightnessThrottler.getBrightnessCap()); + brightnessState = Math.min(brightnessState, mBrightnessThrottler.getBrightnessCap()); + mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_THROTTLED); + if (!mAppliedThrottling) { + // Brightness throttling is needed, so do so quickly. + // Later, when throttling is removed, we let other mechanisms decide on speed. + slowChange = false; + } + mAppliedThrottling = true; + } else if (mAppliedThrottling) { + mAppliedThrottling = false; + } + + if (updateScreenBrightnessSetting) { + // Tell the rest of the system about the new brightness in case we had to change it + // for things like auto-brightness or high-brightness-mode. Note that we do this + // before applying the low power or dim transformations so that the slider + // accurately represents the full possible range, even if they range changes what + // it means in absolute terms. + updateScreenBrightnessSetting(brightnessState); + } + + // Apply dimming by at least some minimum amount when user activity + // timeout is about to expire. + if (mPowerRequest.policy == DisplayPowerRequest.POLICY_DIM) { + if (brightnessState > PowerManager.BRIGHTNESS_MIN) { + brightnessState = Math.max( + Math.min(brightnessState - mScreenBrightnessMinimumDimAmount, + mScreenBrightnessDimConfig), + PowerManager.BRIGHTNESS_MIN); + mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_DIMMED); + } + if (!mAppliedDimming) { + slowChange = false; + } + mAppliedDimming = true; + } else if (mAppliedDimming) { + slowChange = false; + mAppliedDimming = false; + } + // If low power mode is enabled, scale brightness by screenLowPowerBrightnessFactor + // as long as it is above the minimum threshold. + if (mPowerRequest.lowPowerMode) { + if (brightnessState > PowerManager.BRIGHTNESS_MIN) { + final float brightnessFactor = + Math.min(mPowerRequest.screenLowPowerBrightnessFactor, 1); + final float lowPowerBrightnessFloat = (brightnessState * brightnessFactor); + brightnessState = Math.max(lowPowerBrightnessFloat, PowerManager.BRIGHTNESS_MIN); + mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_LOW_POWER); + } + if (!mAppliedLowPower) { + slowChange = false; + } + mAppliedLowPower = true; + } else if (mAppliedLowPower) { + slowChange = false; + mAppliedLowPower = false; + } + + // The current brightness to use has been calculated at this point, and HbmController should + // be notified so that it can accurately calculate HDR or HBM levels. We specifically do it + // here instead of having HbmController listen to the brightness setting because certain + // brightness sources (such as an app override) are not saved to the setting, but should be + // reflected in HBM calculations. + mHbmController.onBrightnessChanged(brightnessState, unthrottledBrightnessState, + mBrightnessThrottler.getBrightnessMaxReason()); + + // Animate the screen brightness when the screen is on or dozing. + // Skip the animation when the screen is off or suspended or transition to/from VR. + boolean brightnessAdjusted = false; + final boolean brightnessIsTemporary = + mAppliedTemporaryBrightness || mAppliedTemporaryAutoBrightnessAdjustment; + if (!mPendingScreenOff) { + if (mSkipScreenOnBrightnessRamp) { + if (state == Display.STATE_ON) { + if (mSkipRampState == RAMP_STATE_SKIP_NONE && mDozing) { + mInitialAutoBrightness = brightnessState; + mSkipRampState = RAMP_STATE_SKIP_INITIAL; + } else if (mSkipRampState == RAMP_STATE_SKIP_INITIAL + && mUseSoftwareAutoBrightnessConfig + && !BrightnessSynchronizer.floatEquals(brightnessState, + mInitialAutoBrightness)) { + mSkipRampState = RAMP_STATE_SKIP_AUTOBRIGHT; + } else if (mSkipRampState == RAMP_STATE_SKIP_AUTOBRIGHT) { + mSkipRampState = RAMP_STATE_SKIP_NONE; + } + } else { + mSkipRampState = RAMP_STATE_SKIP_NONE; + } + } + + final boolean wasOrWillBeInVr = + (state == Display.STATE_VR || oldState == Display.STATE_VR); + final boolean initialRampSkip = (state == Display.STATE_ON && mSkipRampState + != RAMP_STATE_SKIP_NONE) || skipRampBecauseOfProximityChangeToNegative; + // While dozing, sometimes the brightness is split into buckets. Rather than animating + // through the buckets, which is unlikely to be smooth in the first place, just jump + // right to the suggested brightness. + final boolean hasBrightnessBuckets = + Display.isDozeState(state) && mBrightnessBucketsInDozeConfig; + // If the color fade is totally covering the screen then we can change the backlight + // level without it being a noticeable jump since any actual content isn't yet visible. + final boolean isDisplayContentVisible = + mColorFadeEnabled && mPowerState.getColorFadeLevel() == 1.0f; + // We only want to animate the brightness if it is between 0.0f and 1.0f. + // brightnessState can contain the values -1.0f and NaN, which we do not want to + // animate to. To avoid this, we check the value first. + // If the brightnessState is off (-1.0f) we still want to animate to the minimum + // brightness (0.0f) to accommodate for LED displays, which can appear bright to the + // user even when the display is all black. We also clamp here in case some + // transformations to the brightness have pushed it outside of the currently + // allowed range. + float animateValue = clampScreenBrightness(brightnessState); + + // If there are any HDR layers on the screen, we have a special brightness value that we + // use instead. We still preserve the calculated brightness for Standard Dynamic Range + // (SDR) layers, but the main brightness value will be the one for HDR. + float sdrAnimateValue = animateValue; + // TODO(b/216365040): The decision to prevent HBM for HDR in low power mode should be + // done in HighBrightnessModeController. + if (mHbmController.getHighBrightnessMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR + && ((mBrightnessReason.getModifier() & BrightnessReason.MODIFIER_DIMMED) == 0 + || (mBrightnessReason.getModifier() & BrightnessReason.MODIFIER_LOW_POWER) + == 0)) { + // We want to scale HDR brightness level with the SDR level + animateValue = mHbmController.getHdrBrightnessValue(); + } + + final float currentBrightness = mPowerState.getScreenBrightness(); + final float currentSdrBrightness = mPowerState.getSdrScreenBrightness(); + if (isValidBrightnessValue(animateValue) + && (animateValue != currentBrightness + || sdrAnimateValue != currentSdrBrightness)) { + if (initialRampSkip || hasBrightnessBuckets + || wasOrWillBeInVr || !isDisplayContentVisible || brightnessIsTemporary) { + animateScreenBrightness(animateValue, sdrAnimateValue, + SCREEN_ANIMATION_RATE_MINIMUM); + } else { + boolean isIncreasing = animateValue > currentBrightness; + final float rampSpeed; + if (isIncreasing && slowChange) { + rampSpeed = mBrightnessRampRateSlowIncrease; + } else if (isIncreasing && !slowChange) { + rampSpeed = mBrightnessRampRateFastIncrease; + } else if (!isIncreasing && slowChange) { + rampSpeed = mBrightnessRampRateSlowDecrease; + } else { + rampSpeed = mBrightnessRampRateFastDecrease; + } + animateScreenBrightness(animateValue, sdrAnimateValue, rampSpeed); + } + } + + // Report brightness to brightnesstracker: + // If brightness is not temporary (ie the slider has been released) + // AND if we are not in idle screen brightness mode. + if (!brightnessIsTemporary + && (mAutomaticBrightnessController != null + && !mAutomaticBrightnessController.isInIdleMode())) { + if (userInitiatedChange && (mAutomaticBrightnessController == null + || !mAutomaticBrightnessController.hasValidAmbientLux())) { + // If we don't have a valid lux reading we can't report a valid + // slider event so notify as if the system changed the brightness. + userInitiatedChange = false; + } + notifyBrightnessTrackerChanged(brightnessState, userInitiatedChange, + hadUserBrightnessPoint); + } + + // We save the brightness info *after* the brightness setting has been changed and + // adjustments made so that the brightness info reflects the latest value. + brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(), animateValue); + } else { + brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting()); + } + + // Only notify if the brightness adjustment is not temporary (i.e. slider has been released) + if (brightnessAdjusted && !brightnessIsTemporary) { + postBrightnessChangeRunnable(); + } + + // Log any changes to what is currently driving the brightness setting. + if (!mBrightnessReasonTemp.equals(mBrightnessReason) || brightnessAdjustmentFlags != 0) { + Slog.v(mTag, "Brightness [" + brightnessState + "] reason changing to: '" + + mBrightnessReasonTemp.toString(brightnessAdjustmentFlags) + + "', previous reason: '" + mBrightnessReason + "'."); + mBrightnessReason.set(mBrightnessReasonTemp); + } else if (mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_MANUAL + && userSetBrightnessChanged) { + Slog.v(mTag, "Brightness [" + brightnessState + "] manual adjustment."); + } + + + // Log brightness events when a detail of significance has changed. Generally this is the + // brightness itself changing, but also includes data like HBM cap, thermal throttling + // brightness cap, RBC state, etc. + mTempBrightnessEvent.setTime(System.currentTimeMillis()); + mTempBrightnessEvent.setBrightness(brightnessState); + mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId); + mTempBrightnessEvent.setReason(mBrightnessReason); + mTempBrightnessEvent.setHbmMax(mHbmController.getCurrentBrightnessMax()); + mTempBrightnessEvent.setHbmMode(mHbmController.getHighBrightnessMode()); + mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags() + | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0) + | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0)); + mTempBrightnessEvent.setRbcStrength(mCdsi != null + ? mCdsi.getReduceBrightColorsStrength() : -1); + mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor); + // Temporary is what we use during slider interactions. We avoid logging those so that + // we don't spam logcat when the slider is being used. + boolean tempToTempTransition = + mTempBrightnessEvent.getReason().getReason() == BrightnessReason.REASON_TEMPORARY + && mLastBrightnessEvent.getReason().getReason() + == BrightnessReason.REASON_TEMPORARY; + if ((!mTempBrightnessEvent.equalsMainData(mLastBrightnessEvent) && !tempToTempTransition) + || brightnessAdjustmentFlags != 0) { + float lastBrightness = mLastBrightnessEvent.getBrightness(); + mTempBrightnessEvent.setInitialBrightness(lastBrightness); + mTempBrightnessEvent.setFastAmbientLux( + mAutomaticBrightnessController == null + ? -1f : mAutomaticBrightnessController.getFastAmbientLux()); + mTempBrightnessEvent.setSlowAmbientLux( + mAutomaticBrightnessController == null + ? -1f : mAutomaticBrightnessController.getSlowAmbientLux()); + mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness); + mLastBrightnessEvent.copyFrom(mTempBrightnessEvent); + BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent); + // Adjustment flags (and user-set flag) only get added after the equality checks since + // they are transient. + newEvent.setAdjustmentFlags(brightnessAdjustmentFlags); + newEvent.setFlags(newEvent.getFlags() | (userSetBrightnessChanged + ? BrightnessEvent.FLAG_USER_SET : 0)); + Slog.i(mTag, newEvent.toString(/* includeTime= */ false)); + + if (userSetBrightnessChanged) { + logManualBrightnessEvent(newEvent); + } + if (mBrightnessEventRingBuffer != null) { + mBrightnessEventRingBuffer.append(newEvent); + } + } + + // Update display white-balance. + if (mDisplayWhiteBalanceController != null) { + if (state == Display.STATE_ON && mDisplayWhiteBalanceSettings.isEnabled()) { + mDisplayWhiteBalanceController.setEnabled(true); + mDisplayWhiteBalanceController.updateDisplayColorTemperature(); + } else { + mDisplayWhiteBalanceController.setEnabled(false); + } + } + + // Determine whether the display is ready for use in the newly requested state. + // Note that we do not wait for the brightness ramp animation to complete before + // reporting the display is ready because we only need to ensure the screen is in the + // right power state even as it continues to converge on the desired brightness. + final boolean ready = mPendingScreenOnUnblocker == null + && (!mColorFadeEnabled || (!mColorFadeOnAnimator.isStarted() + && !mColorFadeOffAnimator.isStarted())) + && mPowerState.waitUntilClean(mCleanListener); + final boolean finished = ready + && !mScreenBrightnessRampAnimator.isAnimating(); + + // Notify policy about screen turned on. + if (ready && state != Display.STATE_OFF + && mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_TURNING_ON) { + setReportedScreenState(REPORTED_TO_POLICY_SCREEN_ON); + mWindowManagerPolicy.screenTurnedOn(mDisplayId); + } + + // Grab a wake lock if we have unfinished business. + if (!finished && !mUnfinishedBusiness) { + if (DEBUG) { + Slog.d(mTag, "Unfinished business..."); + } + mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness); + mUnfinishedBusiness = true; + } + + // Notify the power manager when ready. + if (ready && mustNotify) { + // Send state change. + synchronized (mLock) { + if (!mPendingRequestChangedLocked) { + mDisplayReadyLocked = true; + + if (DEBUG) { + Slog.d(mTag, "Display ready!"); + } + } + } + sendOnStateChangedWithWakelock(); + } + + // Release the wake lock when we have no unfinished business. + if (finished && mUnfinishedBusiness) { + if (DEBUG) { + Slog.d(mTag, "Finished business..."); + } + mUnfinishedBusiness = false; + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness); + } + + // Record if dozing for future comparison. + mDozing = state != Display.STATE_ON; + + if (previousPolicy != mPowerRequest.policy) { + logDisplayPolicyChanged(mPowerRequest.policy); + } + } + + @Override + public void updateBrightness() { + sendUpdatePowerState(); + } + + /** + * Ignores the proximity sensor until the sensor state changes, but only if the sensor is + * currently enabled and forcing the screen to be dark. + */ + @Override + public void ignoreProximitySensorUntilChanged() { + mHandler.sendEmptyMessage(MSG_IGNORE_PROXIMITY); + } + + @Override + public void setBrightnessConfiguration(BrightnessConfiguration c) { + Message msg = mHandler.obtainMessage(MSG_CONFIGURE_BRIGHTNESS, c); + msg.sendToTarget(); + } + + @Override + public void setTemporaryBrightness(float brightness) { + Message msg = mHandler.obtainMessage(MSG_SET_TEMPORARY_BRIGHTNESS, + Float.floatToIntBits(brightness), 0 /*unused*/); + msg.sendToTarget(); + } + + @Override + public void setTemporaryAutoBrightnessAdjustment(float adjustment) { + Message msg = mHandler.obtainMessage(MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT, + Float.floatToIntBits(adjustment), 0 /*unused*/); + msg.sendToTarget(); + } + + @Override + public BrightnessInfo getBrightnessInfo() { + synchronized (mCachedBrightnessInfo) { + return new BrightnessInfo( + mCachedBrightnessInfo.brightness.value, + mCachedBrightnessInfo.adjustedBrightness.value, + mCachedBrightnessInfo.brightnessMin.value, + mCachedBrightnessInfo.brightnessMax.value, + mCachedBrightnessInfo.hbmMode.value, + mCachedBrightnessInfo.hbmTransitionPoint.value, + mCachedBrightnessInfo.brightnessMaxReason.value); + } + } + + private boolean saveBrightnessInfo(float brightness) { + return saveBrightnessInfo(brightness, brightness); + } + + private boolean saveBrightnessInfo(float brightness, float adjustedBrightness) { + synchronized (mCachedBrightnessInfo) { + final float minBrightness = Math.min(mHbmController.getCurrentBrightnessMin(), + mBrightnessThrottler.getBrightnessCap()); + final float maxBrightness = Math.min(mHbmController.getCurrentBrightnessMax(), + mBrightnessThrottler.getBrightnessCap()); + boolean changed = false; + + changed |= + mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.brightness, + brightness); + changed |= + mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.adjustedBrightness, + adjustedBrightness); + changed |= + mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.brightnessMin, + minBrightness); + changed |= + mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.brightnessMax, + maxBrightness); + changed |= + mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.hbmMode, + mHbmController.getHighBrightnessMode()); + changed |= + mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.hbmTransitionPoint, + mHbmController.getTransitionPoint()); + changed |= + mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason, + mBrightnessThrottler.getBrightnessMaxReason()); + + return changed; + } + } + + void postBrightnessChangeRunnable() { + mHandler.post(mOnBrightnessChangeRunnable); + } + + private HighBrightnessModeController createHbmControllerLocked() { + final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); + final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig(); + final IBinder displayToken = + mLogicalDisplay.getPrimaryDisplayDeviceLocked().getDisplayTokenLocked(); + final String displayUniqueId = + mLogicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(); + final DisplayDeviceConfig.HighBrightnessModeData hbmData = + ddConfig != null ? ddConfig.getHighBrightnessModeData() : null; + final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); + return new HighBrightnessModeController(mHandler, info.width, info.height, displayToken, + displayUniqueId, PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, hbmData, + new HighBrightnessModeController.HdrBrightnessDeviceConfig() { + @Override + public float getHdrBrightnessFromSdr(float sdrBrightness) { + return mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness); + } + }, + () -> { + sendUpdatePowerState(); + postBrightnessChangeRunnable(); + // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern. + if (mAutomaticBrightnessController != null) { + mAutomaticBrightnessController.update(); + } + }, mContext); + } + + private BrightnessThrottler createBrightnessThrottlerLocked() { + final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); + final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig(); + final DisplayDeviceConfig.BrightnessThrottlingData data = + ddConfig != null ? ddConfig.getBrightnessThrottlingData() : null; + return new BrightnessThrottler(mHandler, data, + () -> { + sendUpdatePowerState(); + postBrightnessChangeRunnable(); + }, mUniqueDisplayId); + } + + private void blockScreenOn() { + if (mPendingScreenOnUnblocker == null) { + Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_TRACE_NAME, 0); + mPendingScreenOnUnblocker = new ScreenOnUnblocker(); + mScreenOnBlockStartRealTime = SystemClock.elapsedRealtime(); + Slog.i(mTag, "Blocking screen on until initial contents have been drawn."); + } + } + + private void unblockScreenOn() { + if (mPendingScreenOnUnblocker != null) { + mPendingScreenOnUnblocker = null; + long delay = SystemClock.elapsedRealtime() - mScreenOnBlockStartRealTime; + Slog.i(mTag, "Unblocked screen on after " + delay + " ms"); + Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_TRACE_NAME, 0); + } + } + + private void blockScreenOff() { + if (mPendingScreenOffUnblocker == null) { + Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, SCREEN_OFF_BLOCKED_TRACE_NAME, 0); + mPendingScreenOffUnblocker = new ScreenOffUnblocker(); + mScreenOffBlockStartRealTime = SystemClock.elapsedRealtime(); + Slog.i(mTag, "Blocking screen off"); + } + } + + private void unblockScreenOff() { + if (mPendingScreenOffUnblocker != null) { + mPendingScreenOffUnblocker = null; + long delay = SystemClock.elapsedRealtime() - mScreenOffBlockStartRealTime; + Slog.i(mTag, "Unblocked screen off after " + delay + " ms"); + Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, SCREEN_OFF_BLOCKED_TRACE_NAME, 0); + } + } + + private boolean setScreenState(int state) { + return setScreenState(state, false /*reportOnly*/); + } + + private boolean setScreenState(int state, boolean reportOnly) { + final boolean isOff = (state == Display.STATE_OFF); + + if (mPowerState.getScreenState() != state + || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) { + // If we are trying to turn screen off, give policy a chance to do something before we + // actually turn the screen off. + if (isOff && !mScreenOffBecauseOfProximity) { + if (mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_ON + || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) { + setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_OFF); + blockScreenOff(); + mWindowManagerPolicy.screenTurningOff(mDisplayId, mPendingScreenOffUnblocker); + unblockScreenOff(); + } else if (mPendingScreenOffUnblocker != null) { + // Abort doing the state change until screen off is unblocked. + return false; + } + } + + if (!reportOnly && mPowerState.getScreenState() != state) { + Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenState", state); + // TODO(b/153319140) remove when we can get this from the above trace invocation + SystemProperties.set("debug.tracing.screen_state", String.valueOf(state)); + mPowerState.setScreenState(state); + // Tell battery stats about the transition. + noteScreenState(state); + } + } + + // Tell the window manager policy when the screen is turned off or on unless it's due + // to the proximity sensor. We temporarily block turning the screen on until the + // window manager is ready by leaving a black surface covering the screen. + // This surface is essentially the final state of the color fade animation and + // it is only removed once the window manager tells us that the activity has + // finished drawing underneath. + if (isOff && mReportedScreenStateToPolicy != REPORTED_TO_POLICY_SCREEN_OFF + && !mScreenOffBecauseOfProximity) { + setReportedScreenState(REPORTED_TO_POLICY_SCREEN_OFF); + unblockScreenOn(); + mWindowManagerPolicy.screenTurnedOff(mDisplayId); + } else if (!isOff + && mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_TURNING_OFF) { + + // We told policy already that screen was turning off, but now we changed our minds. + // Complete the full state transition on -> turningOff -> off. + unblockScreenOff(); + mWindowManagerPolicy.screenTurnedOff(mDisplayId); + setReportedScreenState(REPORTED_TO_POLICY_SCREEN_OFF); + } + if (!isOff + && (mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_OFF + || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED)) { + setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_ON); + if (mPowerState.getColorFadeLevel() == 0.0f) { + blockScreenOn(); + } else { + unblockScreenOn(); + } + mWindowManagerPolicy.screenTurningOn(mDisplayId, mPendingScreenOnUnblocker); + } + + // Return true if the screen isn't blocked. + return mPendingScreenOnUnblocker == null; + } + + private void setReportedScreenState(int state) { + Trace.traceCounter(Trace.TRACE_TAG_POWER, "ReportedScreenStateToPolicy", state); + mReportedScreenStateToPolicy = state; + } + + private void loadAmbientLightSensor() { + DisplayDeviceConfig.SensorData lightSensor = mDisplayDeviceConfig.getAmbientLightSensor(); + final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY + ? Sensor.TYPE_LIGHT : SensorUtils.NO_FALLBACK; + mLightSensor = SensorUtils.findSensor(mSensorManager, lightSensor.type, lightSensor.name, + fallbackType); + } + + private void loadProximitySensor() { + if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT) { + return; + } + final DisplayDeviceConfig.SensorData proxSensor = + mDisplayDeviceConfig.getProximitySensor(); + final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY + ? Sensor.TYPE_PROXIMITY : SensorUtils.NO_FALLBACK; + mProximitySensor = SensorUtils.findSensor(mSensorManager, proxSensor.type, proxSensor.name, + fallbackType); + if (mProximitySensor != null) { + mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(), + TYPICAL_PROXIMITY_THRESHOLD); + } + } + + private float clampScreenBrightnessForVr(float value) { + return MathUtils.constrain( + value, mScreenBrightnessForVrRangeMinimum, + mScreenBrightnessForVrRangeMaximum); + } + + private float clampScreenBrightness(float value) { + if (Float.isNaN(value)) { + value = PowerManager.BRIGHTNESS_MIN; + } + return MathUtils.constrain(value, + mHbmController.getCurrentBrightnessMin(), mHbmController.getCurrentBrightnessMax()); + } + + // Checks whether the brightness is within the valid brightness range, not including off. + private boolean isValidBrightnessValue(float brightness) { + return brightness >= PowerManager.BRIGHTNESS_MIN + && brightness <= PowerManager.BRIGHTNESS_MAX; + } + + private void animateScreenBrightness(float target, float sdrTarget, float rate) { + if (DEBUG) { + Slog.d(mTag, "Animating brightness: target=" + target + ", sdrTarget=" + sdrTarget + + ", rate=" + rate); + } + if (mScreenBrightnessRampAnimator.animateTo(target, sdrTarget, rate)) { + Trace.traceCounter(Trace.TRACE_TAG_POWER, "TargetScreenBrightness", (int) target); + // TODO(b/153319140) remove when we can get this from the above trace invocation + SystemProperties.set("debug.tracing.screen_brightness", String.valueOf(target)); + noteScreenBrightness(target); + } + } + + private void animateScreenStateChange(int target, boolean performScreenOffTransition) { + // If there is already an animation in progress, don't interfere with it. + if (mColorFadeEnabled + && (mColorFadeOnAnimator.isStarted() || mColorFadeOffAnimator.isStarted())) { + if (target != Display.STATE_ON) { + return; + } + // If display state changed to on, proceed and stop the color fade and turn screen on. + mPendingScreenOff = false; + } + + if (mDisplayBlanksAfterDozeConfig + && Display.isDozeState(mPowerState.getScreenState()) + && !Display.isDozeState(target)) { + // Skip the screen off animation and add a black surface to hide the + // contents of the screen. + mPowerState.prepareColorFade(mContext, + mColorFadeFadesConfig ? ColorFade.MODE_FADE : ColorFade.MODE_WARM_UP); + if (mColorFadeOffAnimator != null) { + mColorFadeOffAnimator.end(); + } + // Some display hardware will blank itself on the transition between doze and non-doze + // but still on display states. In this case we want to report to policy that the + // display has turned off so it can prepare the appropriate power on animation, but we + // don't want to actually transition to the fully off state since that takes + // significantly longer to transition from. + setScreenState(Display.STATE_OFF, target != Display.STATE_OFF /*reportOnly*/); + } + + // If we were in the process of turning off the screen but didn't quite + // finish. Then finish up now to prevent a jarring transition back + // to screen on if we skipped blocking screen on as usual. + if (mPendingScreenOff && target != Display.STATE_OFF) { + setScreenState(Display.STATE_OFF); + mPendingScreenOff = false; + mPowerState.dismissColorFadeResources(); + } + + if (target == Display.STATE_ON) { + // Want screen on. The contents of the screen may not yet + // be visible if the color fade has not been dismissed because + // its last frame of animation is solid black. + if (!setScreenState(Display.STATE_ON)) { + return; // screen on blocked + } + if (USE_COLOR_FADE_ON_ANIMATION && mColorFadeEnabled && mPowerRequest.isBrightOrDim()) { + // Perform screen on animation. + if (mPowerState.getColorFadeLevel() == 1.0f) { + mPowerState.dismissColorFade(); + } else if (mPowerState.prepareColorFade(mContext, + mColorFadeFadesConfig + ? ColorFade.MODE_FADE : ColorFade.MODE_WARM_UP)) { + mColorFadeOnAnimator.start(); + } else { + mColorFadeOnAnimator.end(); + } + } else { + // Skip screen on animation. + mPowerState.setColorFadeLevel(1.0f); + mPowerState.dismissColorFade(); + } + } else if (target == Display.STATE_VR) { + // Wait for brightness animation to complete beforehand when entering VR + // from screen on to prevent a perceptible jump because brightness may operate + // differently when the display is configured for dozing. + if (mScreenBrightnessRampAnimator.isAnimating() + && mPowerState.getScreenState() == Display.STATE_ON) { + return; + } + + // Set screen state. + if (!setScreenState(Display.STATE_VR)) { + return; // screen on blocked + } + + // Dismiss the black surface without fanfare. + mPowerState.setColorFadeLevel(1.0f); + mPowerState.dismissColorFade(); + } else if (target == Display.STATE_DOZE) { + // Want screen dozing. + // Wait for brightness animation to complete beforehand when entering doze + // from screen on to prevent a perceptible jump because brightness may operate + // differently when the display is configured for dozing. + if (mScreenBrightnessRampAnimator.isAnimating() + && mPowerState.getScreenState() == Display.STATE_ON) { + return; + } + + // Set screen state. + if (!setScreenState(Display.STATE_DOZE)) { + return; // screen on blocked + } + + // Dismiss the black surface without fanfare. + mPowerState.setColorFadeLevel(1.0f); + mPowerState.dismissColorFade(); + } else if (target == Display.STATE_DOZE_SUSPEND) { + // Want screen dozing and suspended. + // Wait for brightness animation to complete beforehand unless already + // suspended because we may not be able to change it after suspension. + if (mScreenBrightnessRampAnimator.isAnimating() + && mPowerState.getScreenState() != Display.STATE_DOZE_SUSPEND) { + return; + } + + // If not already suspending, temporarily set the state to doze until the + // screen on is unblocked, then suspend. + if (mPowerState.getScreenState() != Display.STATE_DOZE_SUSPEND) { + if (!setScreenState(Display.STATE_DOZE)) { + return; // screen on blocked + } + setScreenState(Display.STATE_DOZE_SUSPEND); // already on so can't block + } + + // Dismiss the black surface without fanfare. + mPowerState.setColorFadeLevel(1.0f); + mPowerState.dismissColorFade(); + } else if (target == Display.STATE_ON_SUSPEND) { + // Want screen full-power and suspended. + // Wait for brightness animation to complete beforehand unless already + // suspended because we may not be able to change it after suspension. + if (mScreenBrightnessRampAnimator.isAnimating() + && mPowerState.getScreenState() != Display.STATE_ON_SUSPEND) { + return; + } + + // If not already suspending, temporarily set the state to on until the + // screen on is unblocked, then suspend. + if (mPowerState.getScreenState() != Display.STATE_ON_SUSPEND) { + if (!setScreenState(Display.STATE_ON)) { + return; + } + setScreenState(Display.STATE_ON_SUSPEND); + } + + // Dismiss the black surface without fanfare. + mPowerState.setColorFadeLevel(1.0f); + mPowerState.dismissColorFade(); + } else { + // Want screen off. + mPendingScreenOff = true; + if (!mColorFadeEnabled) { + mPowerState.setColorFadeLevel(0.0f); + } + + if (mPowerState.getColorFadeLevel() == 0.0f) { + // Turn the screen off. + // A black surface is already hiding the contents of the screen. + setScreenState(Display.STATE_OFF); + mPendingScreenOff = false; + mPowerState.dismissColorFadeResources(); + } else if (performScreenOffTransition + && mPowerState.prepareColorFade(mContext, + mColorFadeFadesConfig + ? ColorFade.MODE_FADE : ColorFade.MODE_COOL_DOWN) + && mPowerState.getScreenState() != Display.STATE_OFF) { + // Perform the screen off animation. + mColorFadeOffAnimator.start(); + } else { + // Skip the screen off animation and add a black surface to hide the + // contents of the screen. + mColorFadeOffAnimator.end(); + } + } + } + + private final Runnable mCleanListener = this::sendUpdatePowerState; + + private void setProximitySensorEnabled(boolean enable) { + if (enable) { + if (!mProximitySensorEnabled) { + // Register the listener. + // Proximity sensor state already cleared initially. + mProximitySensorEnabled = true; + mIgnoreProximityUntilChanged = false; + mSensorManager.registerListener(mProximitySensorListener, mProximitySensor, + SensorManager.SENSOR_DELAY_NORMAL, mHandler); + } + } else { + if (mProximitySensorEnabled) { + // Unregister the listener. + // Clear the proximity sensor state for next time. + mProximitySensorEnabled = false; + mProximity = PROXIMITY_UNKNOWN; + mIgnoreProximityUntilChanged = false; + mPendingProximity = PROXIMITY_UNKNOWN; + mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED); + mSensorManager.unregisterListener(mProximitySensorListener); + clearPendingProximityDebounceTime(); // release wake lock (must be last) + } + } + } + + private void handleProximitySensorEvent(long time, boolean positive) { + if (mProximitySensorEnabled) { + if (mPendingProximity == PROXIMITY_NEGATIVE && !positive) { + return; // no change + } + if (mPendingProximity == PROXIMITY_POSITIVE && positive) { + return; // no change + } + + // Only accept a proximity sensor reading if it remains + // stable for the entire debounce delay. We hold a wake lock while + // debouncing the sensor. + mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED); + if (positive) { + mPendingProximity = PROXIMITY_POSITIVE; + setPendingProximityDebounceTime( + time + PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY); // acquire wake lock + } else { + mPendingProximity = PROXIMITY_NEGATIVE; + setPendingProximityDebounceTime( + time + PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY); // acquire wake lock + } + + // Debounce the new sensor reading. + debounceProximitySensor(); + } + } + + private void debounceProximitySensor() { + if (mProximitySensorEnabled + && mPendingProximity != PROXIMITY_UNKNOWN + && mPendingProximityDebounceTime >= 0) { + final long now = mClock.uptimeMillis(); + if (mPendingProximityDebounceTime <= now) { + if (mProximity != mPendingProximity) { + // if the status of the sensor changed, stop ignoring. + mIgnoreProximityUntilChanged = false; + Slog.i(mTag, "No longer ignoring proximity [" + mPendingProximity + "]"); + } + // Sensor reading accepted. Apply the change then release the wake lock. + mProximity = mPendingProximity; + updatePowerState(); + clearPendingProximityDebounceTime(); // release wake lock (must be last) + } else { + // Need to wait a little longer. + // Debounce again later. We continue holding a wake lock while waiting. + Message msg = mHandler.obtainMessage(MSG_PROXIMITY_SENSOR_DEBOUNCED); + mHandler.sendMessageAtTime(msg, mPendingProximityDebounceTime); + } + } + } + + private void clearPendingProximityDebounceTime() { + if (mPendingProximityDebounceTime >= 0) { + mPendingProximityDebounceTime = -1; + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxDebounce); + } + } + + private void setPendingProximityDebounceTime(long debounceTime) { + if (mPendingProximityDebounceTime < 0) { + mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxDebounce); + } + mPendingProximityDebounceTime = debounceTime; + } + + private void sendOnStateChangedWithWakelock() { + if (!mOnStateChangedPending) { + mOnStateChangedPending = true; + mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged); + mHandler.post(mOnStateChangedRunnable); + } + } + + private void logDisplayPolicyChanged(int newPolicy) { + LogMaker log = new LogMaker(MetricsEvent.DISPLAY_POLICY); + log.setType(MetricsEvent.TYPE_UPDATE); + log.setSubtype(newPolicy); + MetricsLogger.action(log); + } + + private void handleSettingsChange(boolean userSwitch) { + mPendingScreenBrightnessSetting = getScreenBrightnessSetting(); + mPendingAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting(); + if (userSwitch) { + // Don't treat user switches as user initiated change. + setCurrentScreenBrightness(mPendingScreenBrightnessSetting); + updateAutoBrightnessAdjustment(); + if (mAutomaticBrightnessController != null) { + mAutomaticBrightnessController.resetShortTermModel(); + } + } + // We don't bother with a pending variable for VR screen brightness since we just + // immediately adapt to it. + mScreenBrightnessForVr = getScreenBrightnessForVrSetting(); + sendUpdatePowerState(); + } + + private float getAutoBrightnessAdjustmentSetting() { + final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT); + return Float.isNaN(adj) ? 0.0f : clampAutoBrightnessAdjustment(adj); + } + + @Override + public float getScreenBrightnessSetting() { + float brightness = mBrightnessSetting.getBrightness(); + if (Float.isNaN(brightness)) { + brightness = mScreenBrightnessDefault; + } + return clampAbsoluteBrightness(brightness); + } + + private float getScreenBrightnessForVrSetting() { + final float brightnessFloat = Settings.System.getFloatForUser(mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS_FOR_VR_FLOAT, mScreenBrightnessForVrDefault, + UserHandle.USER_CURRENT); + return clampScreenBrightnessForVr(brightnessFloat); + } + + @Override + public void setBrightness(float brightnessValue) { + // Update the setting, which will eventually call back into DPC to have us actually update + // the display with the new value. + mBrightnessSetting.setBrightness(brightnessValue); + } + + private void updateScreenBrightnessSetting(float brightnessValue) { + if (!isValidBrightnessValue(brightnessValue) + || brightnessValue == mCurrentScreenBrightnessSetting) { + return; + } + setCurrentScreenBrightness(brightnessValue); + mBrightnessSetting.setBrightness(brightnessValue); + } + + private void setCurrentScreenBrightness(float brightnessValue) { + if (brightnessValue != mCurrentScreenBrightnessSetting) { + mCurrentScreenBrightnessSetting = brightnessValue; + postBrightnessChangeRunnable(); + } + } + + private void putAutoBrightnessAdjustmentSetting(float adjustment) { + if (mDisplayId == Display.DEFAULT_DISPLAY) { + mAutoBrightnessAdjustment = adjustment; + Settings.System.putFloatForUser(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, adjustment, + UserHandle.USER_CURRENT); + } + } + + private boolean updateAutoBrightnessAdjustment() { + if (Float.isNaN(mPendingAutoBrightnessAdjustment)) { + return false; + } + if (mAutoBrightnessAdjustment == mPendingAutoBrightnessAdjustment) { + mPendingAutoBrightnessAdjustment = Float.NaN; + return false; + } + mAutoBrightnessAdjustment = mPendingAutoBrightnessAdjustment; + mPendingAutoBrightnessAdjustment = Float.NaN; + mTemporaryAutoBrightnessAdjustment = Float.NaN; + return true; + } + + // We want to return true if the user has set the screen brightness. + // RBC on, off, and intensity changes will return false. + // Slider interactions whilst in RBC will return true, just as when in non-rbc. + private boolean updateUserSetScreenBrightness() { + if ((Float.isNaN(mPendingScreenBrightnessSetting) + || mPendingScreenBrightnessSetting < 0.0f)) { + return false; + } + if (mCurrentScreenBrightnessSetting == mPendingScreenBrightnessSetting) { + mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; + return false; + } + setCurrentScreenBrightness(mPendingScreenBrightnessSetting); + mLastUserSetScreenBrightness = mPendingScreenBrightnessSetting; + mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; + return true; + } + + private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated, + boolean hadUserDataPoint) { + final float brightnessInNits = convertToNits(brightness); + if (mPowerRequest.useAutoBrightness && brightnessInNits >= 0.0f + && mAutomaticBrightnessController != null) { + // We only want to track changes on devices that can actually map the display backlight + // values into a physical brightness unit since the value provided by the API is in + // nits and not using the arbitrary backlight units. + final float powerFactor = mPowerRequest.lowPowerMode + ? mPowerRequest.screenLowPowerBrightnessFactor + : 1.0f; + mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated, + powerFactor, hadUserDataPoint, + mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId); + } + } + + private float convertToNits(float brightness) { + if (mAutomaticBrightnessController == null) { + return -1f; + } + return mAutomaticBrightnessController.convertToNits(brightness); + } + + @GuardedBy("mLock") + private void updatePendingProximityRequestsLocked() { + mWaitingForNegativeProximity |= mPendingWaitForNegativeProximityLocked; + mPendingWaitForNegativeProximityLocked = false; + + if (mIgnoreProximityUntilChanged) { + // Also, lets stop waiting for negative proximity if we're ignoring it. + mWaitingForNegativeProximity = false; + } + } + + private void ignoreProximitySensorUntilChangedInternal() { + if (!mIgnoreProximityUntilChanged + && mProximity == PROXIMITY_POSITIVE) { + // Only ignore if it is still reporting positive (near) + mIgnoreProximityUntilChanged = true; + Slog.i(mTag, "Ignoring proximity"); + updatePowerState(); + } + } + + private final Runnable mOnStateChangedRunnable = new Runnable() { + @Override + public void run() { + mOnStateChangedPending = false; + mCallbacks.onStateChanged(); + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged); + } + }; + + private void sendOnProximityPositiveWithWakelock() { + mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxPositive); + mHandler.post(mOnProximityPositiveRunnable); + mOnProximityPositiveMessages++; + } + + private final Runnable mOnProximityPositiveRunnable = new Runnable() { + @Override + public void run() { + mOnProximityPositiveMessages--; + mCallbacks.onProximityPositive(); + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive); + } + }; + + private void sendOnProximityNegativeWithWakelock() { + mOnProximityNegativeMessages++; + mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxNegative); + mHandler.post(mOnProximityNegativeRunnable); + } + + private final Runnable mOnProximityNegativeRunnable = new Runnable() { + @Override + public void run() { + mOnProximityNegativeMessages--; + mCallbacks.onProximityNegative(); + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative); + } + }; + + @Override + public void dump(final PrintWriter pw) { + synchronized (mLock) { + pw.println(); + pw.println("Display Power Controller:"); + pw.println(" mDisplayId=" + mDisplayId); + pw.println(" mLightSensor=" + mLightSensor); + + pw.println(); + pw.println("Display Power Controller Locked State:"); + pw.println(" mDisplayReadyLocked=" + mDisplayReadyLocked); + pw.println(" mPendingRequestLocked=" + mPendingRequestLocked); + pw.println(" mPendingRequestChangedLocked=" + mPendingRequestChangedLocked); + pw.println(" mPendingWaitForNegativeProximityLocked=" + + mPendingWaitForNegativeProximityLocked); + pw.println(" mPendingUpdatePowerStateLocked=" + mPendingUpdatePowerStateLocked); + } + + pw.println(); + pw.println("Display Power Controller Configuration:"); + pw.println(" mScreenBrightnessRangeDefault=" + mScreenBrightnessDefault); + pw.println(" mScreenBrightnessDozeConfig=" + mScreenBrightnessDozeConfig); + pw.println(" mScreenBrightnessDimConfig=" + mScreenBrightnessDimConfig); + pw.println(" mScreenBrightnessForVrRangeMinimum=" + mScreenBrightnessForVrRangeMinimum); + pw.println(" mScreenBrightnessForVrRangeMaximum=" + mScreenBrightnessForVrRangeMaximum); + pw.println(" mScreenBrightnessForVrDefault=" + mScreenBrightnessForVrDefault); + pw.println(" mUseSoftwareAutoBrightnessConfig=" + mUseSoftwareAutoBrightnessConfig); + pw.println(" mAllowAutoBrightnessWhileDozingConfig=" + + mAllowAutoBrightnessWhileDozingConfig); + pw.println(" mSkipScreenOnBrightnessRamp=" + mSkipScreenOnBrightnessRamp); + pw.println(" mColorFadeFadesConfig=" + mColorFadeFadesConfig); + pw.println(" mColorFadeEnabled=" + mColorFadeEnabled); + synchronized (mCachedBrightnessInfo) { + pw.println(" mCachedBrightnessInfo.brightness=" + + mCachedBrightnessInfo.brightness.value); + pw.println(" mCachedBrightnessInfo.adjustedBrightness=" + + mCachedBrightnessInfo.adjustedBrightness.value); + pw.println(" mCachedBrightnessInfo.brightnessMin=" + + mCachedBrightnessInfo.brightnessMin.value); + pw.println(" mCachedBrightnessInfo.brightnessMax=" + + mCachedBrightnessInfo.brightnessMax.value); + pw.println(" mCachedBrightnessInfo.hbmMode=" + mCachedBrightnessInfo.hbmMode.value); + pw.println(" mCachedBrightnessInfo.hbmTransitionPoint=" + + mCachedBrightnessInfo.hbmTransitionPoint.value); + pw.println(" mCachedBrightnessInfo.brightnessMaxReason =" + + mCachedBrightnessInfo.brightnessMaxReason.value); + } + pw.println(" mDisplayBlanksAfterDozeConfig=" + mDisplayBlanksAfterDozeConfig); + pw.println(" mBrightnessBucketsInDozeConfig=" + mBrightnessBucketsInDozeConfig); + + mHandler.runWithScissors(() -> dumpLocal(pw), 1000); + } + + private void dumpLocal(PrintWriter pw) { + pw.println(); + pw.println("Display Power Controller Thread State:"); + pw.println(" mPowerRequest=" + mPowerRequest); + pw.println(" mUnfinishedBusiness=" + mUnfinishedBusiness); + pw.println(" mWaitingForNegativeProximity=" + mWaitingForNegativeProximity); + pw.println(" mProximitySensor=" + mProximitySensor); + pw.println(" mProximitySensorEnabled=" + mProximitySensorEnabled); + pw.println(" mProximityThreshold=" + mProximityThreshold); + pw.println(" mProximity=" + proximityToString(mProximity)); + pw.println(" mPendingProximity=" + proximityToString(mPendingProximity)); + pw.println(" mPendingProximityDebounceTime=" + + TimeUtils.formatUptime(mPendingProximityDebounceTime)); + pw.println(" mScreenOffBecauseOfProximity=" + mScreenOffBecauseOfProximity); + pw.println(" mLastUserSetScreenBrightness=" + mLastUserSetScreenBrightness); + pw.println(" mPendingScreenBrightnessSetting=" + + mPendingScreenBrightnessSetting); + pw.println(" mTemporaryScreenBrightness=" + mTemporaryScreenBrightness); + pw.println(" mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment); + pw.println(" mBrightnessReason=" + mBrightnessReason); + pw.println(" mTemporaryAutoBrightnessAdjustment=" + mTemporaryAutoBrightnessAdjustment); + pw.println(" mPendingAutoBrightnessAdjustment=" + mPendingAutoBrightnessAdjustment); + pw.println(" mScreenBrightnessForVrFloat=" + mScreenBrightnessForVr); + pw.println(" mAppliedAutoBrightness=" + mAppliedAutoBrightness); + pw.println(" mAppliedDimming=" + mAppliedDimming); + pw.println(" mAppliedLowPower=" + mAppliedLowPower); + pw.println(" mAppliedThrottling=" + mAppliedThrottling); + pw.println(" mAppliedScreenBrightnessOverride=" + mAppliedScreenBrightnessOverride); + pw.println(" mAppliedTemporaryBrightness=" + mAppliedTemporaryBrightness); + pw.println(" mAppliedTemporaryAutoBrightnessAdjustment=" + + mAppliedTemporaryAutoBrightnessAdjustment); + pw.println(" mAppliedBrightnessBoost=" + mAppliedBrightnessBoost); + pw.println(" mDozing=" + mDozing); + pw.println(" mSkipRampState=" + skipRampStateToString(mSkipRampState)); + pw.println(" mScreenOnBlockStartRealTime=" + mScreenOnBlockStartRealTime); + pw.println(" mScreenOffBlockStartRealTime=" + mScreenOffBlockStartRealTime); + pw.println(" mPendingScreenOnUnblocker=" + mPendingScreenOnUnblocker); + pw.println(" mPendingScreenOffUnblocker=" + mPendingScreenOffUnblocker); + pw.println(" mPendingScreenOff=" + mPendingScreenOff); + pw.println(" mReportedToPolicy=" + + reportedToPolicyToString(mReportedScreenStateToPolicy)); + pw.println(" mIsRbcActive=" + mIsRbcActive); + pw.println(" mOnStateChangePending=" + mOnStateChangedPending); + pw.println(" mOnProximityPositiveMessages=" + mOnProximityPositiveMessages); + pw.println(" mOnProximityNegativeMessages=" + mOnProximityNegativeMessages); + + if (mScreenBrightnessRampAnimator != null) { + pw.println(" mScreenBrightnessRampAnimator.isAnimating()=" + + mScreenBrightnessRampAnimator.isAnimating()); + } + + if (mColorFadeOnAnimator != null) { + pw.println(" mColorFadeOnAnimator.isStarted()=" + + mColorFadeOnAnimator.isStarted()); + } + if (mColorFadeOffAnimator != null) { + pw.println(" mColorFadeOffAnimator.isStarted()=" + + mColorFadeOffAnimator.isStarted()); + } + + if (mPowerState != null) { + mPowerState.dump(pw); + } + + if (mAutomaticBrightnessController != null) { + mAutomaticBrightnessController.dump(pw); + dumpBrightnessEvents(pw); + } + + if (mHbmController != null) { + mHbmController.dump(pw); + } + + if (mBrightnessThrottler != null) { + mBrightnessThrottler.dump(pw); + } + + pw.println(); + if (mDisplayWhiteBalanceController != null) { + mDisplayWhiteBalanceController.dump(pw); + mDisplayWhiteBalanceSettings.dump(pw); + } + } + + private static String proximityToString(int state) { + switch (state) { + case PROXIMITY_UNKNOWN: + return "Unknown"; + case PROXIMITY_NEGATIVE: + return "Negative"; + case PROXIMITY_POSITIVE: + return "Positive"; + default: + return Integer.toString(state); + } + } + + private static String reportedToPolicyToString(int state) { + switch (state) { + case REPORTED_TO_POLICY_SCREEN_OFF: + return "REPORTED_TO_POLICY_SCREEN_OFF"; + case REPORTED_TO_POLICY_SCREEN_TURNING_ON: + return "REPORTED_TO_POLICY_SCREEN_TURNING_ON"; + case REPORTED_TO_POLICY_SCREEN_ON: + return "REPORTED_TO_POLICY_SCREEN_ON"; + default: + return Integer.toString(state); + } + } + + private static String skipRampStateToString(int state) { + switch (state) { + case RAMP_STATE_SKIP_NONE: + return "RAMP_STATE_SKIP_NONE"; + case RAMP_STATE_SKIP_INITIAL: + return "RAMP_STATE_SKIP_INITIAL"; + case RAMP_STATE_SKIP_AUTOBRIGHT: + return "RAMP_STATE_SKIP_AUTOBRIGHT"; + default: + return Integer.toString(state); + } + } + + private void dumpBrightnessEvents(PrintWriter pw) { + int size = mBrightnessEventRingBuffer.size(); + if (size < 1) { + pw.println("No Automatic Brightness Adjustments"); + return; + } + + pw.println("Automatic Brightness Adjustments Last " + size + " Events: "); + BrightnessEvent[] eventArray = mBrightnessEventRingBuffer.toArray(); + for (int i = 0; i < mBrightnessEventRingBuffer.size(); i++) { + pw.println(" " + eventArray[i].toString()); + } + } + + private static float clampAbsoluteBrightness(float value) { + return MathUtils.constrain(value, PowerManager.BRIGHTNESS_MIN, + PowerManager.BRIGHTNESS_MAX); + } + + private static float clampAutoBrightnessAdjustment(float value) { + return MathUtils.constrain(value, -1.0f, 1.0f); + } + + private void noteScreenState(int screenState) { + if (mBatteryStats != null) { + try { + // TODO(multi-display): make this multi-display + mBatteryStats.noteScreenState(screenState); + } catch (RemoteException e) { + // same process + } + } + } + + private void noteScreenBrightness(float brightness) { + if (mBatteryStats != null) { + try { + // TODO(brightnessfloat): change BatteryStats to use float + mBatteryStats.noteScreenBrightness(BrightnessSynchronizer.brightnessFloatToInt( + brightness)); + } catch (RemoteException e) { + // same process + } + } + } + + private void reportStats(float brightness) { + if (mLastStatsBrightness == brightness) { + return; + } + + float hbmTransitionPoint = PowerManager.BRIGHTNESS_MAX; + synchronized (mCachedBrightnessInfo) { + if (mCachedBrightnessInfo.hbmTransitionPoint == null) { + return; + } + hbmTransitionPoint = mCachedBrightnessInfo.hbmTransitionPoint.value; + } + + final boolean aboveTransition = brightness > hbmTransitionPoint; + final boolean oldAboveTransition = mLastStatsBrightness > hbmTransitionPoint; + + if (aboveTransition || oldAboveTransition) { + mLastStatsBrightness = brightness; + mHandler.removeMessages(MSG_STATSD_HBM_BRIGHTNESS); + if (aboveTransition != oldAboveTransition) { + // report immediately + logHbmBrightnessStats(brightness, mDisplayStatsId); + } else { + // delay for rate limiting + Message msg = mHandler.obtainMessage(); + msg.what = MSG_STATSD_HBM_BRIGHTNESS; + msg.arg1 = Float.floatToIntBits(brightness); + msg.arg2 = mDisplayStatsId; + mHandler.sendMessageDelayed(msg, BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS); + } + } + } + + private void logHbmBrightnessStats(float brightness, int displayStatsId) { + synchronized (mHandler) { + FrameworkStatsLog.write( + FrameworkStatsLog.DISPLAY_HBM_BRIGHTNESS_CHANGED, displayStatsId, brightness); + } + } + + private void logManualBrightnessEvent(BrightnessEvent event) { + float appliedLowPowerMode = event.isLowPowerModeSet() ? event.getPowerFactor() : -1f; + int appliedRbcStrength = event.isRbcEnabled() ? event.getRbcStrength() : -1; + float appliedHbmMaxNits = + event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF + ? -1f : convertToNits(event.getHbmMax()); + // thermalCapNits set to -1 if not currently capping max brightness + float appliedThermalCapNits = + event.getThermalMax() == PowerManager.BRIGHTNESS_MAX + ? -1f : convertToNits(event.getThermalMax()); + + FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED, + convertToNits(event.getInitialBrightness()), + convertToNits(event.getBrightness()), + event.getSlowAmbientLux(), + event.getPhysicalDisplayId(), + event.isShortTermModelActive(), + appliedLowPowerMode, + appliedRbcStrength, + appliedHbmMaxNits, + appliedThermalCapNits, + event.isAutomaticBrightnessEnabled(), + FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL); + } + + private final class DisplayControllerHandler extends Handler { + DisplayControllerHandler(Looper looper) { + super(looper, null, true /*async*/); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE_POWER_STATE: + updatePowerState(); + break; + + case MSG_PROXIMITY_SENSOR_DEBOUNCED: + debounceProximitySensor(); + break; + + case MSG_SCREEN_ON_UNBLOCKED: + if (mPendingScreenOnUnblocker == msg.obj) { + unblockScreenOn(); + updatePowerState(); + } + break; + case MSG_SCREEN_OFF_UNBLOCKED: + if (mPendingScreenOffUnblocker == msg.obj) { + unblockScreenOff(); + updatePowerState(); + } + break; + case MSG_CONFIGURE_BRIGHTNESS: + mBrightnessConfiguration = (BrightnessConfiguration) msg.obj; + updatePowerState(); + break; + + case MSG_SET_TEMPORARY_BRIGHTNESS: + // TODO: Should we have a a timeout for the temporary brightness? + mTemporaryScreenBrightness = Float.intBitsToFloat(msg.arg1); + updatePowerState(); + break; + + case MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT: + mTemporaryAutoBrightnessAdjustment = Float.intBitsToFloat(msg.arg1); + updatePowerState(); + break; + + case MSG_IGNORE_PROXIMITY: + ignoreProximitySensorUntilChangedInternal(); + break; + + case MSG_STOP: + cleanupHandlerThreadAfterStop(); + break; + + case MSG_UPDATE_BRIGHTNESS: + if (mStopped) { + return; + } + handleSettingsChange(false /*userSwitch*/); + break; + + case MSG_UPDATE_RBC: + handleRbcChanged(); + break; + + case MSG_BRIGHTNESS_RAMP_DONE: + if (mPowerState != null) { + final float brightness = mPowerState.getScreenBrightness(); + reportStats(brightness); + } + break; + + case MSG_STATSD_HBM_BRIGHTNESS: + logHbmBrightnessStats(Float.intBitsToFloat(msg.arg1), msg.arg2); + break; + } + } + } + + private final SensorEventListener mProximitySensorListener = new SensorEventListener() { + @Override + public void onSensorChanged(SensorEvent event) { + if (mProximitySensorEnabled) { + final long time = mClock.uptimeMillis(); + final float distance = event.values[0]; + boolean positive = distance >= 0.0f && distance < mProximityThreshold; + handleProximitySensorEvent(time, positive); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // Not used. + } + }; + + + private final class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + handleSettingsChange(false /* userSwitch */); + } + } + + private final class ScreenOnUnblocker implements WindowManagerPolicy.ScreenOnListener { + @Override + public void onScreenOn() { + Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this); + mHandler.sendMessage(msg); + } + } + + private final class ScreenOffUnblocker implements WindowManagerPolicy.ScreenOffListener { + @Override + public void onScreenOff() { + Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this); + mHandler.sendMessage(msg); + } + } + + @Override + public void setAutoBrightnessLoggingEnabled(boolean enabled) { + if (mAutomaticBrightnessController != null) { + mAutomaticBrightnessController.setLoggingEnabled(enabled); + } + } + + @Override // DisplayWhiteBalanceController.Callbacks + public void updateWhiteBalance() { + sendUpdatePowerState(); + } + + @Override + public void setDisplayWhiteBalanceLoggingEnabled(boolean enabled) { + if (mDisplayWhiteBalanceController != null) { + mDisplayWhiteBalanceController.setLoggingEnabled(enabled); + mDisplayWhiteBalanceSettings.setLoggingEnabled(enabled); + } + } + + @Override + public void setAmbientColorTemperatureOverride(float cct) { + if (mDisplayWhiteBalanceController != null) { + mDisplayWhiteBalanceController.setAmbientColorTemperatureOverride(cct); + // The ambient color temperature override is only applied when the ambient color + // temperature changes or is updated, so it doesn't necessarily change the screen color + // temperature immediately. So, let's make it! + sendUpdatePowerState(); + } + } + + @VisibleForTesting + String getSuspendBlockerUnfinishedBusinessId(int displayId) { + return "[" + displayId + "]unfinished business"; + } + + String getSuspendBlockerOnStateChangedId(int displayId) { + return "[" + displayId + "]on state changed"; + } + + String getSuspendBlockerProxPositiveId(int displayId) { + return "[" + displayId + "]prox positive"; + } + + String getSuspendBlockerProxNegativeId(int displayId) { + return "[" + displayId + "]prox negative"; + } + + @VisibleForTesting + String getSuspendBlockerProxDebounceId(int displayId) { + return "[" + displayId + "]prox debounce"; + } + + /** Functional interface for providing time. */ + @VisibleForTesting + interface Clock { + /** + * Returns current time in milliseconds since boot, not counting time spent in deep sleep. + */ + long uptimeMillis(); + } + + @VisibleForTesting + static class Injector { + Clock getClock() { + return SystemClock::uptimeMillis; + } + + DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade, + int displayId, int displayState) { + return new DisplayPowerState(blanker, colorFade, displayId, displayState); + } + + DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps, + FloatProperty<DisplayPowerState> firstProperty, + FloatProperty<DisplayPowerState> secondProperty) { + return new DualRampAnimator(dps, firstProperty, secondProperty); + } + } + + static class CachedBrightnessInfo { + public MutableFloat brightness = new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT); + public MutableFloat adjustedBrightness = + new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT); + public MutableFloat brightnessMin = + new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT); + public MutableFloat brightnessMax = + new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT); + public MutableInt hbmMode = new MutableInt(BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF); + public MutableFloat hbmTransitionPoint = + new MutableFloat(HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID); + public MutableInt brightnessMaxReason = + new MutableInt(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE); + + public boolean checkAndSetFloat(MutableFloat mf, float f) { + if (mf.value != f) { + mf.value = f; + return true; + } + return false; + } + + public boolean checkAndSetInt(MutableInt mi, int i) { + if (mi.value != i) { + mi.value = i; + return true; + } + return false; + } + } +} diff --git a/services/core/java/com/android/server/display/HysteresisLevels.java b/services/core/java/com/android/server/display/HysteresisLevels.java index 34134892552f..abf8fe3d3f17 100644 --- a/services/core/java/com/android/server/display/HysteresisLevels.java +++ b/services/core/java/com/android/server/display/HysteresisLevels.java @@ -36,8 +36,7 @@ public class HysteresisLevels { private final float mMinBrightening; /** - * Creates a {@code HysteresisLevels} object with the given equal-length - * integer arrays. + * Creates a {@code HysteresisLevels} object for ambient brightness. * @param brighteningThresholds an array of brightening hysteresis constraint constants. * @param darkeningThresholds an array of darkening hysteresis constraint constants. * @param thresholdLevels a monotonically increasing array of threshold levels. @@ -59,6 +58,28 @@ public class HysteresisLevels { } /** + * Creates a {@code HysteresisLevels} object for screen brightness. + * @param brighteningThresholds an array of brightening hysteresis constraint constants. + * @param darkeningThresholds an array of darkening hysteresis constraint constants. + * @param thresholdLevels a monotonically increasing array of threshold levels. + * @param minBrighteningThreshold the minimum value for which the brightening value needs to + * return. + * @param minDarkeningThreshold the minimum value for which the darkening value needs to return. + */ + HysteresisLevels(int[] brighteningThresholds, int[] darkeningThresholds, + float[] thresholdLevels, float minDarkeningThreshold, float minBrighteningThreshold) { + if (brighteningThresholds.length != darkeningThresholds.length + || darkeningThresholds.length != thresholdLevels.length + 1) { + throw new IllegalArgumentException("Mismatch between hysteresis array lengths."); + } + mBrighteningThresholds = setArrayFormat(brighteningThresholds, 1000.0f); + mDarkeningThresholds = setArrayFormat(darkeningThresholds, 1000.0f); + mThresholdLevels = constraintInRangeIfNeeded(thresholdLevels); + mMinDarkening = minDarkeningThreshold; + mMinBrightening = minBrighteningThreshold; + } + + /** * Return the brightening hysteresis threshold for the given value level. */ public float getBrighteningThreshold(float value) { @@ -104,11 +125,42 @@ public class HysteresisLevels { private float[] setArrayFormat(int[] configArray, float divideFactor) { float[] levelArray = new float[configArray.length]; for (int index = 0; levelArray.length > index; ++index) { - levelArray[index] = (float)configArray[index] / divideFactor; + levelArray[index] = (float) configArray[index] / divideFactor; } return levelArray; } + /** + * This check is due to historical reasons, where screen thresholdLevels used to be + * integer values in the range of [0-255], but then was changed to be float values from [0,1]. + * To accommodate both the possibilities, we first check if all the thresholdLevels are in [0, + * 1], and if not, we divide all the levels with 255 to bring them down to the same scale. + */ + private float[] constraintInRangeIfNeeded(float[] thresholdLevels) { + if (isAllInRange(thresholdLevels, /* minValueInclusive = */ 0.0f, /* maxValueInclusive = */ + 1.0f)) { + return thresholdLevels; + } + + Slog.w(TAG, "Detected screen thresholdLevels on a deprecated brightness scale"); + float[] thresholdLevelsScaled = new float[thresholdLevels.length]; + for (int index = 0; thresholdLevels.length > index; ++index) { + thresholdLevelsScaled[index] = thresholdLevels[index] / 255.0f; + } + return thresholdLevelsScaled; + } + + private boolean isAllInRange(float[] configArray, float minValueInclusive, + float maxValueInclusive) { + int configArraySize = configArray.length; + for (int index = 0; configArraySize > index; ++index) { + if (configArray[index] < minValueInclusive || configArray[index] > maxValueInclusive) { + return false; + } + } + return true; + } + void dump(PrintWriter pw) { pw.println("HysteresisLevels"); pw.println(" mBrighteningThresholds=" + Arrays.toString(mBrighteningThresholds)); diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java index b51f3f587659..21a851895c15 100644 --- a/services/core/java/com/android/server/display/color/ColorDisplayService.java +++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java @@ -50,6 +50,7 @@ import android.database.ContentObserver; import android.hardware.display.ColorDisplayManager; import android.hardware.display.ColorDisplayManager.AutoMode; import android.hardware.display.ColorDisplayManager.ColorMode; +import android.hardware.display.DisplayManagerInternal; import android.hardware.display.IColorDisplayManager; import android.hardware.display.Time; import android.net.Uri; @@ -154,7 +155,8 @@ public final class ColorDisplayService extends SystemService { @VisibleForTesting final DisplayWhiteBalanceTintController mDisplayWhiteBalanceTintController = - new DisplayWhiteBalanceTintController(); + new DisplayWhiteBalanceTintController( + LocalServices.getService(DisplayManagerInternal.class)); private final NightDisplayTintController mNightDisplayTintController = new NightDisplayTintController(); private final TintController mGlobalSaturationTintController = diff --git a/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java b/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java index 93a78c1507ad..c5dd6acdd008 100644 --- a/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java +++ b/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java @@ -16,6 +16,8 @@ package com.android.server.display.color; +import static android.view.Display.DEFAULT_DISPLAY; + import static com.android.server.display.color.DisplayTransformManager.LEVEL_COLOR_MATRIX_DISPLAY_WHITE_BALANCE; import android.annotation.NonNull; @@ -24,10 +26,9 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.ColorSpace; import android.hardware.display.ColorDisplayManager; +import android.hardware.display.DisplayManagerInternal; import android.opengl.Matrix; -import android.os.IBinder; import android.util.Slog; -import android.view.SurfaceControl; import android.view.SurfaceControl.DisplayPrimaries; import com.android.internal.R; @@ -64,6 +65,12 @@ final class DisplayWhiteBalanceTintController extends TintController { // This feature becomes disallowed if the device is in an unsupported strong/light state. private boolean mIsAllowed = true; + private final DisplayManagerInternal mDisplayManagerInternal; + + DisplayWhiteBalanceTintController(DisplayManagerInternal dm) { + mDisplayManagerInternal = dm; + } + @Override public void setUp(Context context, boolean needsLinear) { mSetUp = false; @@ -287,12 +294,8 @@ final class DisplayWhiteBalanceTintController extends TintController { } private ColorSpace.Rgb getDisplayColorSpaceFromSurfaceControl() { - final IBinder displayToken = SurfaceControl.getInternalDisplayToken(); - if (displayToken == null) { - return null; - } - - DisplayPrimaries primaries = SurfaceControl.getDisplayNativePrimaries(displayToken); + DisplayPrimaries primaries = + mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY); if (primaries == null || primaries.red == null || primaries.green == null || primaries.blue == null || primaries.white == null) { return null; diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java index 1d1057f8ae9f..9bd48f2e0a58 100644 --- a/services/core/java/com/android/server/location/LocationManagerService.java +++ b/services/core/java/com/android/server/location/LocationManagerService.java @@ -115,6 +115,7 @@ import com.android.server.location.injector.Injector; import com.android.server.location.injector.LocationPermissionsHelper; import com.android.server.location.injector.LocationPowerSaveModeHelper; import com.android.server.location.injector.LocationUsageLogger; +import com.android.server.location.injector.PackageResetHelper; import com.android.server.location.injector.ScreenInteractiveHelper; import com.android.server.location.injector.SettingsHelper; import com.android.server.location.injector.SystemAlarmHelper; @@ -125,6 +126,7 @@ import com.android.server.location.injector.SystemDeviceStationaryHelper; import com.android.server.location.injector.SystemEmergencyHelper; import com.android.server.location.injector.SystemLocationPermissionsHelper; import com.android.server.location.injector.SystemLocationPowerSaveModeHelper; +import com.android.server.location.injector.SystemPackageResetHelper; import com.android.server.location.injector.SystemScreenInteractiveHelper; import com.android.server.location.injector.SystemSettingsHelper; import com.android.server.location.injector.SystemUserInfoHelper; @@ -1696,11 +1698,13 @@ public class LocationManagerService extends ILocationManager.Stub implements private final SystemDeviceStationaryHelper mDeviceStationaryHelper; private final SystemDeviceIdleHelper mDeviceIdleHelper; private final LocationUsageLogger mLocationUsageLogger; + private final PackageResetHelper mPackageResetHelper; // lazily instantiated since they may not always be used @GuardedBy("this") - private @Nullable SystemEmergencyHelper mEmergencyCallHelper; + @Nullable + private SystemEmergencyHelper mEmergencyCallHelper; @GuardedBy("this") private boolean mSystemReady; @@ -1721,6 +1725,7 @@ public class LocationManagerService extends ILocationManager.Stub implements mDeviceStationaryHelper = new SystemDeviceStationaryHelper(); mDeviceIdleHelper = new SystemDeviceIdleHelper(context); mLocationUsageLogger = new LocationUsageLogger(); + mPackageResetHelper = new SystemPackageResetHelper(context); } synchronized void onSystemReady() { @@ -1811,5 +1816,10 @@ public class LocationManagerService extends ILocationManager.Stub implements public LocationUsageLogger getLocationUsageLogger() { return mLocationUsageLogger; } + + @Override + public PackageResetHelper getPackageResetHelper() { + return mPackageResetHelper; + } } } diff --git a/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java b/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java index 82bcca2b8470..e7f6e67f13ad 100644 --- a/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java +++ b/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java @@ -39,6 +39,7 @@ import com.android.server.LocalServices; import com.android.server.location.injector.AppForegroundHelper; import com.android.server.location.injector.Injector; import com.android.server.location.injector.LocationPermissionsHelper; +import com.android.server.location.injector.PackageResetHelper; import com.android.server.location.injector.SettingsHelper; import com.android.server.location.injector.UserInfoHelper; import com.android.server.location.injector.UserInfoHelper.UserListener; @@ -193,6 +194,7 @@ public abstract class GnssListenerMultiplexer<TRequest, TListener extends IInter protected final LocationPermissionsHelper mLocationPermissionsHelper; protected final AppForegroundHelper mAppForegroundHelper; protected final LocationManagerInternal mLocationManagerInternal; + private final PackageResetHelper mPackageResetHelper; private final UserListener mUserChangedListener = this::onUserChanged; private final ProviderEnabledListener mProviderEnabledChangedListener = @@ -218,12 +220,25 @@ public abstract class GnssListenerMultiplexer<TRequest, TListener extends IInter }; private final AppForegroundHelper.AppForegroundListener mAppForegroundChangedListener = this::onAppForegroundChanged; + private final PackageResetHelper.Responder mPackageResetResponder = + new PackageResetHelper.Responder() { + @Override + public void onPackageReset(String packageName) { + GnssListenerMultiplexer.this.onPackageReset(packageName); + } + + @Override + public boolean isResetableForPackage(String packageName) { + return GnssListenerMultiplexer.this.isResetableForPackage(packageName); + } + }; protected GnssListenerMultiplexer(Injector injector) { mUserInfoHelper = injector.getUserInfoHelper(); mSettingsHelper = injector.getSettingsHelper(); mLocationPermissionsHelper = injector.getLocationPermissionsHelper(); mAppForegroundHelper = injector.getAppForegroundHelper(); + mPackageResetHelper = injector.getPackageResetHelper(); mLocationManagerInternal = Objects.requireNonNull( LocalServices.getService(LocationManagerInternal.class)); } @@ -357,6 +372,7 @@ public abstract class GnssListenerMultiplexer<TRequest, TListener extends IInter mLocationPackageBlacklistChangedListener); mLocationPermissionsHelper.addListener(mLocationPermissionsListener); mAppForegroundHelper.addListener(mAppForegroundChangedListener); + mPackageResetHelper.register(mPackageResetResponder); } @Override @@ -374,6 +390,7 @@ public abstract class GnssListenerMultiplexer<TRequest, TListener extends IInter mLocationPackageBlacklistChangedListener); mLocationPermissionsHelper.removeListener(mLocationPermissionsListener); mAppForegroundHelper.removeListener(mAppForegroundChangedListener); + mPackageResetHelper.unregister(mPackageResetResponder); } private void onUserChanged(int userId, int change) { @@ -407,6 +424,27 @@ public abstract class GnssListenerMultiplexer<TRequest, TListener extends IInter updateRegistrations(registration -> registration.onForegroundChanged(uid, foreground)); } + private void onPackageReset(String packageName) { + // invoked when a package is "force quit" - move off the main thread + FgThread.getExecutor().execute( + () -> + updateRegistrations( + registration -> { + if (registration.getIdentity().getPackageName().equals( + packageName)) { + registration.remove(); + } + + return false; + })); + } + + private boolean isResetableForPackage(String packageName) { + // invoked to find out if the given package has any state that can be "force quit" + return findRegistration( + registration -> registration.getIdentity().getPackageName().equals(packageName)); + } + @Override protected String getServiceState() { if (!isSupported()) { diff --git a/services/core/java/com/android/server/location/injector/Injector.java b/services/core/java/com/android/server/location/injector/Injector.java index c0ce3a6853cf..b2c86721fdcc 100644 --- a/services/core/java/com/android/server/location/injector/Injector.java +++ b/services/core/java/com/android/server/location/injector/Injector.java @@ -63,4 +63,7 @@ public interface Injector { /** Returns a LocationUsageLogger. */ LocationUsageLogger getLocationUsageLogger(); + + /** Returns a PackageResetHelper. */ + PackageResetHelper getPackageResetHelper(); } diff --git a/services/core/java/com/android/server/location/injector/PackageResetHelper.java b/services/core/java/com/android/server/location/injector/PackageResetHelper.java new file mode 100644 index 000000000000..721c576b1b10 --- /dev/null +++ b/services/core/java/com/android/server/location/injector/PackageResetHelper.java @@ -0,0 +1,101 @@ +/* + * 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.location.injector; + +import static com.android.server.location.LocationManagerService.D; +import static com.android.server.location.LocationManagerService.TAG; + +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** Helpers for tracking queries and resets of package state. */ +public abstract class PackageResetHelper { + + /** Interface for responding to reset events. */ + public interface Responder { + + /** + * Called when a package's runtime state is being reset for whatever reason and any + * components carrying runtime state on behalf of the package should clear that state. + * + * @param packageName The name of the package. + */ + void onPackageReset(String packageName); + + /** + * Called when the system queries whether this package has any active state for the given + * package. Should return true if the component has some runtime state that is resetable of + * behalf of the given package, and false otherwise. + * + * @param packageName The name of the package. + * @return True if this component has resetable state for the given package. + */ + boolean isResetableForPackage(String packageName); + } + + private final CopyOnWriteArrayList<Responder> mResponders; + + public PackageResetHelper() { + mResponders = new CopyOnWriteArrayList<>(); + } + + /** Begin listening for package reset events. */ + public synchronized void register(Responder responder) { + boolean empty = mResponders.isEmpty(); + mResponders.add(responder); + if (empty) { + onRegister(); + } + } + + /** Stop listening for package reset events. */ + public synchronized void unregister(Responder responder) { + mResponders.remove(responder); + if (mResponders.isEmpty()) { + onUnregister(); + } + } + + @GuardedBy("this") + protected abstract void onRegister(); + + @GuardedBy("this") + protected abstract void onUnregister(); + + protected final void notifyPackageReset(String packageName) { + if (D) { + Log.d(TAG, "package " + packageName + " reset"); + } + + for (Responder responder : mResponders) { + responder.onPackageReset(packageName); + } + } + + protected final boolean queryResetableForPackage(String packageName) { + for (Responder responder : mResponders) { + if (responder.isResetableForPackage(packageName)) { + return true; + } + } + + return false; + } +} diff --git a/services/core/java/com/android/server/location/injector/SystemPackageResetHelper.java b/services/core/java/com/android/server/location/injector/SystemPackageResetHelper.java new file mode 100644 index 000000000000..91b0212df734 --- /dev/null +++ b/services/core/java/com/android/server/location/injector/SystemPackageResetHelper.java @@ -0,0 +1,140 @@ +/* + * 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.location.injector; + +import android.annotation.Nullable; +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.Uri; + +import com.android.internal.util.Preconditions; + +/** Listens to appropriate broadcasts for queries and resets. */ +public class SystemPackageResetHelper extends PackageResetHelper { + + private final Context mContext; + + @Nullable + private BroadcastReceiver mReceiver; + + public SystemPackageResetHelper(Context context) { + mContext = context; + } + + @Override + protected void onRegister() { + Preconditions.checkState(mReceiver == null); + mReceiver = new Receiver(); + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART); + filter.addDataScheme("package"); + + // We don't filter for Intent.ACTION_PACKAGE_DATA_CLEARED as 1) it refers to persistent + // data, and 2) it should always be preceded by Intent.ACTION_PACKAGE_RESTARTED, which + // refers to runtime data. in this way we also avoid redundant callbacks. + + mContext.registerReceiver(mReceiver, filter); + } + + @Override + protected void onUnregister() { + Preconditions.checkState(mReceiver != null); + mContext.unregisterReceiver(mReceiver); + mReceiver = null; + } + + private class Receiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action == null) { + return; + } + + Uri data = intent.getData(); + if (data == null) { + return; + } + + String packageName = data.getSchemeSpecificPart(); + if (packageName == null) { + return; + } + + switch (action) { + case Intent.ACTION_QUERY_PACKAGE_RESTART: + String[] packages = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); + if (packages != null) { + // it would be more efficient to pass through the whole array, but at the + // moment the array is always size 1, and this makes for a nicer callback. + for (String pkg : packages) { + if (queryResetableForPackage(pkg)) { + setResultCode(Activity.RESULT_OK); + break; + } + } + } + break; + case Intent.ACTION_PACKAGE_CHANGED: + // make sure this is an enabled/disabled change to the package as a whole, not + // just some of its components. This avoids unnecessary work in the callback. + boolean isPackageChange = false; + String[] components = intent.getStringArrayExtra( + Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); + if (components != null) { + for (String component : components) { + if (packageName.equals(component)) { + isPackageChange = true; + break; + } + } + } + + if (isPackageChange) { + try { + ApplicationInfo appInfo = + context.getPackageManager().getApplicationInfo(packageName, + PackageManager.ApplicationInfoFlags.of(0)); + if (!appInfo.enabled) { + notifyPackageReset(packageName); + } + } catch (PackageManager.NameNotFoundException e) { + return; + } + } + break; + case Intent.ACTION_PACKAGE_REMOVED: + // fall through + case Intent.ACTION_PACKAGE_RESTARTED: + notifyPackageReset(packageName); + break; + default: + break; + } + } + } +} 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 a69a079b679d..bd7525133624 100644 --- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java +++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java @@ -106,6 +106,7 @@ import com.android.server.location.injector.LocationPermissionsHelper.LocationPe import com.android.server.location.injector.LocationPowerSaveModeHelper; import com.android.server.location.injector.LocationPowerSaveModeHelper.LocationPowerSaveModeChangedListener; import com.android.server.location.injector.LocationUsageLogger; +import com.android.server.location.injector.PackageResetHelper; import com.android.server.location.injector.ScreenInteractiveHelper; import com.android.server.location.injector.ScreenInteractiveHelper.ScreenInteractiveChangedListener; import com.android.server.location.injector.SettingsHelper; @@ -1373,6 +1374,7 @@ public class LocationProviderManager extends protected final ScreenInteractiveHelper mScreenInteractiveHelper; protected final LocationUsageLogger mLocationUsageLogger; protected final LocationFudger mLocationFudger; + private final PackageResetHelper mPackageResetHelper; private final UserListener mUserChangedListener = this::onUserChanged; private final LocationSettings.LocationUserSettingsListener mLocationUserSettingsListener = @@ -1407,6 +1409,18 @@ public class LocationProviderManager extends this::onLocationPowerSaveModeChanged; private final ScreenInteractiveChangedListener mScreenInteractiveChangedListener = this::onScreenInteractiveChanged; + private final PackageResetHelper.Responder mPackageResetResponder = + new PackageResetHelper.Responder() { + @Override + public void onPackageReset(String packageName) { + LocationProviderManager.this.onPackageReset(packageName); + } + + @Override + public boolean isResetableForPackage(String packageName) { + return LocationProviderManager.this.isResetableForPackage(packageName); + } + }; // acquiring mMultiplexerLock makes operations on mProvider atomic, but is otherwise unnecessary protected final MockableLocationProvider mProvider; @@ -1442,6 +1456,7 @@ public class LocationProviderManager extends mScreenInteractiveHelper = injector.getScreenInteractiveHelper(); mLocationUsageLogger = injector.getLocationUsageLogger(); mLocationFudger = new LocationFudger(mSettingsHelper.getCoarseLocationAccuracyM()); + mPackageResetHelper = injector.getPackageResetHelper(); mProvider = new MockableLocationProvider(mMultiplexerLock); @@ -1970,6 +1985,7 @@ public class LocationProviderManager extends mAppForegroundHelper.addListener(mAppForegroundChangedListener); mLocationPowerSaveModeHelper.addListener(mLocationPowerSaveModeChangedListener); mScreenInteractiveHelper.addListener(mScreenInteractiveChangedListener); + mPackageResetHelper.register(mPackageResetResponder); } @GuardedBy("mMultiplexerLock") @@ -1988,6 +2004,7 @@ public class LocationProviderManager extends mAppForegroundHelper.removeListener(mAppForegroundChangedListener); mLocationPowerSaveModeHelper.removeListener(mLocationPowerSaveModeChangedListener); mScreenInteractiveHelper.removeListener(mScreenInteractiveChangedListener); + mPackageResetHelper.unregister(mPackageResetResponder); } @GuardedBy("mMultiplexerLock") @@ -2391,6 +2408,27 @@ public class LocationProviderManager extends updateRegistrations(registration -> registration.onLocationPermissionsChanged(uid)); } + private void onPackageReset(String packageName) { + // invoked when a package is "force quit" - move off the main thread + FgThread.getExecutor().execute( + () -> + updateRegistrations( + registration -> { + if (registration.getIdentity().getPackageName().equals( + packageName)) { + registration.remove(); + } + + return false; + })); + } + + private boolean isResetableForPackage(String packageName) { + // invoked to find out if the given package has any state that can be "force quit" + return findRegistration( + registration -> registration.getIdentity().getPackageName().equals(packageName)); + } + @GuardedBy("mMultiplexerLock") @Override public void onStateChanged( diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index bfa8af957208..92a63295f8a4 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -601,6 +601,26 @@ class MediaRouter2ServiceImpl { } } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + pw.println(prefix + "MediaRouter2ServiceImpl"); + + String indent = prefix + " "; + + synchronized (mLock) { + pw.println(indent + "mNextRouterOrManagerId=" + mNextRouterOrManagerId.get()); + pw.println(indent + "mCurrentUserId=" + mCurrentUserId); + + pw.println(indent + "UserRecords:"); + if (mUserRecords.size() > 0) { + for (int i = 0; i < mUserRecords.size(); i++) { + mUserRecords.get(i).dump(pw, indent + " "); + } + } else { + pw.println(indent + "<no user records>"); + } + } + } + //TODO(b/136703681): Review this is handling multi-user properly. void switchUser() { synchronized (mLock) { @@ -1197,6 +1217,41 @@ class MediaRouter2ServiceImpl { } return null; } + + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + pw.println(prefix + "UserRecord"); + + String indent = prefix + " "; + + pw.println(indent + "mUserId=" + mUserId); + + pw.println(indent + "Router Records:"); + if (!mRouterRecords.isEmpty()) { + for (RouterRecord routerRecord : mRouterRecords) { + routerRecord.dump(pw, indent + " "); + } + } else { + pw.println(indent + "<no router records>"); + } + + pw.println(indent + "Manager Records:"); + if (!mManagerRecords.isEmpty()) { + for (ManagerRecord managerRecord : mManagerRecords) { + managerRecord.dump(pw, indent + " "); + } + } else { + pw.println(indent + "<no manager records>"); + } + + if (!mHandler.runWithScissors(new Runnable() { + @Override + public void run() { + mHandler.dump(pw, indent); + } + }, 1000)) { + pw.println(indent + "<could not dump handler state>"); + } + } } final class RouterRecord implements IBinder.DeathRecipient { @@ -1236,6 +1291,22 @@ class MediaRouter2ServiceImpl { public void binderDied() { routerDied(this); } + + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + pw.println(prefix + "RouterRecord"); + + String indent = prefix + " "; + + pw.println(indent + "mPackageName=" + mPackageName); + pw.println(indent + "mSelectRouteSequenceNumbers=" + mSelectRouteSequenceNumbers); + pw.println(indent + "mUid=" + mUid); + pw.println(indent + "mPid=" + mPid); + pw.println(indent + "mHasConfigureWifiDisplayPermission=" + + mHasConfigureWifiDisplayPermission); + pw.println(indent + "mHasModifyAudioRoutingPermission=" + + mHasModifyAudioRoutingPermission); + pw.println(indent + "mRouterId=" + mRouterId); + } } final class ManagerRecord implements IBinder.DeathRecipient { @@ -1267,8 +1338,20 @@ class MediaRouter2ServiceImpl { managerDied(this); } - public void dump(PrintWriter pw, String prefix) { - pw.println(prefix + this); + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + pw.println(prefix + "ManagerRecord"); + + String indent = prefix + " "; + + pw.println(indent + "mPackageName=" + mPackageName); + pw.println(indent + "mManagerId=" + mManagerId); + pw.println(indent + "mUid=" + mUid); + pw.println(indent + "mPid=" + mPid); + pw.println(indent + "mIsScanning=" + mIsScanning); + + if (mLastSessionCreationRequest != null) { + mLastSessionCreationRequest.dump(pw, indent); + } } public void startScan() { @@ -1455,6 +1538,15 @@ class MediaRouter2ServiceImpl { } } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + pw.println(prefix + "UserHandler"); + + String indent = prefix + " "; + pw.println(indent + "mRunning=" + mRunning); + + mWatcher.dump(pw, prefix); + } + private void onProviderStateChangedOnHandler(@NonNull MediaRoute2Provider provider) { MediaRoute2ProviderInfo currentInfo = provider.getProviderInfo(); @@ -2340,5 +2432,14 @@ class MediaRouter2ServiceImpl { mOldSession = oldSession; mRoute = route; } + + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + pw.println(prefix + "SessionCreationRequest"); + + String indent = prefix + " "; + + pw.println(indent + "mUniqueRequestId=" + mUniqueRequestId); + pw.println(indent + "mManagerRequestId=" + mManagerRequestId); + } } } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 4806b522eca6..4f0da7952867 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -387,6 +387,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub userRecord.dump(pw, ""); } } + + pw.println(); + mService2.dump(pw, ""); } // Binder call diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java index 14140b5fe264..5fa5de75eaf3 100644 --- a/services/core/java/com/android/server/pm/AppsFilterBase.java +++ b/services/core/java/com/android/server/pm/AppsFilterBase.java @@ -28,7 +28,6 @@ import android.content.pm.SigningDetails; import android.os.Binder; import android.os.Handler; import android.os.Process; -import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.text.TextUtils; @@ -199,6 +198,7 @@ public abstract class AppsFilterBase implements AppsFilterSnapshot { protected SnapshotCache<WatchedSparseBooleanMatrix> mShouldFilterCacheSnapshot; protected volatile boolean mCacheReady = false; + protected volatile boolean mCacheEnabled = true; protected static final boolean CACHE_VALID = true; protected static final boolean CACHE_INVALID = false; @@ -342,8 +342,7 @@ public abstract class AppsFilterBase implements AppsFilterSnapshot { && !isImplicitlyQueryable(callingAppId, targetPkgSetting.getAppId()); } // use cache - if (mCacheReady && SystemProperties.getBoolean("debug.pm.use_app_filter_cache", - true)) { + if (mCacheReady && mCacheEnabled) { if (!shouldFilterApplicationUsingCache(callingUid, targetPkgSetting.getAppId(), userId)) { diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index 79d72a3ad7f0..4c21195e2890 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -36,6 +36,7 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.SigningDetails; import android.content.pm.UserInfo; import android.os.Handler; +import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.provider.DeviceConfig; @@ -223,6 +224,12 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, return new AppsFilterSnapshotImpl(AppsFilterImpl.this); } }; + readCacheEnabledSysProp(); + SystemProperties.addChangeCallback(this::readCacheEnabledSysProp); + } + + private void readCacheEnabledSysProp() { + mCacheEnabled = SystemProperties.getBoolean("debug.pm.use_app_filter_cache", true); } /** diff --git a/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java b/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java index 019c853471fe..4e268a277102 100644 --- a/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java @@ -74,6 +74,7 @@ public final class AppsFilterSnapshotImpl extends AppsFilterBase { // cache is not ready, use an empty cache for the snapshot mShouldFilterCache = new WatchedSparseBooleanMatrix(); } + mCacheEnabled = orig.mCacheEnabled; mShouldFilterCacheSnapshot = new SnapshotCache.Sealed<>(); mBackgroundHandler = null; diff --git a/services/core/java/com/android/server/pm/InitAppsHelper.java b/services/core/java/com/android/server/pm/InitAppsHelper.java index 797d4c3cfed3..f6472a739979 100644 --- a/services/core/java/com/android/server/pm/InitAppsHelper.java +++ b/services/core/java/com/android/server/pm/InitAppsHelper.java @@ -33,11 +33,9 @@ import static com.android.server.pm.PackageManagerService.SCAN_REQUIRE_KNOWN; import static com.android.server.pm.PackageManagerService.SYSTEM_PARTITIONS; import static com.android.server.pm.PackageManagerService.TAG; import static com.android.server.pm.pkg.parsing.ParsingPackageUtils.PARSE_APK_IN_APEX; -import static com.android.server.pm.pkg.parsing.ParsingPackageUtils.PARSE_FRAMEWORK_RES_SPLITS; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.pm.parsing.ApkLiteParseUtils; import android.os.Environment; import android.os.SystemClock; import android.os.Trace; @@ -121,29 +119,6 @@ final class InitAppsHelper { mExecutorService = ParallelPackageParser.makeExecutorService(); } - private List<File> getFrameworkResApkSplitFiles() { - Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanFrameworkResApkSplits"); - try { - final List<File> splits = new ArrayList<>(); - final List<ApexManager.ActiveApexInfo> activeApexInfos = - mPm.mApexManager.getActiveApexInfos(); - for (int i = 0; i < activeApexInfos.size(); i++) { - ApexManager.ActiveApexInfo apexInfo = activeApexInfos.get(i); - File splitsFolder = new File(apexInfo.apexDirectory, "etc/splits"); - if (splitsFolder.isDirectory()) { - for (File file : splitsFolder.listFiles()) { - if (ApkLiteParseUtils.isApkFile(file)) { - splits.add(file); - } - } - } - } - return splits; - } finally { - Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); - } - } - private List<ScanPartition> getSystemScanPartitions() { final List<ScanPartition> scanPartitions = new ArrayList<>(); scanPartitions.addAll(mSystemPartitions); @@ -270,7 +245,7 @@ final class InitAppsHelper { long startTime) { EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START, SystemClock.uptimeMillis()); - scanDirTracedLI(mPm.getAppInstallDir(), /* frameworkSplits= */ null, 0, + scanDirTracedLI(mPm.getAppInstallDir(), 0, mScanFlags | SCAN_REQUIRE_KNOWN, packageParser, mExecutorService); List<Runnable> unfinishedTasks = mExecutorService.shutdownNow(); @@ -338,15 +313,13 @@ final class InitAppsHelper { if (partition.getOverlayFolder() == null) { continue; } - scanDirTracedLI(partition.getOverlayFolder(), /* frameworkSplits= */ null, + scanDirTracedLI(partition.getOverlayFolder(), mSystemParseFlags, mSystemScanFlags | partition.scanFlag, packageParser, executorService); } - List<File> frameworkSplits = getFrameworkResApkSplitFiles(); - scanDirTracedLI(frameworkDir, frameworkSplits, - mSystemParseFlags | PARSE_FRAMEWORK_RES_SPLITS, - mSystemScanFlags | SCAN_NO_DEX | SCAN_AS_PRIVILEGED, + scanDirTracedLI(frameworkDir, + mSystemParseFlags, mSystemScanFlags | SCAN_NO_DEX | SCAN_AS_PRIVILEGED, packageParser, executorService); if (!mPm.mPackages.containsKey("android")) { throw new IllegalStateException( @@ -356,12 +329,12 @@ final class InitAppsHelper { for (int i = 0, size = mDirsToScanAsSystem.size(); i < size; i++) { final ScanPartition partition = mDirsToScanAsSystem.get(i); if (partition.getPrivAppFolder() != null) { - scanDirTracedLI(partition.getPrivAppFolder(), /* frameworkSplits= */ null, + scanDirTracedLI(partition.getPrivAppFolder(), mSystemParseFlags, mSystemScanFlags | SCAN_AS_PRIVILEGED | partition.scanFlag, packageParser, executorService); } - scanDirTracedLI(partition.getAppFolder(), /* frameworkSplits= */ null, + scanDirTracedLI(partition.getAppFolder(), mSystemParseFlags, mSystemScanFlags | partition.scanFlag, packageParser, executorService); } @@ -379,7 +352,7 @@ final class InitAppsHelper { } @GuardedBy({"mPm.mInstallLock", "mPm.mLock"}) - private void scanDirTracedLI(File scanDir, List<File> frameworkSplits, + private void scanDirTracedLI(File scanDir, int parseFlags, int scanFlags, PackageParser2 packageParser, ExecutorService executorService) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanDir [" + scanDir.getAbsolutePath() + "]"); @@ -388,7 +361,7 @@ final class InitAppsHelper { // when scanning apk in apexes, we want to check the maxSdkVersion parseFlags |= PARSE_APK_IN_APEX; } - mInstallPackageHelper.installPackagesFromDir(scanDir, frameworkSplits, parseFlags, + mInstallPackageHelper.installPackagesFromDir(scanDir, parseFlags, scanFlags, packageParser, executorService); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index f6e5b690fd2e..1746d93c99b9 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -3472,7 +3472,7 @@ final class InstallPackageHelper { } @GuardedBy({"mPm.mInstallLock", "mPm.mLock"}) - public void installPackagesFromDir(File scanDir, List<File> frameworkSplits, int parseFlags, + public void installPackagesFromDir(File scanDir, int parseFlags, int scanFlags, PackageParser2 packageParser, ExecutorService executorService) { final File[] files = scanDir.listFiles(); @@ -3486,7 +3486,7 @@ final class InstallPackageHelper { + " flags=0x" + Integer.toHexString(parseFlags)); } ParallelPackageParser parallelPackageParser = - new ParallelPackageParser(packageParser, executorService, frameworkSplits); + new ParallelPackageParser(packageParser, executorService); // Submit files for parsing in parallel int fileCount = 0; diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 09bc25bc8b2f..9481f8a7da87 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -6541,7 +6541,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService if (dependentState == null) { continue; } - if (!Objects.equals(dependentState.getUserStateOrDefault(userId) + if (canSetOverlayPaths(dependentState.getUserStateOrDefault(userId) .getSharedLibraryOverlayPaths() .get(libName), newOverlayPaths)) { String dependentPackageName = dependent.getPackageName(); @@ -6565,7 +6565,10 @@ public class PackageManagerService implements PackageSender, TestUtilityService } } - outUpdatedPackageNames.add(targetPackageName); + if (canSetOverlayPaths(packageState.getUserStateOrDefault(userId).getOverlayPaths(), + newOverlayPaths)) { + outUpdatedPackageNames.add(targetPackageName); + } } commitPackageStateMutation(null, mutator -> { @@ -6616,6 +6619,17 @@ public class PackageManagerService implements PackageSender, TestUtilityService invalidatePackageInfoCache(); } + private boolean canSetOverlayPaths(OverlayPaths origPaths, OverlayPaths newPaths) { + if (Objects.equals(origPaths, newPaths)) { + return false; + } + if ((origPaths == null && newPaths.isEmpty()) + || (newPaths == null && origPaths.isEmpty())) { + return false; + } + return true; + } + private void maybeUpdateSystemOverlays(String targetPackageName, OverlayPaths newOverlayPaths) { if (!mResolverReplaced) { if (targetPackageName.equals("android")) { diff --git a/services/core/java/com/android/server/pm/ParallelPackageParser.java b/services/core/java/com/android/server/pm/ParallelPackageParser.java index 45030bfa59ce..56258844a3fe 100644 --- a/services/core/java/com/android/server/pm/ParallelPackageParser.java +++ b/services/core/java/com/android/server/pm/ParallelPackageParser.java @@ -27,7 +27,6 @@ import com.android.server.pm.parsing.PackageParser2; import com.android.server.pm.parsing.pkg.ParsedPackage; import java.io.File; -import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; @@ -55,17 +54,9 @@ class ParallelPackageParser { private final ExecutorService mExecutorService; - private final List<File> mFrameworkSplits; - ParallelPackageParser(PackageParser2 packageParser, ExecutorService executorService) { - this(packageParser, executorService, /* frameworkSplits= */ null); - } - - ParallelPackageParser(PackageParser2 packageParser, ExecutorService executorService, - List<File> frameworkSplits) { mPackageParser = packageParser; mExecutorService = executorService; - mFrameworkSplits = frameworkSplits; } static class ParseResult { @@ -134,6 +125,6 @@ class ParallelPackageParser { @VisibleForTesting protected ParsedPackage parsePackage(File scanFile, int parseFlags) throws PackageManagerException { - return mPackageParser.parsePackage(scanFile, parseFlags, true, mFrameworkSplits); + return mPackageParser.parsePackage(scanFile, parseFlags, true); } } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 3b3e1db0f35d..c77459da8cec 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -2048,8 +2048,8 @@ public class UserManagerService extends IUserManager.Stub { } } - @Override - public boolean isUserSwitcherEnabled(@UserIdInt int mUserId) { + @VisibleForTesting + boolean isUserSwitcherEnabled(@UserIdInt int mUserId) { boolean multiUserSettingOn = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.USER_SWITCHER_ENABLED, Resources.getSystem().getBoolean(com.android.internal @@ -2062,6 +2062,33 @@ public class UserManagerService extends IUserManager.Stub { } @Override + public boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable, + @UserIdInt int mUserId) { + if (!isUserSwitcherEnabled(mUserId)) { + return false; + } + // The feature is enabled. But is it worth showing? + return showEvenIfNotActionable + || !hasUserRestriction(UserManager.DISALLOW_ADD_USER, mUserId) // Can add new user + || areThereMultipleSwitchableUsers(); // There are switchable users + } + + /** Returns true if there is more than one user that can be switched to. */ + private boolean areThereMultipleSwitchableUsers() { + List<UserInfo> aliveUsers = getUsers(true, true, true); + boolean isAnyAliveUser = false; + for (UserInfo userInfo : aliveUsers) { + if (userInfo.supportsSwitchToByUser()) { + if (isAnyAliveUser) { + return true; + } + isAnyAliveUser = true; + } + } + return false; + } + + @Override public boolean isRestricted(@UserIdInt int userId) { if (userId != UserHandle.getCallingUserId()) { checkCreateUsersPermission("query isRestricted for user " + userId); @@ -7072,4 +7099,5 @@ public class UserManagerService extends IUserManager.Stub { } return mAmInternal; } + } diff --git a/services/core/java/com/android/server/pm/parsing/PackageParser2.java b/services/core/java/com/android/server/pm/parsing/PackageParser2.java index 6caddaf430dc..f5ba3f61a00c 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageParser2.java +++ b/services/core/java/com/android/server/pm/parsing/PackageParser2.java @@ -143,15 +143,6 @@ public class PackageParser2 implements AutoCloseable { @AnyThread public ParsedPackage parsePackage(File packageFile, int flags, boolean useCaches) throws PackageManagerException { - return parsePackage(packageFile, flags, useCaches, /* frameworkSplits= */ null); - } - - /** - * TODO(b/135203078): Document new package parsing - */ - @AnyThread - public ParsedPackage parsePackage(File packageFile, int flags, boolean useCaches, - List<File> frameworkSplits) throws PackageManagerException { var files = packageFile.listFiles(); // Apk directory is directly nested under the current directory if (ArrayUtils.size(files) == 1 && files[0].isDirectory()) { @@ -167,8 +158,7 @@ public class PackageParser2 implements AutoCloseable { long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0; ParseInput input = mSharedResult.get().reset(); - ParseResult<ParsingPackage> result = parsingUtils.parsePackage(input, packageFile, flags, - frameworkSplits); + ParseResult<ParsingPackage> result = parsingUtils.parsePackage(input, packageFile, flags); if (result.isError()) { throw new PackageManagerException(result.getErrorCode(), result.getErrorMessage(), result.getException()); 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 14f787e4fbea..a8d48aeaa577 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 @@ -238,7 +238,6 @@ public class ParsingPackageUtils { * of required system property within the overlay tag. */ public static final int PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY = 1 << 7; - public static final int PARSE_FRAMEWORK_RES_SPLITS = 1 << 8; public static final int PARSE_APK_IN_APEX = 1 << 9; public static final int PARSE_CHATTY = 1 << 31; @@ -257,7 +256,6 @@ public class ParsingPackageUtils { PARSE_IS_SYSTEM_DIR, PARSE_MUST_BE_APK, PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY, - PARSE_FRAMEWORK_RES_SPLITS }) @Retention(RetentionPolicy.SOURCE) public @interface ParseFlags {} @@ -307,7 +305,7 @@ public class ParsingPackageUtils { isCoreApp); } }); - var parseResult = parser.parsePackage(input, file, parseFlags, /* frameworkSplits= */ null); + var parseResult = parser.parsePackage(input, file, parseFlags); if (parseResult.isError()) { return input.error(parseResult); } @@ -356,14 +354,9 @@ public class ParsingPackageUtils { * not check whether {@code packageFile} has changed since the last parse, it's up to callers to * do so. */ - public ParseResult<ParsingPackage> parsePackage(ParseInput input, File packageFile, int flags, - List<File> frameworkSplits) { - if (((flags & PARSE_FRAMEWORK_RES_SPLITS) != 0) - && frameworkSplits.size() > 0 - && packageFile.getAbsolutePath().endsWith("/framework-res.apk")) { - return parseClusterPackage(input, packageFile, frameworkSplits, flags); - } else if (packageFile.isDirectory()) { - return parseClusterPackage(input, packageFile, /* frameworkSplits= */null, flags); + public ParseResult<ParsingPackage> parsePackage(ParseInput input, File packageFile, int flags) { + if (packageFile.isDirectory()) { + return parseClusterPackage(input, packageFile, flags); } else { return parseMonolithicPackage(input, packageFile, flags); } @@ -375,28 +368,17 @@ public class ParsingPackageUtils { * identical package name and version codes, a single base APK, and unique * split names. * <p> - * Can also be passed the framework-res.apk file and a list of split apks coming from apexes - * (via {@code frameworkSplits}) in which case they will be parsed similar to cluster packages - * even if they are in different folders. Note that this code path may have other behaviour - * differences. - * <p> * Note that this <em>does not</em> perform signature verification; that must be done separately * in {@link #getSigningDetails(ParseInput, ParsedPackage, boolean)}. */ private ParseResult<ParsingPackage> parseClusterPackage(ParseInput input, File packageDir, - List<File> frameworkSplits, int flags) { - // parseClusterPackageLite should receive no flags (0) for regular splits but we want to - // pass the flags for framework splits + int flags) { int liteParseFlags = 0; - if ((flags & PARSE_FRAMEWORK_RES_SPLITS) != 0) { - liteParseFlags = flags; - } if ((flags & PARSE_APK_IN_APEX) != 0) { liteParseFlags |= PARSE_APK_IN_APEX; } final ParseResult<PackageLite> liteResult = - ApkLiteParseUtils.parseClusterPackageLite(input, packageDir, frameworkSplits, - liteParseFlags); + ApkLiteParseUtils.parseClusterPackageLite(input, packageDir, liteParseFlags); if (liteResult.isError()) { return input.error(liteResult); } @@ -647,7 +629,7 @@ public class ParsingPackageUtils { final TypedArray manifestArray = res.obtainAttributes(parser, R.styleable.AndroidManifest); try { final boolean isCoreApp = parser.getAttributeBooleanValue(null /*namespace*/, - "coreApp",false); + "coreApp", false); final ParsingPackage pkg = mCallback.startParsingPackage( pkgName, apkPath, codePath, manifestArray, isCoreApp); final ParseResult<ParsingPackage> result = diff --git a/services/core/java/com/android/server/resources/ResourcesManagerService.java b/services/core/java/com/android/server/resources/ResourcesManagerService.java index cc275466a5ff..eec3a0259a3e 100644 --- a/services/core/java/com/android/server/resources/ResourcesManagerService.java +++ b/services/core/java/com/android/server/resources/ResourcesManagerService.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.IResourcesManager; +import android.content.res.ResourceTimer; import android.os.Binder; import android.os.IBinder; import android.os.ParcelFileDescriptor; @@ -55,7 +56,7 @@ public class ResourcesManagerService extends SystemService { @Override public void onStart() { - // Intentionally left empty. + ResourceTimer.start(); } private final IBinder mService = new IResourcesManager.Stub() { diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index e21feae65c97..886e8e687e5a 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -93,6 +93,7 @@ import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collection; import java.util.List; @@ -254,6 +255,7 @@ public class TrustManagerService extends SystemService { return; } if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { + checkNewAgents(); mPackageMonitor.register(mContext, mHandler.getLooper(), UserHandle.ALL, true); mReceiver.register(mContext); mLockPatternUtils.registerStrongAuthTracker(mStrongAuthTracker); @@ -262,7 +264,7 @@ public class TrustManagerService extends SystemService { refreshAgentList(UserHandle.USER_ALL); refreshDeviceLockedForUser(UserHandle.USER_ALL); } else if (phase == SystemService.PHASE_BOOT_COMPLETED) { - maybeEnableFactoryTrustAgents(mLockPatternUtils, UserHandle.USER_SYSTEM); + maybeEnableFactoryTrustAgents(UserHandle.USER_SYSTEM); } } @@ -685,7 +687,7 @@ public class TrustManagerService extends SystemService { */ public void lockUser(int userId) { mLockPatternUtils.requireStrongAuth( - StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, userId); + StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, userId); try { WindowManagerGlobal.getWindowManagerService().lockNow(null); } catch (RemoteException e) { @@ -1083,7 +1085,7 @@ public class TrustManagerService extends SystemService { return new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name); } - private void maybeEnableFactoryTrustAgents(LockPatternUtils utils, int userId) { + private void maybeEnableFactoryTrustAgents(int userId) { if (0 != Settings.Secure.getIntForUser(mContext.getContentResolver(), Settings.Secure.TRUST_AGENTS_INITIALIZED, 0, userId)) { return; @@ -1100,8 +1102,7 @@ public class TrustManagerService extends SystemService { } else { // A default agent is not set; perform regular trust agent discovery for (ResolveInfo resolveInfo : resolveInfos) { ComponentName componentName = getComponentName(resolveInfo); - int applicationInfoFlags = resolveInfo.serviceInfo.applicationInfo.flags; - if ((applicationInfoFlags & ApplicationInfo.FLAG_SYSTEM) == 0) { + if (!isSystemTrustAgent(resolveInfo)) { Log.i(TAG, "Leaving agent " + componentName + " disabled because package " + "is not a system package."); continue; @@ -1110,13 +1111,88 @@ public class TrustManagerService extends SystemService { } } - List<ComponentName> previouslyEnabledAgents = utils.getEnabledTrustAgents(userId); - discoveredAgents.addAll(previouslyEnabledAgents); - utils.setEnabledTrustAgents(discoveredAgents, userId); + enableNewAgents(discoveredAgents, userId); Settings.Secure.putIntForUser(mContext.getContentResolver(), Settings.Secure.TRUST_AGENTS_INITIALIZED, 1, userId); } + private void checkNewAgents() { + for (UserInfo userInfo : mUserManager.getAliveUsers()) { + checkNewAgentsForUser(userInfo.id); + } + } + + /** + * Checks for any new trust agents that become available after the first boot, add them to the + * list of known agents, and enable them if they should be enabled by default. + */ + private void checkNewAgentsForUser(int userId) { + // When KNOWN_TRUST_AGENTS_INITIALIZED is not set, only update the known agent list but do + // not enable any agent. + // These agents will be enabled by #maybeEnableFactoryTrustAgents if this is the first time + // that this device boots and TRUST_AGENTS_INITIALIZED is not already set. + // Otherwise, these agents may have been manually disabled by the user, and we should not + // re-enable them. + if (0 == Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.KNOWN_TRUST_AGENTS_INITIALIZED, 0, userId)) { + initializeKnownAgents(userId); + return; + } + + List<ComponentName> knownAgents = mLockPatternUtils.getKnownTrustAgents(userId); + List<ResolveInfo> agentInfoList = resolveAllowedTrustAgents(mContext.getPackageManager(), + userId); + ArraySet<ComponentName> newAgents = new ArraySet<>(agentInfoList.size()); + ArraySet<ComponentName> newSystemAgents = new ArraySet<>(agentInfoList.size()); + + for (ResolveInfo agentInfo : agentInfoList) { + ComponentName agentComponentName = getComponentName(agentInfo); + if (knownAgents.contains(agentComponentName)) { + continue; + } + newAgents.add(agentComponentName); + if (isSystemTrustAgent(agentInfo)) { + newSystemAgents.add(agentComponentName); + } + } + + if (newAgents.isEmpty()) { + return; + } + + ArraySet<ComponentName> updatedKnowAgents = new ArraySet<>(knownAgents); + updatedKnowAgents.addAll(newAgents); + mLockPatternUtils.setKnownTrustAgents(updatedKnowAgents, userId); + + // Do not auto enable new trust agents when the default agent is set + boolean hasDefaultAgent = getDefaultFactoryTrustAgent(mContext) != null; + if (!hasDefaultAgent) { + enableNewAgents(newSystemAgents, userId); + } + } + + private void enableNewAgents(Collection<ComponentName> agents, int userId) { + if (agents.isEmpty()) { + return; + } + + ArraySet<ComponentName> agentsToEnable = new ArraySet<>(agents); + agentsToEnable.addAll(mLockPatternUtils.getEnabledTrustAgents(userId)); + mLockPatternUtils.setEnabledTrustAgents(agentsToEnable, userId); + } + + private void initializeKnownAgents(int userId) { + List<ResolveInfo> agentInfoList = resolveAllowedTrustAgents(mContext.getPackageManager(), + userId); + ArraySet<ComponentName> agentComponentNames = new ArraySet<>(agentInfoList.size()); + for (ResolveInfo agentInfo : agentInfoList) { + agentComponentNames.add(getComponentName(agentInfo)); + } + mLockPatternUtils.setKnownTrustAgents(agentComponentNames, userId); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.KNOWN_TRUST_AGENTS_INITIALIZED, 1, userId); + } + /** * Returns the {@link ComponentName} for the default trust agent, or {@code null} if there * is no trust agent set. @@ -1152,6 +1228,10 @@ public class TrustManagerService extends SystemService { return allowedAgents; } + private static boolean isSystemTrustAgent(ResolveInfo agentInfo) { + return (agentInfo.serviceInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + } + // Agent dispatch and aggregation private boolean aggregateIsTrusted(int userId) { @@ -1825,7 +1905,13 @@ public class TrustManagerService extends SystemService { } @Override + public void onPackageAdded(String packageName, int uid) { + checkNewAgentsForUser(UserHandle.getUserId(uid)); + } + + @Override public boolean onPackageChanged(String packageName, int uid, String[] components) { + checkNewAgentsForUser(UserHandle.getUserId(uid)); // We're interested in all changes, even if just some components get enabled / disabled. return true; } @@ -1860,7 +1946,7 @@ public class TrustManagerService extends SystemService { action)) { int userId = getUserId(intent); if (userId > 0) { - maybeEnableFactoryTrustAgents(mLockPatternUtils, userId); + maybeEnableFactoryTrustAgents(userId); } } else if (Intent.ACTION_USER_REMOVED.equals(action)) { int userId = getUserId(intent); @@ -1997,7 +2083,7 @@ public class TrustManagerService extends SystemService { if (mStrongAuthTracker.isTrustAllowedForUser(mUserId)) { if (DEBUG) Slog.d(TAG, "Revoking all trust because of trust timeout"); mLockPatternUtils.requireStrongAuth( - mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, mUserId); + mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, mUserId); } maybeLockScreen(mUserId); } diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 7067ae474aad..08d2e69846d3 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -237,9 +237,21 @@ class ActivityMetricsLogger { if (mAssociatedTransitionInfo == null) { launchResult = ":failed"; } else { - launchResult = (abort ? ":canceled:" : mAssociatedTransitionInfo.mProcessSwitch - ? ":completed:" : ":completed-same-process:") - + mAssociatedTransitionInfo.mLastLaunchedActivity.packageName; + final String status; + if (abort) { + status = ":canceled:"; + } else if (!mAssociatedTransitionInfo.mProcessSwitch) { + status = ":completed-same-process:"; + } else { + if (endInfo.mTransitionType == TYPE_TRANSITION_HOT_LAUNCH) { + status = ":completed-hot:"; + } else if (endInfo.mTransitionType == TYPE_TRANSITION_WARM_LAUNCH) { + status = ":completed-warm:"; + } else { + status = ":completed-cold:"; + } + } + launchResult = status + mAssociatedTransitionInfo.mLastLaunchedActivity.packageName; } // Put a supplement trace as the description of the async trace with the same id. Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, mTraceName + launchResult); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 98d3adc71fe6..d8d75ed9790f 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1586,11 +1586,19 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (oldParent != null) { oldParent.cleanUpActivityReferences(this); + // Update isVisibleRequested value of parent TaskFragment and send the callback to the + // client side if needed. + oldParent.onActivityVisibleRequestedChanged(); } - if (newParent != null && isState(RESUMED)) { - newParent.setResumedActivity(this, "onParentChanged"); - mImeInsetsFrozenUntilStartInput = false; + if (newParent != null) { + // Update isVisibleRequested value of parent TaskFragment and send the callback to the + // client side if needed. + newParent.onActivityVisibleRequestedChanged(); + if (isState(RESUMED)) { + newParent.setResumedActivity(this, "onParentChanged"); + mImeInsetsFrozenUntilStartInput = false; + } } if (rootTask != null && rootTask.topRunningActivity() == this) { @@ -5094,6 +5102,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return; } mVisibleRequested = visible; + final TaskFragment taskFragment = getTaskFragment(); + if (taskFragment != null) { + taskFragment.onActivityVisibleRequestedChanged(); + } setInsetsFrozen(!visible); if (app != null) { mTaskSupervisor.onProcessActivityStateChanged(app, false /* forceBatch */); @@ -9550,7 +9562,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override boolean showToCurrentUser() { - return mShowForAllUsers || mWmService.isCurrentProfile(mUserId); + return mShowForAllUsers || mWmService.isUserVisible(mUserId); } @Override diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index bc1c6b2e8f3a..a0a4f760a889 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -892,9 +892,10 @@ class ActivityStarter { final int userId = aInfo != null && aInfo.applicationInfo != null ? UserHandle.getUserId(aInfo.applicationInfo.uid) : 0; + final int launchMode = aInfo != null ? aInfo.launchMode : 0; if (err == ActivityManager.START_SUCCESS) { Slog.i(TAG, "START u" + userId + " {" + intent.toShortString(true, true, true, false) - + "} from uid " + callingUid); + + "} with " + launchModeToString(launchMode) + " from uid " + callingUid); } ActivityRecord sourceRecord = null; diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 28cd0016b019..5bddae66fed3 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -1183,10 +1183,14 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } if (!displayContent.isPrivate()) { - // Anyone can launch on a public display. - ProtoLog.d(WM_DEBUG_TASKS, "Launch on display check: allow launch on public " - + "display"); - return true; + // Checks if the caller can be shown in the given public display. + int userId = UserHandle.getUserId(callingUid); + int displayId = display.getDisplayId(); + boolean allowed = mWindowManager.mUmInternal.isUserVisible(userId, displayId); + ProtoLog.d(WM_DEBUG_TASKS, + "Launch on display check: %s launch for userId=%d on displayId=%d", + (allowed ? "allow" : "disallow"), userId, displayId); + return allowed; } // Check if the caller is the owner of the display. diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index dcc16ebd88f5..d54f77a73661 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -19,17 +19,16 @@ package com.android.server.wm; import static com.android.server.wm.IdentifierProto.HASH_CODE; import static com.android.server.wm.IdentifierProto.TITLE; -import static com.android.server.wm.WindowContainerProto.IDENTIFIER; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; +import static com.android.server.wm.WindowStateProto.IDENTIFIER; import android.annotation.Nullable; import android.os.IBinder; import android.os.RemoteException; -import android.os.UserHandle; import android.util.ArrayMap; -import android.util.proto.ProtoOutputStream; import android.util.Slog; +import android.util.proto.ProtoOutputStream; import android.view.IWindow; import android.view.InputApplicationHandle; import android.view.InputChannel; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 837045c0019b..829f01e09eed 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -186,6 +186,7 @@ import android.view.WindowManager.TransitionOldType; import android.window.ITaskOrganizer; import android.window.PictureInPictureSurfaceTransaction; import android.window.StartingWindowInfo; +import android.window.TaskFragmentParentInfo; import android.window.TaskSnapshot; import android.window.WindowContainerToken; @@ -2679,6 +2680,7 @@ class Task extends TaskFragment { if (isRootTask()) { updateSurfaceBounds(); } + sendTaskFragmentParentInfoChangedIfNeeded(); } boolean isResizeable() { @@ -2914,7 +2916,7 @@ class Task extends TaskFragment { @Override boolean showToCurrentUser() { return mForceShowForAllUsers || showForAllUsers() - || mWmService.isCurrentProfile(getTopMostTask().mUserId); + || mWmService.isUserVisible(getTopMostTask().mUserId); } void setForceShowForAllUsers(boolean forceShowForAllUsers) { @@ -3513,6 +3515,33 @@ class Task extends TaskFragment { return info; } + /** + * Returns the {@link TaskFragmentParentInfo} which will send to the client + * {@link android.window.TaskFragmentOrganizer} + */ + TaskFragmentParentInfo getTaskFragmentParentInfo() { + return new TaskFragmentParentInfo(getConfiguration(), getDisplayId(), isVisibleRequested()); + } + + @Override + void onActivityVisibleRequestedChanged() { + if (mVisibleRequested != isVisibleRequested()) { + sendTaskFragmentParentInfoChangedIfNeeded(); + } + } + + void sendTaskFragmentParentInfoChangedIfNeeded() { + if (!isLeafTask()) { + // Only send parent info changed event for leaf task. + return; + } + final TaskFragment childOrganizedTf = + getTaskFragment(TaskFragment::isOrganizedTaskFragment); + if (childOrganizedTf != null) { + childOrganizedTf.sendTaskFragmentParentInfoChanged(); + } + } + boolean isTaskId(int taskId) { return mTaskId == taskId; } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index de4c84c27a69..2cfc563b0d6e 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -298,6 +298,9 @@ class TaskFragment extends WindowContainer<WindowContainer> { final Point mLastSurfaceSize = new Point(); + /** The latest updated value when there's a child {@link #onActivityVisibleRequestedChanged} */ + boolean mVisibleRequested; + private final Rect mTmpBounds = new Rect(); private final Rect mTmpFullBounds = new Rect(); /** For calculating screenWidthDp and screenWidthDp, i.e. the area without the system bars. */ @@ -2382,6 +2385,14 @@ class TaskFragment extends WindowContainer<WindowContainer> { } } + void sendTaskFragmentParentInfoChanged() { + final Task parentTask = getParent().asTask(); + if (mTaskFragmentOrganizer != null && parentTask != null) { + mTaskFragmentOrganizerController + .onTaskFragmentParentInfoChanged(mTaskFragmentOrganizer, parentTask); + } + } + private void sendTaskFragmentAppeared() { if (mTaskFragmentOrganizer != null) { mTaskFragmentOrganizerController.onTaskFragmentAppeared(mTaskFragmentOrganizer, this); @@ -2417,7 +2428,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { mRemoteToken.toWindowContainerToken(), getConfiguration(), getNonFinishingActivityCount(), - isVisible(), + isVisibleRequested(), childActivities, positionInParent, mClearedTaskForReuse, @@ -2669,6 +2680,18 @@ class TaskFragment extends WindowContainer<WindowContainer> { return getWindowingMode() == WINDOWING_MODE_FULLSCREEN || matchParentBounds(); } + void onActivityVisibleRequestedChanged() { + final boolean isVisibleRequested = isVisibleRequested(); + if (mVisibleRequested == isVisibleRequested) { + return; + } + mVisibleRequested = isVisibleRequested; + final TaskFragment parentTf = getParent().asTaskFragment(); + if (parentTf != null) { + parentTf.onActivityVisibleRequestedChanged(); + } + } + String toFullString() { final StringBuilder sb = new StringBuilder(128); sb.append(this); diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 8c037a7390b1..2d5c9897a82c 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -49,6 +49,7 @@ import android.view.WindowManager; import android.window.ITaskFragmentOrganizer; import android.window.ITaskFragmentOrganizerController; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; @@ -118,10 +119,10 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr private final Map<TaskFragment, Integer> mTaskFragmentTaskIds = new WeakHashMap<>(); /** - * Map from {@link Task#mTaskId} to the last Task {@link Configuration} sent to the + * Map from {@link Task#mTaskId} to the last {@link TaskFragmentParentInfo} sent to the * organizer. */ - private final SparseArray<Configuration> mLastSentTaskFragmentParentConfigs = + private final SparseArray<TaskFragmentParentInfo> mLastSentTaskFragmentParentInfos = new SparseArray<>(); /** @@ -225,7 +226,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr taskId = mTaskFragmentTaskIds.remove(tf); if (!mTaskFragmentTaskIds.containsValue(taskId)) { // No more TaskFragment in the Task. - mLastSentTaskFragmentParentConfigs.remove(taskId); + mLastSentTaskFragmentParentInfos.remove(taskId); } } else { // This can happen if the appeared wasn't sent before remove. @@ -260,25 +261,27 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } @Nullable - TaskFragmentTransaction.Change prepareTaskFragmentParentInfoChanged( - @NonNull Task task) { + TaskFragmentTransaction.Change prepareTaskFragmentParentInfoChanged(@NonNull Task task) { final int taskId = task.mTaskId; // Check if the parent info is different from the last reported parent info. - final Configuration taskConfig = task.getConfiguration(); - final Configuration lastParentConfig = mLastSentTaskFragmentParentConfigs.get(taskId); - if (configurationsAreEqualForOrganizer(taskConfig, lastParentConfig) - && taskConfig.windowConfiguration.getWindowingMode() - == lastParentConfig.windowConfiguration.getWindowingMode()) { + final TaskFragmentParentInfo parentInfo = task.getTaskFragmentParentInfo(); + final TaskFragmentParentInfo lastParentInfo = mLastSentTaskFragmentParentInfos + .get(taskId); + final Configuration lastParentConfig = lastParentInfo != null + ? lastParentInfo.getConfiguration() : null; + if (parentInfo.equalsForTaskFragmentOrganizer(lastParentInfo) + && configurationsAreEqualForOrganizer(parentInfo.getConfiguration(), + lastParentConfig)) { return null; } ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment parent info changed name=%s parentTaskId=%d", task.getName(), taskId); - mLastSentTaskFragmentParentConfigs.put(taskId, new Configuration(taskConfig)); + mLastSentTaskFragmentParentInfos.put(taskId, new TaskFragmentParentInfo(parentInfo)); return new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED) .setTaskId(taskId) - .setTaskConfiguration(taskConfig); + .setTaskFragmentParentInfo(parentInfo); } @NonNull @@ -646,6 +649,34 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr .build()); } + void onTaskFragmentParentInfoChanged(@NonNull ITaskFragmentOrganizer organizer, + @NonNull Task task) { + validateAndGetState(organizer); + final PendingTaskFragmentEvent pendingEvent = getLastPendingParentInfoChangedEvent( + organizer, task); + if (pendingEvent == null) { + addPendingEvent(new PendingTaskFragmentEvent.Builder( + PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED, organizer) + .setTask(task) + .build()); + } + } + + @Nullable + private PendingTaskFragmentEvent getLastPendingParentInfoChangedEvent( + @NonNull ITaskFragmentOrganizer organizer, @NonNull Task task) { + final List<PendingTaskFragmentEvent> events = mPendingTaskFragmentEvents + .get(organizer.asBinder()); + for (int i = events.size() - 1; i >= 0; i--) { + final PendingTaskFragmentEvent event = events.get(i); + if (task == event.mTask + && event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) { + return event; + } + } + return null; + } + private void addPendingEvent(@NonNull PendingTaskFragmentEvent event) { mPendingTaskFragmentEvents.get(event.mTaskFragmentOrg.asBinder()).add(event); } @@ -848,7 +879,9 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } private boolean shouldSendEventWhenTaskInvisible(@NonNull PendingTaskFragmentEvent event) { - if (event.mEventType == PendingTaskFragmentEvent.EVENT_ERROR) { + if (event.mEventType == PendingTaskFragmentEvent.EVENT_ERROR + // Always send parent info changed to update task visibility + || event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) { return true; } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 723aa19bc2b5..93d9b0e724ce 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -42,6 +42,7 @@ import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; +import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; @@ -1860,14 +1861,12 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe // Whether this is in a Task with embedded activity. flags |= FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; } - final Rect taskBounds = parentTask.getBounds(); - final Rect startBounds = mAbsoluteBounds; - final Rect endBounds = wc.getBounds(); - if (taskBounds.width() == startBounds.width() - && taskBounds.height() == startBounds.height() - && taskBounds.width() == endBounds.width() - && taskBounds.height() == endBounds.height()) { - // Whether the container fills the Task bounds before and after the transition. + if (parentTask.forAllActivities(ActivityRecord::hasStartingWindow)) { + // The starting window should cover all windows inside the leaf Task. + flags |= FLAG_IS_BEHIND_STARTING_WINDOW; + } + if (isWindowFillingTask(wc, parentTask)) { + // Whether the container fills its parent Task bounds. flags |= FLAG_FILLS_TASK; } } @@ -1889,6 +1888,22 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } return flags; } + + /** Whether the container fills its parent Task bounds before and after the transition. */ + private boolean isWindowFillingTask(@NonNull WindowContainer wc, @NonNull Task parentTask) { + final Rect taskBounds = parentTask.getBounds(); + final int taskWidth = taskBounds.width(); + final int taskHeight = taskBounds.height(); + final Rect startBounds = mAbsoluteBounds; + final Rect endBounds = wc.getBounds(); + // Treat it as filling the task if it is not visible. + final boolean isInvisibleOrFillingTaskBeforeTransition = !mVisible + || (taskWidth == startBounds.width() && taskHeight == startBounds.height()); + final boolean isInVisibleOrFillingTaskAfterTransition = !wc.isVisibleRequested() + || (taskWidth == endBounds.width() && taskHeight == endBounds.height()); + return isInvisibleOrFillingTaskBeforeTransition + && isInVisibleOrFillingTaskAfterTransition; + } } /** diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 8cc0d5dd8c1b..69d86b60302b 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -2195,6 +2195,17 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< callback, boundary, includeBoundary, traverseTopToBottom, boundaryFound); } + @Nullable + TaskFragment getTaskFragment(Predicate<TaskFragment> callback) { + for (int i = mChildren.size() - 1; i >= 0; --i) { + final TaskFragment tf = mChildren.get(i).getTaskFragment(callback); + if (tf != null) { + return tf; + } + } + return null; + } + WindowState getWindow(Predicate<WindowState> callback) { for (int i = mChildren.size() - 1; i >= 0; --i) { final WindowState w = mChildren.get(i).getWindow(callback); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 49e7a0c8c745..43ef1d4d778d 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -123,7 +123,10 @@ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_ import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS; 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.SurfaceAnimator.ANIMATION_TYPE_RECENTS; +import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN; +import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY; @@ -155,6 +158,7 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ActivityThread; @@ -315,6 +319,7 @@ import com.android.server.LocalServices; import com.android.server.UiThread; import com.android.server.Watchdog; import com.android.server.input.InputManagerService; +import com.android.server.pm.UserManagerInternal; import com.android.server.policy.WindowManagerPolicy; import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; import com.android.server.power.ShutdownThread; @@ -504,15 +509,9 @@ public class WindowManagerService extends IWindowManager.Stub }; /** - * Current user when multi-user is enabled. Don't show windows of - * non-current user. Also see mCurrentProfileIds. + * Current user when multi-user is enabled. Don't show windows of non-current user. */ - int mCurrentUserId; - /** - * Users that are profiles of the current user. These are also allowed to show windows - * on the current user. - */ - int[] mCurrentProfileIds = new int[] {}; + @UserIdInt int mCurrentUserId; final Context mContext; @@ -534,6 +533,7 @@ public class WindowManagerService extends IWindowManager.Stub final IActivityManager mActivityManager; final ActivityManagerInternal mAmInternal; + final UserManagerInternal mUmInternal; final AppOpsManager mAppOps; final PackageManagerInternal mPmInternal; @@ -1263,6 +1263,7 @@ public class WindowManagerService extends IWindowManager.Stub mActivityManager = ActivityManager.getService(); mAmInternal = LocalServices.getService(ActivityManagerInternal.class); + mUmInternal = LocalServices.getService(UserManagerInternal.class); mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE); AppOpsManager.OnOpChangedInternalListener opListener = new AppOpsManager.OnOpChangedInternalListener() { @@ -2604,10 +2605,22 @@ public class WindowManagerService extends IWindowManager.Stub if (win.isWinVisibleLw() && win.mDisplayContent.okToAnimate()) { String reason = null; if (winAnimator.applyAnimationLocked(transit, false)) { + // This is a WMCore-driven window animation. reason = "applyAnimation"; focusMayChange = true; win.mAnimatingExit = true; - } else if (win.isExitAnimationRunningSelfOrParent()) { + } else if ( + // This is already animating via a WMCore-driven window animation + win.isSelfAnimating(0 /* flags */, ANIMATION_TYPE_WINDOW_ANIMATION) + // Or already animating as part of a legacy app-transition + || win.isAnimating(PARENTS | TRANSITION, + ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS) + // Or already animating as part of a shell-transition. + || (win.inTransition() + // Filter out non-app windows since transitions don't animate those + // (but may still "wait" on them for readiness) + && (win.mActivityRecord != null || win.mIsWallpaper))) { + // TODO(b/247005789): set mAnimatingExit somewhere in shell-transitions setup. reason = "animating"; win.mAnimatingExit = true; } else if (win.mDisplayContent.mWallpaperController.isWallpaperTarget(win) @@ -3558,16 +3571,9 @@ public class WindowManagerService extends IWindowManager.Stub confirm); } - public void setCurrentProfileIds(final int[] currentProfileIds) { - synchronized (mGlobalLock) { - mCurrentProfileIds = currentProfileIds; - } - } - - public void setCurrentUser(final int newUserId, final int[] currentProfileIds) { + public void setCurrentUser(@UserIdInt int newUserId) { synchronized (mGlobalLock) { mCurrentUserId = newUserId; - mCurrentProfileIds = currentProfileIds; mPolicy.setCurrentUserLw(newUserId); mKeyguardDisableHandler.setCurrentUser(newUserId); @@ -3590,12 +3596,8 @@ public class WindowManagerService extends IWindowManager.Stub } /* Called by WindowState */ - boolean isCurrentProfile(int userId) { - if (userId == mCurrentUserId) return true; - for (int i = 0; i < mCurrentProfileIds.length; i++) { - if (mCurrentProfileIds[i] == userId) return true; - } - return false; + boolean isUserVisible(@UserIdInt int userId) { + return mUmInternal.isUserVisible(userId); } public void enableScreenAfterBoot() { @@ -9277,46 +9279,4 @@ public class WindowManagerService extends IWindowManager.Stub "Unexpected letterbox background type: " + letterboxBackgroundType); } } - - @Override - public void captureDisplay(int displayId, @Nullable ScreenCapture.CaptureArgs captureArgs, - ScreenCapture.ScreenCaptureListener listener) { - Slog.d(TAG, "captureDisplay"); - if (!checkCallingPermission(READ_FRAME_BUFFER, "captureDisplay()")) { - throw new SecurityException("Requires READ_FRAME_BUFFER permission"); - } - - ScreenCapture.captureLayers(getCaptureArgs(displayId, captureArgs), listener); - } - - @VisibleForTesting - ScreenCapture.LayerCaptureArgs getCaptureArgs(int displayId, - @Nullable ScreenCapture.CaptureArgs captureArgs) { - final SurfaceControl displaySurfaceControl; - synchronized (mGlobalLock) { - DisplayContent displayContent = mRoot.getDisplayContent(displayId); - if (displayContent == null) { - throw new IllegalArgumentException("Trying to screenshot and invalid display: " - + displayId); - } - - displaySurfaceControl = displayContent.getSurfaceControl(); - - if (captureArgs == null) { - captureArgs = new ScreenCapture.CaptureArgs.Builder<>() - .build(); - } - - if (captureArgs.mSourceCrop.isEmpty()) { - displayContent.getBounds(mTmpRect); - mTmpRect.offsetTo(0, 0); - } else { - mTmpRect.set(captureArgs.mSourceCrop); - } - } - - return new ScreenCapture.LayerCaptureArgs.Builder(displaySurfaceControl, captureArgs) - .setSourceCrop(mTmpRect) - .build(); - } } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index df7b8bb4ec9b..1d43d18ab917 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -3684,7 +3684,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } return win.showForAllUsers() - || mWmService.isCurrentProfile(win.mShowUserId); + || mWmService.isUserVisible(win.mShowUserId); } private static void applyInsets(Region outRegion, Rect frame, Rect inset) { diff --git a/services/core/jni/com_android_server_display_DisplayControl.cpp b/services/core/jni/com_android_server_display_DisplayControl.cpp index 61f2b1411a0b..02e5061a3ac6 100644 --- a/services/core/jni/com_android_server_display_DisplayControl.cpp +++ b/services/core/jni/com_android_server_display_DisplayControl.cpp @@ -17,6 +17,7 @@ #include <android_util_Binder.h> #include <gui/SurfaceComposerClient.h> #include <jni.h> +#include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/ScopedUtfChars.h> namespace android { @@ -33,6 +34,27 @@ static void nativeDestroyDisplay(JNIEnv* env, jclass clazz, jobject tokenObj) { SurfaceComposerClient::destroyDisplay(token); } +static void nativeOverrideHdrTypes(JNIEnv* env, jclass clazz, jobject tokenObject, + jintArray jHdrTypes) { + sp<IBinder> token(ibinderForJavaObject(env, tokenObject)); + if (token == nullptr || jHdrTypes == nullptr) return; + + ScopedIntArrayRO hdrTypes(env, jHdrTypes); + size_t numHdrTypes = hdrTypes.size(); + + std::vector<ui::Hdr> hdrTypesVector; + hdrTypesVector.reserve(numHdrTypes); + for (int i = 0; i < numHdrTypes; i++) { + hdrTypesVector.push_back(static_cast<ui::Hdr>(hdrTypes[i])); + } + + status_t error = SurfaceComposerClient::overrideHdrTypes(token, hdrTypesVector); + if (error != NO_ERROR) { + jniThrowExceptionFmt(env, "java/lang/SecurityException", + "ACCESS_SURFACE_FLINGER is missing"); + } +} + // ---------------------------------------------------------------------------- static const JNINativeMethod sDisplayMethods[] = { @@ -41,6 +63,8 @@ static const JNINativeMethod sDisplayMethods[] = { (void*)nativeCreateDisplay }, {"nativeDestroyDisplay", "(Landroid/os/IBinder;)V", (void*)nativeDestroyDisplay }, + {"nativeOverrideHdrTypes", "(Landroid/os/IBinder;[I)V", + (void*)nativeOverrideHdrTypes }, // clang-format on }; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index bb6bd4aa1bbc..42dfee375ae4 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -5725,7 +5725,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { try { return setKeyChainGrantInternal(alias, hasGrant, granteeUid, caller.getUserHandle()); } catch (IllegalArgumentException e) { - if (mInjector.isChangeEnabled(THROW_EXCEPTION_WHEN_KEY_MISSING, packageName, + if (mInjector.isChangeEnabled(THROW_EXCEPTION_WHEN_KEY_MISSING, callerPackage, caller.getUserId())) { throw e; } diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml index 07b763dcd85b..33ac73560968 100644 --- a/services/tests/mockingservicestests/AndroidManifest.xml +++ b/services/tests/mockingservicestests/AndroidManifest.xml @@ -34,10 +34,15 @@ <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> <uses-permission android:name="android.permission.MANAGE_GAME_ACTIVITY" /> <uses-permission android:name="android.permission.SET_ALWAYS_FINISH" /> + <uses-permission android:name="android.permission.MANAGE_USERS" /> + <uses-permission android:name="android.permission.USE_BIOMETRIC_INTERNAL" /> <!-- needed by MasterClearReceiverTest to display a system dialog --> <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"/> + <!-- needed by TrustManagerServiceTest to access LockSettings' secure storage --> + <uses-permission android:name="android.permission.ACCESS_KEYGUARD_SECURE_STORAGE" /> + <application android:testOnly="true" android:debuggable="true"> <uses-library android:name="android.test.runner" /> diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 9bdc93e11b6a..6bf102a09368 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -16,7 +16,6 @@ package com.android.server.am; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -46,7 +45,6 @@ import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; -import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.util.Log; @@ -92,7 +90,8 @@ public class BroadcastQueueTest { private final Impl mImpl; private enum Impl { - DEFAULT + DEFAULT, + MODERN, } private Context mContext; @@ -116,7 +115,7 @@ public class BroadcastQueueTest { @Parameters(name = "impl={0}") public static Collection<Object[]> data() { - return Arrays.asList(new Object[][] { {Impl.DEFAULT} }); + return Arrays.asList(new Object[][] { {Impl.DEFAULT}, {Impl.MODERN} }); } public BroadcastQueueTest(Impl impl) { @@ -152,7 +151,9 @@ public class BroadcastQueueTest { final ApplicationInfo ai = invocation.getArgument(1); final ProcessRecord res = makeActiveProcessRecord(ai, processName); mHandlerThread.getThreadHandler().post(() -> { - mQueue.onApplicationAttachedLocked(res); + synchronized (mAms) { + mQueue.onApplicationAttachedLocked(res); + } }); return res; }).when(mAms).startProcessLocked(any(), any(), anyBoolean(), anyInt(), @@ -177,6 +178,9 @@ public class BroadcastQueueTest { mQueue = new BroadcastQueueImpl(mAms, mHandlerThread.getThreadHandler(), TAG, constants, emptySkipPolicy, emptyHistory, false, ProcessList.SCHED_GROUP_DEFAULT); + } else if (mImpl == Impl.MODERN) { + mQueue = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(), + constants, emptySkipPolicy, emptyHistory); } else { throw new UnsupportedOperationException(); } @@ -230,8 +234,10 @@ public class BroadcastQueueTest { Log.v(TAG, "Intercepting scheduleReceiver() for " + Arrays.toString(invocation.getArguments())); mHandlerThread.getThreadHandler().post(() -> { - mQueue.finishReceiverLocked(r, Activity.RESULT_OK, - null, null, false, false); + synchronized (mAms) { + mQueue.finishReceiverLocked(r, Activity.RESULT_OK, + null, null, false, false); + } }); return null; }).when(thread).scheduleReceiver(any(), any(), any(), anyInt(), any(), any(), anyBoolean(), @@ -240,10 +246,15 @@ public class BroadcastQueueTest { doAnswer((invocation) -> { Log.v(TAG, "Intercepting scheduleRegisteredReceiver() for " + Arrays.toString(invocation.getArguments())); - mHandlerThread.getThreadHandler().post(() -> { - mQueue.finishReceiverLocked(r, Activity.RESULT_OK, null, null, - false, false); - }); + final boolean ordered = invocation.getArgument(5); + if (ordered) { + mHandlerThread.getThreadHandler().post(() -> { + synchronized (mAms) { + mQueue.finishReceiverLocked(r, Activity.RESULT_OK, + null, null, false, false); + } + }); + } return null; }).when(thread).scheduleRegisteredReceiver(any(), any(), anyInt(), any(), any(), anyBoolean(), anyBoolean(), anyInt(), anyInt()); @@ -302,6 +313,12 @@ public class BroadcastQueueTest { }; } + private void enqueueBroadcast(BroadcastRecord r) { + synchronized (mAms) { + mQueue.enqueueBroadcastLocked(r); + } + } + private void waitForIdle() throws Exception { mQueue.waitForIdle(null); } @@ -349,7 +366,7 @@ public class BroadcastQueueTest { final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN); final Intent intent = new Intent(Intent.ACTION_TIMEZONE_CHANGED); - mQueue.enqueueBroadcastLocked(makeBroadcastRecord(intent, callerApp, + enqueueBroadcast(makeBroadcastRecord(intent, callerApp, List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)))); waitForIdle(); @@ -368,12 +385,12 @@ public class BroadcastQueueTest { final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); - mQueue.enqueueBroadcastLocked(makeBroadcastRecord(timezone, callerApp, + enqueueBroadcast(makeBroadcastRecord(timezone, callerApp, List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)))); final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); - mQueue.enqueueBroadcastLocked(makeBroadcastRecord(airplane, callerApp, + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of(makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)))); waitForIdle(); @@ -394,12 +411,12 @@ public class BroadcastQueueTest { // the second time it should already be running final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); - mQueue.enqueueBroadcastLocked(makeBroadcastRecord(timezone, callerApp, + enqueueBroadcast(makeBroadcastRecord(timezone, callerApp, List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)))); final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); - mQueue.enqueueBroadcastLocked(makeBroadcastRecord(airplane, callerApp, + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)))); waitForIdle(); @@ -407,7 +424,6 @@ public class BroadcastQueueTest { getUidForPackage(PACKAGE_GREEN)); final ProcessRecord receiverBlueApp = mAms.getProcessRecordLocked(PACKAGE_BLUE, getUidForPackage(PACKAGE_BLUE)); - assertTrue(receiverBlueApp.getPid() > receiverGreenApp.getPid()); verifyScheduleReceiver(receiverGreenApp, timezone); verifyScheduleReceiver(receiverGreenApp, airplane); verifyScheduleReceiver(receiverBlueApp, timezone); @@ -423,7 +439,7 @@ public class BroadcastQueueTest { final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN); final Intent intent = new Intent(Intent.ACTION_TIMEZONE_CHANGED); - mQueue.enqueueBroadcastLocked(makeBroadcastRecord(intent, callerApp, + enqueueBroadcast(makeBroadcastRecord(intent, callerApp, List.of(makeRegisteredReceiver(receiverApp)))); waitForIdle(); @@ -442,12 +458,12 @@ public class BroadcastQueueTest { final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); - mQueue.enqueueBroadcastLocked(makeBroadcastRecord(timezone, callerApp, + enqueueBroadcast(makeBroadcastRecord(timezone, callerApp, List.of(makeRegisteredReceiver(receiverGreenApp), makeRegisteredReceiver(receiverBlueApp)))); final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); - mQueue.enqueueBroadcastLocked(makeBroadcastRecord(airplane, callerApp, + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of(makeRegisteredReceiver(receiverBlueApp)))); waitForIdle(); @@ -468,14 +484,14 @@ public class BroadcastQueueTest { final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW); final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); - mQueue.enqueueBroadcastLocked(makeBroadcastRecord(timezone, callerApp, + enqueueBroadcast(makeBroadcastRecord(timezone, callerApp, List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), makeRegisteredReceiver(receiverGreenApp), makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), makeRegisteredReceiver(receiverYellowApp)))); final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); - mQueue.enqueueBroadcastLocked(makeBroadcastRecord(airplane, callerApp, + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of(makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW)))); waitForIdle(); diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java new file mode 100644 index 000000000000..1a5d496f2002 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java @@ -0,0 +1,235 @@ +/* + * 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.display; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.Resources; +import android.hardware.Sensor; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks; +import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest; +import android.os.Handler; +import android.os.PowerManager; +import android.os.test.TestLooper; +import android.util.FloatProperty; +import android.view.Display; +import android.view.DisplayInfo; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.LocalServices; +import com.android.server.display.RampAnimator.DualRampAnimator; +import com.android.server.policy.WindowManagerPolicy; +import com.android.server.testutils.OffsettableClock; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; + + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class DisplayPowerController2Test { + private static final String UNIQUE_DISPLAY_ID = "unique_id_test123"; + private static final int DISPLAY_ID = 42; + + private OffsettableClock mClock; + private TestLooper mTestLooper; + private Handler mHandler; + private DisplayPowerController2.Injector mInjector; + private Context mContextSpy; + + @Mock + private DisplayPowerCallbacks mDisplayPowerCallbacksMock; + @Mock + private SensorManager mSensorManagerMock; + @Mock + private DisplayBlanker mDisplayBlankerMock; + @Mock + private LogicalDisplay mLogicalDisplayMock; + @Mock + private DisplayDevice mDisplayDeviceMock; + @Mock + private BrightnessTracker mBrightnessTrackerMock; + @Mock + private BrightnessSetting mBrightnessSettingMock; + @Mock + private WindowManagerPolicy mWindowManagerPolicyMock; + @Mock + private PowerManager mPowerManagerMock; + @Mock + private Resources mResourcesMock; + @Mock + private DisplayDeviceConfig mDisplayDeviceConfigMock; + @Mock + private DisplayPowerState mDisplayPowerStateMock; + @Mock + private DualRampAnimator<DisplayPowerState> mDualRampAnimatorMock; + + @Captor + private ArgumentCaptor<SensorEventListener> mSensorEventListenerCaptor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); + mClock = new OffsettableClock.Stopped(); + mTestLooper = new TestLooper(mClock::now); + mHandler = new Handler(mTestLooper.getLooper()); + mInjector = new DisplayPowerController2.Injector() { + @Override + DisplayPowerController2.Clock getClock() { + return mClock::now; + } + + @Override + DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade, + int displayId, int displayState) { + return mDisplayPowerStateMock; + } + + @Override + DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps, + FloatProperty<DisplayPowerState> firstProperty, + FloatProperty<DisplayPowerState> secondProperty) { + return mDualRampAnimatorMock; + } + }; + + addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock); + + when(mContextSpy.getSystemService(eq(PowerManager.class))).thenReturn(mPowerManagerMock); + when(mContextSpy.getResources()).thenReturn(mResourcesMock); + } + + @After + public void tearDown() { + LocalServices.removeServiceForTest(WindowManagerPolicy.class); + } + + @Test + public void testReleaseProxSuspendBlockersOnExit() throws Exception { + setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID); + + Sensor proxSensor = setUpProxSensor(); + + DisplayPowerController2 dpc = new DisplayPowerController2( + mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler, + mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock, + mBrightnessTrackerMock, mBrightnessSettingMock, () -> { + }); + + when(mDisplayPowerStateMock.getScreenState()).thenReturn(Display.STATE_ON); + // send a display power request + DisplayPowerRequest dpr = new DisplayPowerRequest(); + dpr.policy = DisplayPowerRequest.POLICY_BRIGHT; + dpr.useProximitySensor = true; + dpc.requestPowerState(dpr, false /* waitForNegativeProximity */); + + // Run updatePowerState to start listener for the prox sensor + advanceTime(1); + + SensorEventListener listener = getSensorEventListener(proxSensor); + assertNotNull(listener); + + listener.onSensorChanged(TestUtils.createSensorEvent(proxSensor, 5 /* lux */)); + advanceTime(1); + + // two times, one for unfinished business and one for proximity + verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker( + dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID)); + verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker( + dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID)); + + dpc.stop(); + advanceTime(1); + + // two times, one for unfinished business and one for proximity + verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker( + dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID)); + verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker( + dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID)); + } + + /** + * Creates a mock and registers it to {@link LocalServices}. + */ + private static <T> void addLocalServiceMock(Class<T> clazz, T mock) { + LocalServices.removeServiceForTest(clazz); + LocalServices.addService(clazz, mock); + } + + private void advanceTime(long timeMs) { + mClock.fastForward(timeMs); + mTestLooper.dispatchAll(); + } + + private Sensor setUpProxSensor() throws Exception { + Sensor proxSensor = TestUtils.createSensor( + Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY); + when(mSensorManagerMock.getSensorList(eq(Sensor.TYPE_ALL))) + .thenReturn(List.of(proxSensor)); + return proxSensor; + } + + private SensorEventListener getSensorEventListener(Sensor sensor) { + verify(mSensorManagerMock).registerListener(mSensorEventListenerCaptor.capture(), + eq(sensor), eq(SensorManager.SENSOR_DELAY_NORMAL), isA(Handler.class)); + return mSensorEventListenerCaptor.getValue(); + } + + private void setUpDisplay(int displayId, String uniqueId) { + DisplayInfo info = new DisplayInfo(); + DisplayDeviceInfo deviceInfo = new DisplayDeviceInfo(); + + when(mLogicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId); + when(mLogicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(mDisplayDeviceMock); + when(mLogicalDisplayMock.getDisplayInfoLocked()).thenReturn(info); + when(mLogicalDisplayMock.isEnabled()).thenReturn(true); + when(mLogicalDisplayMock.getPhase()).thenReturn(LogicalDisplay.DISPLAY_PHASE_ENABLED); + when(mDisplayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo); + when(mDisplayDeviceMock.getUniqueId()).thenReturn(uniqueId); + when(mDisplayDeviceMock.getDisplayDeviceConfig()).thenReturn(mDisplayDeviceConfigMock); + when(mDisplayDeviceConfigMock.getProximitySensor()).thenReturn( + new DisplayDeviceConfig.SensorData() { + { + type = Sensor.STRING_TYPE_PROXIMITY; + name = null; + } + }); + when(mDisplayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500}); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java index bc9e9bb9bce8..941a3a419d59 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java @@ -16,14 +16,18 @@ package com.android.server.display.color; +import static android.view.Display.DEFAULT_DISPLAY; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertArrayEquals; +import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; +import android.hardware.display.DisplayManagerInternal; import android.os.Binder; import android.os.IBinder; import android.view.SurfaceControl; @@ -50,6 +54,8 @@ public class DisplayWhiteBalanceTintControllerTest { private Context mMockedContext; @Mock private Resources mMockedResources; + @Mock + private DisplayManagerInternal mDisplayManagerInternal; private MockitoSession mSession; private Resources mResources; @@ -81,7 +87,6 @@ public class DisplayWhiteBalanceTintControllerTest { doReturn(mMockedResources).when(mMockedContext).getResources(); mDisplayToken = new Binder(); - doReturn(mDisplayToken).when(() -> SurfaceControl.getInternalDisplayToken()); } @After @@ -114,8 +119,8 @@ public class DisplayWhiteBalanceTintControllerTest { displayPrimaries.white.X = 0.950456f; displayPrimaries.white.Y = 1.000000f; displayPrimaries.white.Z = 1.089058f; - doReturn(displayPrimaries) - .when(() -> SurfaceControl.getDisplayNativePrimaries(mDisplayToken)); + when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY)) + .thenReturn(displayPrimaries); setUpTintController(); assertWithMessage("Setup with valid SurfaceControl failed") @@ -134,8 +139,8 @@ public class DisplayWhiteBalanceTintControllerTest { displayPrimaries.green = new CieXyz(); displayPrimaries.blue = new CieXyz(); displayPrimaries.white = new CieXyz(); - doReturn(displayPrimaries) - .when(() -> SurfaceControl.getDisplayNativePrimaries(mDisplayToken)); + when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY)) + .thenReturn(displayPrimaries); setUpTintController(); assertWithMessage("Setup with invalid SurfaceControl succeeded") @@ -154,7 +159,7 @@ public class DisplayWhiteBalanceTintControllerTest { .when(mMockedResources) .getStringArray(R.array.config_displayWhiteBalanceDisplayPrimaries); // Make SurfaceControl setup fail - doReturn(null).when(() -> SurfaceControl.getDisplayNativePrimaries(mDisplayToken)); + when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY)).thenReturn(null); setUpTintController(); assertWithMessage("Setup with valid Resources failed") @@ -178,7 +183,7 @@ public class DisplayWhiteBalanceTintControllerTest { .when(mMockedResources) .getStringArray(R.array.config_displayWhiteBalanceDisplayPrimaries); // Make SurfaceControl setup fail - doReturn(null).when(() -> SurfaceControl.getDisplayNativePrimaries(mDisplayToken)); + when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY)).thenReturn(null); setUpTintController(); assertWithMessage("Setup with invalid Resources succeeded") @@ -208,8 +213,8 @@ public class DisplayWhiteBalanceTintControllerTest { displayPrimaries.white.X = 0.950456f; displayPrimaries.white.Y = 1.000000f; displayPrimaries.white.Z = 1.089058f; - doReturn(displayPrimaries) - .when(() -> SurfaceControl.getDisplayNativePrimaries(mDisplayToken)); + when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY)) + .thenReturn(displayPrimaries); setUpTintController(); assertWithMessage("Setup with valid SurfaceControl failed") @@ -234,7 +239,8 @@ public class DisplayWhiteBalanceTintControllerTest { } private void setUpTintController() { - mDisplayWhiteBalanceTintController = new DisplayWhiteBalanceTintController(); + mDisplayWhiteBalanceTintController = new DisplayWhiteBalanceTintController( + mDisplayManagerInternal); mDisplayWhiteBalanceTintController.setUp(mMockedContext, true); mDisplayWhiteBalanceTintController.setActivated(true); } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt b/services/tests/mockingservicestests/src/com/android/server/location/injector/FakePackageResetHelper.java index 1b8a44bbe78e..c2768d51970a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt +++ b/services/tests/mockingservicestests/src/com/android/server/location/injector/FakePackageResetHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -14,19 +14,25 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.helpers +package com.android.server.location.injector; -import android.app.Instrumentation -import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.wm.shell.flicker.testapp.Components +/** Version of PackageResetHelper for testing. */ +public class FakePackageResetHelper extends PackageResetHelper { -class LaunchBubbleHelper(instrumentation: Instrumentation) : BaseAppHelper( - instrumentation, - Components.LaunchBubbleActivity.LABEL, - Components.LaunchBubbleActivity.COMPONENT.toFlickerComponent() -) { + public FakePackageResetHelper() {} - companion object { - const val TIMEOUT_MS = 3_000L + @Override + protected void onRegister() {} + + @Override + protected void onUnregister() {} + + public boolean isResetableForPackage(String packageName) { + return queryResetableForPackage(packageName); + } + + public void reset(String packageName) { + notifyPackageReset(packageName); } } + diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java index 02cacb7bc57c..ca730910943b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java @@ -35,6 +35,7 @@ public class TestInjector implements Injector { private final FakeDeviceIdleHelper mDeviceIdleHelper; private final FakeEmergencyHelper mEmergencyHelper; private final LocationUsageLogger mLocationUsageLogger; + private final FakePackageResetHelper mPackageResetHelper; public TestInjector(Context context) { mUserInfoHelper = new FakeUserInfoHelper(); @@ -50,6 +51,7 @@ public class TestInjector implements Injector { mDeviceIdleHelper = new FakeDeviceIdleHelper(); mEmergencyHelper = new FakeEmergencyHelper(); mLocationUsageLogger = new LocationUsageLogger(); + mPackageResetHelper = new FakePackageResetHelper(); } @Override @@ -116,4 +118,9 @@ public class TestInjector implements Injector { public LocationUsageLogger getLocationUsageLogger() { return mLocationUsageLogger; } + + @Override + public FakePackageResetHelper getPackageResetHelper() { + return mPackageResetHelper; + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java index 0ac14432d113..20e4e8011327 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java @@ -1218,6 +1218,44 @@ public class LocationProviderManagerTest { assertThat(mProvider.getRequest().isActive()).isFalse(); } + @Test + public void testQueryPackageReset() { + assertThat(mInjector.getPackageResetHelper().isResetableForPackage("mypackage")).isFalse(); + + ILocationListener listener1 = createMockLocationListener(); + mManager.registerLocationRequest(new LocationRequest.Builder(0).setWorkSource( + WORK_SOURCE).build(), IDENTITY, PERMISSION_FINE, listener1); + assertThat(mInjector.getPackageResetHelper().isResetableForPackage("mypackage")).isTrue(); + + ILocationListener listener2 = createMockLocationListener(); + mManager.registerLocationRequest(new LocationRequest.Builder(0).setWorkSource( + WORK_SOURCE).build(), IDENTITY, PERMISSION_FINE, listener2); + assertThat(mInjector.getPackageResetHelper().isResetableForPackage("mypackage")).isTrue(); + + mManager.unregisterLocationRequest(listener1); + assertThat(mInjector.getPackageResetHelper().isResetableForPackage("mypackage")).isTrue(); + + mManager.unregisterLocationRequest(listener2); + assertThat(mInjector.getPackageResetHelper().isResetableForPackage("mypackage")).isFalse(); + } + + @Test + public void testPackageReset() { + ILocationListener listener1 = createMockLocationListener(); + mManager.registerLocationRequest(new LocationRequest.Builder(0).setWorkSource( + WORK_SOURCE).build(), IDENTITY, PERMISSION_FINE, listener1); + ILocationListener listener2 = createMockLocationListener(); + mManager.registerLocationRequest(new LocationRequest.Builder(0).setWorkSource( + WORK_SOURCE).build(), IDENTITY, PERMISSION_FINE, listener2); + + assertThat(mProvider.getRequest().isActive()).isTrue(); + assertThat(mInjector.getPackageResetHelper().isResetableForPackage("mypackage")).isTrue(); + + mInjector.getPackageResetHelper().reset("mypackage"); + assertThat(mProvider.getRequest().isActive()).isFalse(); + assertThat(mInjector.getPackageResetHelper().isResetableForPackage("mypackage")).isFalse(); + } + private ILocationListener createMockLocationListener() { return spy(new ILocationListener.Stub() { @Override diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt index bb5b1d8a67a9..cc57b9f913e3 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt @@ -62,10 +62,10 @@ import com.android.server.compat.PlatformCompat import com.android.server.extendedtestutils.wheneverStatic import com.android.server.pm.dex.DexManager import com.android.server.pm.parsing.PackageParser2 -import com.android.server.pm.pkg.AndroidPackage import com.android.server.pm.parsing.pkg.PackageImpl import com.android.server.pm.parsing.pkg.ParsedPackage import com.android.server.pm.permission.PermissionManagerServiceInternal +import com.android.server.pm.pkg.AndroidPackage import com.android.server.pm.pkg.parsing.ParsingPackage import com.android.server.pm.pkg.parsing.ParsingPackageUtils import com.android.server.pm.resolution.ComponentResolver @@ -77,14 +77,6 @@ import com.android.server.testutils.mock import com.android.server.testutils.nullable import com.android.server.testutils.whenever import com.android.server.utils.WatchedArrayMap -import libcore.util.HexEncoding -import org.junit.Assert -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement -import org.mockito.AdditionalMatchers.or -import org.mockito.Mockito -import org.mockito.quality.Strictness import java.io.File import java.io.IOException import java.nio.file.Files @@ -93,6 +85,14 @@ import java.security.cert.CertificateException import java.util.Arrays import java.util.Random import java.util.concurrent.FutureTask +import libcore.util.HexEncoding +import org.junit.Assert +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import org.mockito.AdditionalMatchers.or +import org.mockito.Mockito +import org.mockito.quality.Strictness /** * A utility for mocking behavior of the system and dependencies when testing PackageManagerService @@ -522,7 +522,7 @@ class MockSystem(withSession: (StaticMockitoSessionBuilder) -> Unit = {}) { whenever(mocks.packageParser.parsePackage( or(eq(path), eq(basePath)), anyInt(), anyBoolean())) { parsedPackage } whenever(mocks.packageParser.parsePackage( - or(eq(path), eq(basePath)), anyInt(), anyBoolean(), any())) { parsedPackage } + or(eq(path), eq(basePath)), anyInt(), anyBoolean())) { parsedPackage } return parsedPackage } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceBootTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceBootTest.kt index 987192d41203..da929af3267b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceBootTest.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceBootTest.kt @@ -23,6 +23,7 @@ import android.os.Process import android.util.Log import com.android.server.pm.pkg.AndroidPackage import com.android.server.testutils.whenever +import java.io.File import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.notNullValue @@ -33,13 +34,11 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.argThat import org.mockito.Mockito import org.mockito.Mockito.verify -import java.io.File @RunWith(JUnit4::class) class PackageManagerServiceBootTest { @@ -120,8 +119,7 @@ class PackageManagerServiceBootTest { whenever(rule.mocks().packageParser.parsePackage( argThat { path: File -> path.path.contains("a.data.package") }, anyInt(), - anyBoolean(), - any())) + anyBoolean())) .thenThrow(PackageManagerException( PackageManager.INSTALL_FAILED_INVALID_APK, "Oh no!")) val pm = createPackageManagerService() diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java new file mode 100644 index 000000000000..33870f1d3b86 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java @@ -0,0 +1,334 @@ +/* + * 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.trust; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.argThat; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static com.google.common.truth.Truth.assertThat; + +import android.annotation.Nullable; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +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.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.net.Uri; +import android.os.Handler; +import android.os.UserHandle; +import android.os.test.TestLooper; +import android.provider.Settings; +import android.service.trust.TrustAgentService; +import android.testing.TestableContext; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.internal.widget.LockPatternUtils; +import com.android.server.LocalServices; +import com.android.server.SystemService; +import com.android.server.SystemServiceManager; + +import com.google.android.collect.Lists; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.ArrayList; +import java.util.Collections; + +public class TrustManagerServiceTest { + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final MockContext mMockContext = new MockContext( + ApplicationProvider.getApplicationContext()); + + private static final String URI_SCHEME_PACKAGE = "package"; + private static final int TEST_USER_ID = UserHandle.USER_SYSTEM; + + private final TestLooper mLooper = new TestLooper(); + private final ArrayList<ResolveInfo> mTrustAgentResolveInfoList = new ArrayList<>(); + private final LockPatternUtils mLockPatternUtils = new LockPatternUtils(mMockContext); + private final TrustManagerService mService = new TrustManagerService(mMockContext); + + @Mock + private PackageManager mPackageManagerMock; + + @Before + public void setUp() { + resetTrustAgentLockSettings(); + LocalServices.addService(SystemServiceManager.class, mock(SystemServiceManager.class)); + + ArgumentMatcher<Intent> trustAgentIntentMatcher = new ArgumentMatcher<Intent>() { + @Override + public boolean matches(Intent argument) { + return TrustAgentService.SERVICE_INTERFACE.equals(argument.getAction()); + } + }; + when(mPackageManagerMock.queryIntentServicesAsUser(argThat(trustAgentIntentMatcher), + anyInt(), anyInt())).thenReturn(mTrustAgentResolveInfoList); + when(mPackageManagerMock.checkPermission(any(), any())).thenReturn( + PackageManager.PERMISSION_GRANTED); + mMockContext.setMockPackageManager(mPackageManagerMock); + } + + @After + public void tearDown() { + resetTrustAgentLockSettings(); + LocalServices.removeServiceForTest(SystemServiceManager.class); + } + + @Test + public void firstBootCompleted_systemTrustAgentsEnabled() { + ComponentName systemTrustAgent1 = ComponentName.unflattenFromString( + "com.android/.SystemTrustAgent"); + ComponentName systemTrustAgent2 = ComponentName.unflattenFromString( + "com.android/.AnotherSystemTrustAgent"); + ComponentName userTrustAgent1 = ComponentName.unflattenFromString( + "com.user/.UserTrustAgent"); + ComponentName userTrustAgent2 = ComponentName.unflattenFromString( + "com.user/.AnotherUserTrustAgent"); + addTrustAgent(systemTrustAgent1, /* isSystemApp= */ true); + addTrustAgent(systemTrustAgent2, /* isSystemApp= */ true); + addTrustAgent(userTrustAgent1, /* isSystemApp= */ false); + addTrustAgent(userTrustAgent2, /* isSystemApp= */ false); + + bootService(); + + assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( + systemTrustAgent1, systemTrustAgent2); + assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( + systemTrustAgent1, systemTrustAgent2, userTrustAgent1, userTrustAgent2); + } + + @Test + public void firstBootCompleted_defaultTrustAgentEnabled() { + ComponentName systemTrustAgent = ComponentName.unflattenFromString( + "com.android/.SystemTrustAgent"); + ComponentName defaultTrustAgent = ComponentName.unflattenFromString( + "com.user/.DefaultTrustAgent"); + addTrustAgent(systemTrustAgent, /* isSystemApp= */ true); + addTrustAgent(defaultTrustAgent, /* isSystemApp= */ false); + mMockContext.getOrCreateTestableResources().addOverride( + com.android.internal.R.string.config_defaultTrustAgent, + defaultTrustAgent.flattenToString()); + + bootService(); + + assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( + defaultTrustAgent); + assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( + systemTrustAgent, defaultTrustAgent); + } + + @Test + public void serviceBooted_knownAgentsNotSet_enabledAgentsNotUpdated() { + ComponentName trustAgent1 = ComponentName.unflattenFromString( + "com.android/.SystemTrustAgent"); + ComponentName trustAgent2 = ComponentName.unflattenFromString( + "com.android/.AnotherSystemTrustAgent"); + initializeEnabledAgents(trustAgent1); + addTrustAgent(trustAgent1, /* isSystemApp= */ true); + addTrustAgent(trustAgent2, /* isSystemApp= */ true); + + bootService(); + + assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( + trustAgent1); + assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( + trustAgent1, trustAgent2); + } + + @Test + public void serviceBooted_knownAgentsSet_enabledAgentsUpdated() { + ComponentName trustAgent1 = ComponentName.unflattenFromString( + "com.android/.SystemTrustAgent"); + ComponentName trustAgent2 = ComponentName.unflattenFromString( + "com.android/.AnotherSystemTrustAgent"); + initializeEnabledAgents(trustAgent1); + initializeKnownAgents(trustAgent1); + addTrustAgent(trustAgent1, /* isSystemApp= */ true); + addTrustAgent(trustAgent2, /* isSystemApp= */ true); + + bootService(); + + assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( + trustAgent1, trustAgent2); + assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( + trustAgent1, trustAgent2); + } + + @Test + public void newSystemTrustAgent_setToEnabledAndKnown() { + bootService(); + ComponentName newAgentComponentName = ComponentName.unflattenFromString( + "com.android/.SystemTrustAgent"); + addTrustAgent(newAgentComponentName, /* isSystemApp= */ true); + + mMockContext.sendPackageChangedBroadcast(newAgentComponentName); + + assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( + newAgentComponentName); + assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( + newAgentComponentName); + } + + @Test + public void newSystemTrustAgent_notEnabledWhenDefaultAgentIsSet() { + ComponentName defaultTrustAgent = ComponentName.unflattenFromString( + "com.user/.DefaultTrustAgent"); + addTrustAgent(defaultTrustAgent, /* isSystemApp= */ false); + mMockContext.getOrCreateTestableResources().addOverride( + com.android.internal.R.string.config_defaultTrustAgent, + defaultTrustAgent.flattenToString()); + bootService(); + ComponentName newAgentComponentName = ComponentName.unflattenFromString( + "com.android/.SystemTrustAgent"); + addTrustAgent(newAgentComponentName, /* isSystemApp= */ true); + + mMockContext.sendPackageChangedBroadcast(newAgentComponentName); + + assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( + defaultTrustAgent); + assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( + defaultTrustAgent, newAgentComponentName); + } + + @Test + public void newNonSystemTrustAgent_notEnabledButMarkedAsKnown() { + bootService(); + ComponentName newAgentComponentName = ComponentName.unflattenFromString( + "com.user/.UserTrustAgent"); + addTrustAgent(newAgentComponentName, /* isSystemApp= */ false); + + mMockContext.sendPackageChangedBroadcast(newAgentComponentName); + + assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).isEmpty(); + assertThat(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).containsExactly( + newAgentComponentName); + } + + @Test + public void existingTrustAgentChanged_notEnabled() { + ComponentName systemTrustAgent1 = ComponentName.unflattenFromString( + "com.android/.SystemTrustAgent"); + ComponentName systemTrustAgent2 = ComponentName.unflattenFromString( + "com.android/.AnotherSystemTrustAgent"); + addTrustAgent(systemTrustAgent1, /* isSystemApp= */ true); + addTrustAgent(systemTrustAgent2, /* isSystemApp= */ true); + bootService(); + // Simulate user turning off systemTrustAgent2 + mLockPatternUtils.setEnabledTrustAgents(Collections.singletonList(systemTrustAgent1), + TEST_USER_ID); + + mMockContext.sendPackageChangedBroadcast(systemTrustAgent2); + + assertThat(mLockPatternUtils.getEnabledTrustAgents(TEST_USER_ID)).containsExactly( + systemTrustAgent1); + } + + private void addTrustAgent(ComponentName agentComponentName, boolean isSystemApp) { + ApplicationInfo applicationInfo = new ApplicationInfo(); + if (isSystemApp) { + applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; + } + + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = agentComponentName.getPackageName(); + serviceInfo.name = agentComponentName.getClassName(); + serviceInfo.applicationInfo = applicationInfo; + + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.serviceInfo = serviceInfo; + mTrustAgentResolveInfoList.add(resolveInfo); + } + + private void initializeEnabledAgents(ComponentName... enabledAgents) { + mLockPatternUtils.setEnabledTrustAgents(Lists.newArrayList(enabledAgents), TEST_USER_ID); + Settings.Secure.putIntForUser(mMockContext.getContentResolver(), + Settings.Secure.TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID); + } + + private void initializeKnownAgents(ComponentName... knownAgents) { + mLockPatternUtils.setKnownTrustAgents(Lists.newArrayList(knownAgents), TEST_USER_ID); + Settings.Secure.putIntForUser(mMockContext.getContentResolver(), + Settings.Secure.KNOWN_TRUST_AGENTS_INITIALIZED, 1, TEST_USER_ID); + } + + private void bootService() { + mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); + mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START); + mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); + } + + private void resetTrustAgentLockSettings() { + mLockPatternUtils.setEnabledTrustAgents(Collections.emptyList(), TEST_USER_ID); + mLockPatternUtils.setKnownTrustAgents(Collections.emptyList(), TEST_USER_ID); + } + + /** A mock Context that allows the test process to send protected broadcasts. */ + private static final class MockContext extends TestableContext { + + private final ArrayList<BroadcastReceiver> mPackageChangedBroadcastReceivers = + new ArrayList<>(); + + MockContext(Context base) { + super(base); + } + + @Override + @Nullable + public Intent registerReceiverAsUser(BroadcastReceiver receiver, + UserHandle user, IntentFilter filter, @Nullable String broadcastPermission, + @Nullable Handler scheduler) { + + if (filter.hasAction(Intent.ACTION_PACKAGE_CHANGED)) { + mPackageChangedBroadcastReceivers.add(receiver); + } + return super.registerReceiverAsUser(receiver, user, filter, broadcastPermission, + scheduler); + } + + void sendPackageChangedBroadcast(ComponentName changedComponent) { + Intent intent = new Intent( + Intent.ACTION_PACKAGE_CHANGED, + Uri.fromParts(URI_SCHEME_PACKAGE, + changedComponent.getPackageName(), /* fragment= */ null)) + .putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, + new String[]{changedComponent.getClassName()}) + .putExtra(Intent.EXTRA_USER_HANDLE, TEST_USER_ID) + .putExtra(Intent.EXTRA_UID, UserHandle.of(TEST_USER_ID).getUid(1234)); + for (BroadcastReceiver receiver : mPackageChangedBroadcastReceivers) { + receiver.onReceive(this, intent); + } + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java index 3f4148bb49e1..a45144e23c17 100644 --- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java @@ -588,6 +588,157 @@ public final class DeviceStateManagerServiceTest { }); } + @Test + public void requestBaseStateOverride() throws RemoteException { + TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); + mService.getBinderService().registerCallback(callback); + flushHandler(); + + final IBinder token = new Binder(); + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_UNKNOWN); + + mService.getBinderService().requestBaseStateOverride(token, + OTHER_DEVICE_STATE.getIdentifier(), + 0 /* flags */); + // Flush the handler twice. The first flush ensures the request is added and the policy is + // notified, while the second flush ensures the callback is notified once the change is + // committed. + flushHandler(2 /* count */); + + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_ACTIVE); + // Committed state changes as there is a requested override. + assertEquals(mService.getCommittedState(), Optional.of(OTHER_DEVICE_STATE)); + assertEquals(mSysPropSetter.getValue(), + OTHER_DEVICE_STATE.getIdentifier() + ":" + OTHER_DEVICE_STATE.getName()); + assertEquals(mService.getBaseState(), Optional.of(OTHER_DEVICE_STATE)); + assertEquals(mService.getOverrideBaseState().get(), OTHER_DEVICE_STATE); + assertFalse(mService.getOverrideState().isPresent()); + assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), + OTHER_DEVICE_STATE.getIdentifier()); + + assertNotNull(callback.getLastNotifiedInfo()); + assertEquals(callback.getLastNotifiedInfo().baseState, + OTHER_DEVICE_STATE.getIdentifier()); + assertEquals(callback.getLastNotifiedInfo().currentState, + OTHER_DEVICE_STATE.getIdentifier()); + + mService.getBinderService().cancelBaseStateOverride(); + flushHandler(); + + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_CANCELED); + // Committed state is set back to the requested state once the override is cleared. + assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE)); + assertEquals(mSysPropSetter.getValue(), + DEFAULT_DEVICE_STATE.getIdentifier() + ":" + DEFAULT_DEVICE_STATE.getName()); + assertEquals(mService.getBaseState(), Optional.of(DEFAULT_DEVICE_STATE)); + assertFalse(mService.getOverrideBaseState().isPresent()); + assertFalse(mService.getOverrideState().isPresent()); + assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), + DEFAULT_DEVICE_STATE.getIdentifier()); + + assertEquals(callback.getLastNotifiedInfo().baseState, + DEFAULT_DEVICE_STATE.getIdentifier()); + assertEquals(callback.getLastNotifiedInfo().currentState, + DEFAULT_DEVICE_STATE.getIdentifier()); + } + + @Test + public void requestBaseStateOverride_cancelledByBaseStateUpdate() throws RemoteException { + final DeviceState testDeviceState = new DeviceState(2, "TEST", 0); + TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); + mService.getBinderService().registerCallback(callback); + mProvider.notifySupportedDeviceStates( + new DeviceState[]{DEFAULT_DEVICE_STATE, OTHER_DEVICE_STATE, testDeviceState }); + flushHandler(); + + final IBinder token = new Binder(); + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_UNKNOWN); + + mService.getBinderService().requestBaseStateOverride(token, + OTHER_DEVICE_STATE.getIdentifier(), + 0 /* flags */); + // Flush the handler twice. The first flush ensures the request is added and the policy is + // notified, while the second flush ensures the callback is notified once the change is + // committed. + flushHandler(2 /* count */); + + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_ACTIVE); + // Committed state changes as there is a requested override. + assertEquals(mService.getCommittedState(), Optional.of(OTHER_DEVICE_STATE)); + assertEquals(mSysPropSetter.getValue(), + OTHER_DEVICE_STATE.getIdentifier() + ":" + OTHER_DEVICE_STATE.getName()); + assertEquals(mService.getBaseState(), Optional.of(OTHER_DEVICE_STATE)); + assertEquals(mService.getOverrideBaseState().get(), OTHER_DEVICE_STATE); + assertFalse(mService.getOverrideState().isPresent()); + assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), + OTHER_DEVICE_STATE.getIdentifier()); + + assertNotNull(callback.getLastNotifiedInfo()); + assertEquals(callback.getLastNotifiedInfo().baseState, + OTHER_DEVICE_STATE.getIdentifier()); + assertEquals(callback.getLastNotifiedInfo().currentState, + OTHER_DEVICE_STATE.getIdentifier()); + + mProvider.setState(testDeviceState.getIdentifier()); + flushHandler(); + + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_CANCELED); + // Committed state is set to the new base state once the override is cleared. + assertEquals(mService.getCommittedState(), Optional.of(testDeviceState)); + assertEquals(mSysPropSetter.getValue(), + testDeviceState.getIdentifier() + ":" + testDeviceState.getName()); + assertEquals(mService.getBaseState(), Optional.of(testDeviceState)); + assertFalse(mService.getOverrideBaseState().isPresent()); + assertFalse(mService.getOverrideState().isPresent()); + assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), + testDeviceState.getIdentifier()); + + assertEquals(callback.getLastNotifiedInfo().baseState, + testDeviceState.getIdentifier()); + assertEquals(callback.getLastNotifiedInfo().currentState, + testDeviceState.getIdentifier()); + } + + @Test + public void requestBaseState_unsupportedState() throws RemoteException { + TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); + mService.getBinderService().registerCallback(callback); + + assertThrows(IllegalArgumentException.class, () -> { + final IBinder token = new Binder(); + mService.getBinderService().requestBaseStateOverride(token, + UNSUPPORTED_DEVICE_STATE.getIdentifier(), 0 /* flags */); + }); + } + + @Test + public void requestBaseState_invalidState() throws RemoteException { + TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); + mService.getBinderService().registerCallback(callback); + + assertThrows(IllegalArgumentException.class, () -> { + final IBinder token = new Binder(); + mService.getBinderService().requestBaseStateOverride(token, INVALID_DEVICE_STATE, + 0 /* flags */); + }); + } + + @Test + public void requestBaseState_beforeRegisteringCallback() { + assertThrows(IllegalStateException.class, () -> { + final IBinder token = new Binder(); + mService.getBinderService().requestBaseStateOverride(token, + DEFAULT_DEVICE_STATE.getIdentifier(), + 0 /* flags */); + }); + } + private static void assertArrayEquals(int[] expected, int[] actual) { Assert.assertTrue(Arrays.equals(expected, actual)); } diff --git a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java index 2297c91818c0..430504ca2428 100644 --- a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java @@ -16,6 +16,8 @@ package com.android.server.devicestate; +import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_BASE_STATE; +import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_EMULATED_STATE; import static com.android.server.devicestate.OverrideRequestController.STATUS_ACTIVE; import static com.android.server.devicestate.OverrideRequestController.STATUS_CANCELED; @@ -57,7 +59,7 @@ public final class OverrideRequestControllerTest { @Test public void addRequest() { OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */, - 0 /* requestedState */, 0 /* flags */); + 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); assertNull(mStatusListener.getLastStatus(request)); mController.addRequest(request); @@ -67,14 +69,14 @@ public final class OverrideRequestControllerTest { @Test public void addRequest_cancelExistingRequestThroughNewRequest() { OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, - 0 /* requestedState */, 0 /* flags */); + 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); assertNull(mStatusListener.getLastStatus(firstRequest)); mController.addRequest(firstRequest); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE); OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */, - 1 /* requestedState */, 0 /* flags */); + 1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); assertNull(mStatusListener.getLastStatus(secondRequest)); mController.addRequest(secondRequest); @@ -85,7 +87,7 @@ public final class OverrideRequestControllerTest { @Test public void addRequest_cancelActiveRequest() { OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, - 0 /* requestedState */, 0 /* flags */); + 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); mController.addRequest(firstRequest); @@ -97,30 +99,90 @@ public final class OverrideRequestControllerTest { } @Test + public void addBaseStateRequest() { + OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */, + 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE); + assertNull(mStatusListener.getLastStatus(request)); + + mController.addBaseStateRequest(request); + assertEquals(mStatusListener.getLastStatus(request).intValue(), STATUS_ACTIVE); + } + + @Test + public void addBaseStateRequest_cancelExistingBaseStateRequestThroughNewRequest() { + OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, + 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE); + assertNull(mStatusListener.getLastStatus(firstRequest)); + + mController.addBaseStateRequest(firstRequest); + assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE); + + OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */, + 1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE); + assertNull(mStatusListener.getLastStatus(secondRequest)); + + mController.addBaseStateRequest(secondRequest); + assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE); + assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED); + } + + @Test + public void addBaseStateRequest_cancelActiveBaseStateRequest() { + OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, + 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE); + + mController.addBaseStateRequest(firstRequest); + + assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE); + + mController.cancelBaseStateOverrideRequest(); + + assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED); + } + + @Test public void handleBaseStateChanged() { OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* requestedState */, - DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES /* flags */); + DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES /* flags */, + OVERRIDE_REQUEST_TYPE_EMULATED_STATE); + + OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */, + 0 /* requestedState */, + 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE); mController.addRequest(firstRequest); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE); - mController.handleBaseStateChanged(); + mController.addBaseStateRequest(baseStateRequest); + + assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_ACTIVE); + + mController.handleBaseStateChanged(1); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED); + assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_CANCELED); } @Test public void handleProcessDied() { OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, - 0 /* requestedState */, 0 /* flags */); + 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); + + OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */, + 1 /* requestedState */, + 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE); mController.addRequest(firstRequest); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE); + mController.addBaseStateRequest(baseStateRequest); + assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_ACTIVE); + mController.handleProcessDied(0); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED); + assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_CANCELED); } @Test @@ -128,13 +190,20 @@ public final class OverrideRequestControllerTest { mController.setStickyRequestsAllowed(true); OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, - 0 /* requestedState */, 0 /* flags */); + 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); + + OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */, + 1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE); mController.addRequest(firstRequest); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE); + mController.addBaseStateRequest(baseStateRequest); + assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_ACTIVE); + mController.handleProcessDied(0); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE); + assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_CANCELED); mController.cancelStickyRequest(); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED); @@ -143,22 +212,31 @@ public final class OverrideRequestControllerTest { @Test public void handleNewSupportedStates() { OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, - 1 /* requestedState */, 0 /* flags */); + 1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); + + OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */, + 1 /* requestedState */, + 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE); mController.addRequest(firstRequest); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE); - mController.handleNewSupportedStates(new int[]{ 0, 1 }); + mController.addBaseStateRequest(baseStateRequest); + assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_ACTIVE); + + mController.handleNewSupportedStates(new int[]{0, 1}); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE); + assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_ACTIVE); - mController.handleNewSupportedStates(new int[]{ 0 }); + mController.handleNewSupportedStates(new int[]{0}); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED); + assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_CANCELED); } @Test public void cancelOverrideRequestsTest() { OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, - 1 /* requestedState */, 0 /* flags */); + 1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); mController.addRequest(firstRequest); assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE); diff --git a/services/tests/servicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java b/services/tests/servicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java index 4ef156e239d3..e0bef1a83821 100644 --- a/services/tests/servicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java @@ -18,21 +18,26 @@ package com.android.server.display.color; import static com.google.common.truth.Truth.assertWithMessage; -import androidx.test.InstrumentationRegistry; +import static org.mockito.Mockito.mock; -import java.lang.System; -import java.util.Arrays; +import android.hardware.display.DisplayManagerInternal; + +import androidx.test.InstrumentationRegistry; import org.junit.Before; import org.junit.Test; +import java.util.Arrays; + public class DisplayWhiteBalanceTintControllerTest { private DisplayWhiteBalanceTintController mDisplayWhiteBalanceTintController; @Before public void setUp() { - mDisplayWhiteBalanceTintController = new DisplayWhiteBalanceTintController(); + DisplayManagerInternal displayManagerInternal = mock(DisplayManagerInternal.class); + mDisplayWhiteBalanceTintController = + new DisplayWhiteBalanceTintController(displayManagerInternal); mDisplayWhiteBalanceTintController.setUp(InstrumentationRegistry.getContext(), true); mDisplayWhiteBalanceTintController.setActivated(true); } diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java index e04edc6d7db2..96707fde8edb 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -216,6 +216,25 @@ public class UserManagerServiceTest { assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isTrue(); } + @Test + public void assertIsUserSwitcherEnabled() throws Exception { + int userId = ActivityManager.getCurrentUser(); + setMaxSupportedUsers(8); + assertThat(mUserManagerService.isUserSwitcherEnabled(true, userId)).isTrue(); + + setUserSwitch(false); + assertThat(mUserManagerService.isUserSwitcherEnabled(true, userId)).isFalse(); + + setUserSwitch(true); + assertThat(mUserManagerService.isUserSwitcherEnabled(false, userId)).isTrue(); + + mUserManagerService.setUserRestriction(UserManager.DISALLOW_ADD_USER, true, userId); + assertThat(mUserManagerService.isUserSwitcherEnabled(false, userId)).isFalse(); + + mUserManagerService.setUserRestriction(UserManager.DISALLOW_ADD_USER, false, userId); + setMaxSupportedUsers(1); + assertThat(mUserManagerService.isUserSwitcherEnabled(true, userId)).isFalse(); + } @Test public void assertIsUserSwitcherEnabledOnShowMultiuserUI() throws Exception { diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml index 107bbe1c79e3..2918365b94c8 100644 --- a/services/tests/wmtests/AndroidManifest.xml +++ b/services/tests/wmtests/AndroidManifest.xml @@ -79,10 +79,7 @@ <activity android:name="com.android.server.wm.ActivityOptionsTest$MainActivity" android:turnScreenOn="true" android:showWhenLocked="true" /> - <activity android:name="com.android.server.wm.ScreenshotTests$ScreenshotActivity" - android:theme="@style/WhiteBackgroundTheme" - android:turnScreenOn="true" - android:showWhenLocked="true"/> + <activity android:name="com.android.server.wm.ScreenshotTests$ScreenshotActivity" /> <activity android:name="android.view.cts.surfacevalidator.CapturedActivity"/> <service android:name="android.view.cts.surfacevalidator.LocalMediaProjectionService" diff --git a/services/tests/wmtests/src/com/android/server/wm/ScreenshotTests.java b/services/tests/wmtests/src/com/android/server/wm/ScreenshotTests.java index 8cd8e9bde1b7..0b58428e9bfb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ScreenshotTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ScreenshotTests.java @@ -16,10 +16,6 @@ package com.android.server.wm; -import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.WindowInsets.Type.displayCutout; -import static android.view.WindowInsets.Type.statusBars; - import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static org.junit.Assert.assertNotNull; @@ -27,31 +23,20 @@ import static org.junit.Assert.assertTrue; import android.app.Activity; import android.app.Instrumentation; -import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.ColorSpace; import android.graphics.GraphicBuffer; -import android.graphics.Insets; import android.graphics.PixelFormat; -import android.graphics.Point; -import android.graphics.Rect; -import android.hardware.DataSpace; -import android.hardware.HardwareBuffer; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.RemoteException; -import android.os.ServiceManager; import android.platform.test.annotations.Presubmit; -import android.view.IWindowManager; import android.view.PointerIcon; import android.view.SurfaceControl; -import android.view.cts.surfacevalidator.BitmapPixelChecker; -import android.view.cts.surfacevalidator.PixelColor; -import android.view.cts.surfacevalidator.SaveBitmapHelper; +import android.view.WindowManager; import android.window.ScreenCapture; -import android.window.ScreenCapture.SyncScreenCaptureListener; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; @@ -60,7 +45,6 @@ import androidx.test.rule.ActivityTestRule; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TestName; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -76,8 +60,6 @@ public class ScreenshotTests { private static final int BUFFER_HEIGHT = 100; private final Instrumentation mInstrumentation = getInstrumentation(); - @Rule - public TestName mTestName = new TestName(); @Rule public ActivityTestRule<ScreenshotActivity> mActivityRule = @@ -113,8 +95,8 @@ public class ScreenshotTests { buffer.unlockCanvasAndPost(canvas); t.show(secureSC) - .setBuffer(secureSC, HardwareBuffer.createFromGraphicBuffer(buffer)) - .setDataSpace(secureSC, DataSpace.DATASPACE_SRGB) + .setBuffer(secureSC, buffer) + .setColorSpace(secureSC, ColorSpace.get(ColorSpace.Named.SRGB)) .apply(true); ScreenCapture.LayerCaptureArgs args = new ScreenCapture.LayerCaptureArgs.Builder(secureSC) @@ -130,69 +112,15 @@ public class ScreenshotTests { Bitmap swBitmap = screenshot.copy(Bitmap.Config.ARGB_8888, false); screenshot.recycle(); - BitmapPixelChecker bitmapPixelChecker = new BitmapPixelChecker(PixelColor.RED); - Rect bounds = new Rect(0, 0, swBitmap.getWidth(), swBitmap.getHeight()); - int numMatchingPixels = bitmapPixelChecker.getNumMatchingPixels(swBitmap, bounds); - int sizeOfBitmap = bounds.width() * bounds.height(); + int numMatchingPixels = PixelChecker.getNumMatchingPixels(swBitmap, + new PixelColor(PixelColor.RED)); + long sizeOfBitmap = swBitmap.getWidth() * swBitmap.getHeight(); boolean success = numMatchingPixels == sizeOfBitmap; swBitmap.recycle(); assertTrue(success); } - @Test - public void testCaptureDisplay() throws RemoteException { - IWindowManager windowManager = IWindowManager.Stub.asInterface( - ServiceManager.getService(Context.WINDOW_SERVICE)); - SurfaceControl sc = new SurfaceControl.Builder() - .setName("Layer") - .setCallsite("testCaptureDisplay") - .build(); - - SurfaceControl.Transaction t = mActivity.addChildSc(sc); - mInstrumentation.waitForIdleSync(); - - GraphicBuffer buffer = GraphicBuffer.create(BUFFER_WIDTH, BUFFER_HEIGHT, - PixelFormat.RGBA_8888, - GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_HW_COMPOSER - | GraphicBuffer.USAGE_SW_WRITE_RARELY); - - Canvas canvas = buffer.lockCanvas(); - canvas.drawColor(Color.RED); - buffer.unlockCanvasAndPost(canvas); - - Point point = mActivity.getPositionBelowStatusBar(); - t.show(sc) - .setBuffer(sc, HardwareBuffer.createFromGraphicBuffer(buffer)) - .setDataSpace(sc, DataSpace.DATASPACE_SRGB) - .setPosition(sc, point.x, point.y) - .apply(true); - - SyncScreenCaptureListener listener = new SyncScreenCaptureListener(); - windowManager.captureDisplay(DEFAULT_DISPLAY, null, listener.getScreenCaptureListener()); - ScreenCapture.ScreenshotHardwareBuffer hardwareBuffer = listener.waitForScreenshot(); - assertNotNull(hardwareBuffer); - - Bitmap screenshot = hardwareBuffer.asBitmap(); - assertNotNull(screenshot); - - Bitmap swBitmap = screenshot.copy(Bitmap.Config.ARGB_8888, false); - screenshot.recycle(); - - BitmapPixelChecker bitmapPixelChecker = new BitmapPixelChecker(PixelColor.RED); - Rect bounds = new Rect(point.x, point.y, BUFFER_WIDTH + point.x, BUFFER_HEIGHT + point.y); - int numMatchingPixels = bitmapPixelChecker.getNumMatchingPixels(swBitmap, bounds); - int pixelMatchSize = bounds.width() * bounds.height(); - boolean success = numMatchingPixels == pixelMatchSize; - - if (!success) { - SaveBitmapHelper.saveBitmap(swBitmap, getClass(), mTestName, "failedImage"); - } - swBitmap.recycle(); - assertTrue("numMatchingPixels=" + numMatchingPixels + " pixelMatchSize=" + pixelMatchSize, - success); - } - public static class ScreenshotActivity extends Activity { private static final long WAIT_TIMEOUT_S = 5; private final Handler mHandler = new Handler(Looper.getMainLooper()); @@ -202,6 +130,7 @@ public class ScreenshotTests { super.onCreate(savedInstanceState); getWindow().getDecorView().setPointerIcon( PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL)); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } SurfaceControl.Transaction addChildSc(SurfaceControl surfaceControl) { @@ -219,14 +148,88 @@ public class ScreenshotTests { } return t; } + } - public Point getPositionBelowStatusBar() { - Insets statusBarInsets = getWindow() - .getDecorView() - .getRootWindowInsets() - .getInsets(statusBars() | displayCutout()); + public abstract static class PixelChecker { + static int getNumMatchingPixels(Bitmap bitmap, PixelColor pixelColor) { + int numMatchingPixels = 0; + for (int x = 0; x < bitmap.getWidth(); x++) { + for (int y = 0; y < bitmap.getHeight(); y++) { + int color = bitmap.getPixel(x, y); + if (matchesColor(pixelColor, color)) { + numMatchingPixels++; + } + } + } + return numMatchingPixels; + } + + static boolean matchesColor(PixelColor expectedColor, int color) { + final float red = Color.red(color); + final float green = Color.green(color); + final float blue = Color.blue(color); + final float alpha = Color.alpha(color); + + return alpha <= expectedColor.mMaxAlpha + && alpha >= expectedColor.mMinAlpha + && red <= expectedColor.mMaxRed + && red >= expectedColor.mMinRed + && green <= expectedColor.mMaxGreen + && green >= expectedColor.mMinGreen + && blue <= expectedColor.mMaxBlue + && blue >= expectedColor.mMinBlue; + } + } + + public static class PixelColor { + public static final int BLACK = 0xFF000000; + public static final int RED = 0xFF0000FF; + public static final int GREEN = 0xFF00FF00; + public static final int BLUE = 0xFFFF0000; + public static final int YELLOW = 0xFF00FFFF; + public static final int MAGENTA = 0xFFFF00FF; + public static final int WHITE = 0xFFFFFFFF; + + public static final int TRANSPARENT_RED = 0x7F0000FF; + public static final int TRANSPARENT_BLUE = 0x7FFF0000; + public static final int TRANSPARENT = 0x00000000; + + // Default to black + public short mMinAlpha; + public short mMaxAlpha; + public short mMinRed; + public short mMaxRed; + public short mMinBlue; + public short mMaxBlue; + public short mMinGreen; + public short mMaxGreen; + + public PixelColor(int color) { + short alpha = (short) ((color >> 24) & 0xFF); + short blue = (short) ((color >> 16) & 0xFF); + short green = (short) ((color >> 8) & 0xFF); + short red = (short) (color & 0xFF); + + mMinAlpha = (short) getMinValue(alpha); + mMaxAlpha = (short) getMaxValue(alpha); + mMinRed = (short) getMinValue(red); + mMaxRed = (short) getMaxValue(red); + mMinBlue = (short) getMinValue(blue); + mMaxBlue = (short) getMaxValue(blue); + mMinGreen = (short) getMinValue(green); + mMaxGreen = (short) getMaxValue(green); + } + + public PixelColor() { + this(BLACK); + } + + private int getMinValue(short color) { + return Math.max(color - 4, 0); + } - return new Point(statusBarInsets.left, statusBarInsets.top); + private int getMaxValue(short color) { + return Math.min(color + 4, 0xFF); } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index f5fc5c13eb4c..70e6f2947c36 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -39,6 +39,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; import android.app.ActivityManagerInternal; @@ -81,6 +82,7 @@ import com.android.server.am.ActivityManagerService; import com.android.server.display.color.ColorDisplayService; import com.android.server.firewall.IntentFirewall; import com.android.server.input.InputManagerService; +import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.policy.PermissionPolicyInternal; import com.android.server.policy.WindowManagerPolicy; @@ -93,6 +95,7 @@ import org.junit.runners.model.Statement; import org.mockito.MockSettings; import org.mockito.Mockito; import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; @@ -283,6 +286,16 @@ public class SystemServicesTestRule implements TestRule { // StatusBarManagerInternal final StatusBarManagerInternal sbmi = mock(StatusBarManagerInternal.class); doReturn(sbmi).when(() -> LocalServices.getService(eq(StatusBarManagerInternal.class))); + + // UserManagerInternal + final UserManagerInternal umi = mock(UserManagerInternal.class); + doReturn(umi).when(() -> LocalServices.getService(UserManagerInternal.class)); + Answer<Boolean> isUserVisibleAnswer = invocation -> { + int userId = invocation.getArgument(0); + return userId == mWmService.mCurrentUserId; + }; + when(umi.isUserVisible(anyInt())).thenAnswer(isUserVisibleAnswer); + when(umi.isUserVisible(anyInt(), anyInt())).thenAnswer(isUserVisibleAnswer); } private void setUpActivityTaskManagerService() { @@ -403,6 +416,7 @@ public class SystemServicesTestRule implements TestRule { LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class); LocalServices.removeServiceForTest(UsageStatsManagerInternal.class); LocalServices.removeServiceForTest(StatusBarManagerInternal.class); + LocalServices.removeServiceForTest(UserManagerInternal.class); } Description getDescription() { diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 9bdf750767b3..61cf8cc76d83 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -18,6 +18,7 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; import static android.window.TaskFragmentOrganizer.getTransitionType; @@ -76,6 +77,7 @@ import android.window.TaskFragmentCreationParams; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOrganizer; import android.window.TaskFragmentOrganizerToken; +import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -271,7 +273,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @Test public void testOnTaskFragmentParentInfoChanged() { setupMockParent(mTaskFragment, mTask); - mTask.getConfiguration().smallestScreenWidthDp = 10; + mTask.getTaskFragmentParentInfo().getConfiguration().smallestScreenWidthDp = 10; mController.onTaskFragmentAppeared( mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); @@ -295,7 +297,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Trigger callback if the size is changed. clearInvocations(mOrganizer); - mTask.getConfiguration().smallestScreenWidthDp = 100; + mTask.getTaskFragmentParentInfo().getConfiguration().smallestScreenWidthDp = 100; mController.onTaskFragmentInfoChanged( mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); mController.dispatchPendingEvents(); @@ -304,7 +306,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Trigger callback if the windowing mode is changed. clearInvocations(mOrganizer); - mTask.getConfiguration().windowConfiguration.setWindowingMode(WINDOWING_MODE_PINNED); + mTask.getTaskFragmentParentInfo().getConfiguration().windowConfiguration + .setWindowingMode(WINDOWING_MODE_PINNED); mController.onTaskFragmentInfoChanged( mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); mController.dispatchPendingEvents(); @@ -1268,7 +1271,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final TaskFragmentTransaction.Change change = changes.get(0); assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, change.getType()); assertEquals(task.mTaskId, change.getTaskId()); - assertEquals(task.getConfiguration(), change.getTaskConfiguration()); + assertEquals(task.getTaskFragmentParentInfo(), change.getTaskFragmentParentInfo()); } /** Asserts that there will be a transaction for TaskFragment error. */ @@ -1316,8 +1319,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { /** Setups the mock Task as the parent of the given TaskFragment. */ private static void setupMockParent(TaskFragment taskFragment, Task mockParent) { doReturn(mockParent).when(taskFragment).getTask(); - final Configuration taskConfig = new Configuration(); - doReturn(taskConfig).when(mockParent).getConfiguration(); + doReturn(new TaskFragmentParentInfo(new Configuration(), DEFAULT_DISPLAY, true)) + .when(mockParent).getTaskFragmentParentInfo(); // Task needs to be visible mockParent.lastActiveTime = 100; diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index e2dff965ef96..3331839193f8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -34,6 +34,7 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; +import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -1077,6 +1078,39 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testIsBehindStartingWindowChange() { + final Transition transition = createTestTransition(TRANSIT_OPEN); + final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + final ArraySet<WindowContainer> participants = transition.mParticipants; + + final Task task = createTask(mDisplayContent); + final ActivityRecord activity0 = createActivityRecord(task); + final ActivityRecord activity1 = createActivityRecord(task); + doReturn(true).when(activity1).hasStartingWindow(); + + // Start states. + changes.put(activity0, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); + changes.put(activity1, new Transition.ChangeInfo(false /* vis */, false /* exChg */)); + // End states. + activity0.mVisibleRequested = false; + activity1.mVisibleRequested = true; + + participants.add(activity0); + participants.add(activity1); + final ArrayList<WindowContainer> targets = Transition.calculateTargets( + participants, changes); + final TransitionInfo info = Transition.calculateTransitionInfo( + transition.mType, 0 /* flags */, targets, changes, mMockT); + + // All windows in the Task should have FLAG_IS_BEHIND_STARTING_WINDOW because the starting + // window should cover the whole Task. + assertEquals(2, info.getChanges().size()); + assertTrue(info.getChanges().get(0).hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)); + assertTrue(info.getChanges().get(1).hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)); + + } + + @Test public void testFlagInTaskWithEmbeddedActivity() { final Transition transition = createTestTransition(TRANSIT_OPEN); final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; @@ -1123,7 +1157,7 @@ public class TransitionTests extends WindowTestsBase { } @Test - public void testFlagFillsTask() { + public void testFlagFillsTask_embeddingNotFillingTask() { final Transition transition = createTestTransition(TRANSIT_OPEN); final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; final ArraySet<WindowContainer> participants = transition.mParticipants; @@ -1168,6 +1202,67 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testFlagFillsTask_openActivityFillingTask() { + final Transition transition = createTestTransition(TRANSIT_OPEN); + final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + final ArraySet<WindowContainer> participants = transition.mParticipants; + + final Task task = createTask(mDisplayContent); + // Set to multi-windowing mode in order to set bounds. + task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + final Rect taskBounds = new Rect(0, 0, 500, 1000); + task.setBounds(taskBounds); + final ActivityRecord activity = createActivityRecord(task); + // Start states: set bounds to make sure the start bounds is ignored if it is not visible. + activity.getConfiguration().windowConfiguration.setBounds(new Rect(0, 0, 250, 500)); + activity.mVisibleRequested = false; + changes.put(activity, new Transition.ChangeInfo(activity)); + // End states: reset bounds to fill Task. + activity.getConfiguration().windowConfiguration.setBounds(taskBounds); + activity.mVisibleRequested = true; + + participants.add(activity); + final ArrayList<WindowContainer> targets = Transition.calculateTargets( + participants, changes); + final TransitionInfo info = Transition.calculateTransitionInfo( + transition.mType, 0 /* flags */, targets, changes, mMockT); + + // Opening activity that is filling Task after transition should have the flag. + assertEquals(1, info.getChanges().size()); + assertTrue(info.getChanges().get(0).hasFlags(FLAG_FILLS_TASK)); + } + + @Test + public void testFlagFillsTask_closeActivityFillingTask() { + final Transition transition = createTestTransition(TRANSIT_CLOSE); + final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + final ArraySet<WindowContainer> participants = transition.mParticipants; + + final Task task = createTask(mDisplayContent); + // Set to multi-windowing mode in order to set bounds. + task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + final Rect taskBounds = new Rect(0, 0, 500, 1000); + task.setBounds(taskBounds); + final ActivityRecord activity = createActivityRecord(task); + // Start states: fills Task without override. + activity.mVisibleRequested = true; + changes.put(activity, new Transition.ChangeInfo(activity)); + // End states: set bounds to make sure the start bounds is ignored if it is not visible. + activity.getConfiguration().windowConfiguration.setBounds(new Rect(0, 0, 250, 500)); + activity.mVisibleRequested = false; + + participants.add(activity); + final ArrayList<WindowContainer> targets = Transition.calculateTargets( + participants, changes); + final TransitionInfo info = Transition.calculateTransitionInfo( + transition.mType, 0 /* flags */, targets, changes, mMockT); + + // Closing activity that is filling Task before transition should have the flag. + assertEquals(1, info.getChanges().size()); + assertTrue(info.getChanges().get(0).hasFlags(FLAG_FILLS_TASK)); + } + + @Test public void testIncludeEmbeddedActivityReparent() { final Transition transition = createTestTransition(TRANSIT_OPEN); final Task task = createTask(mDisplayContent); 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 8b63904baf31..46b4b76dc12f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -41,7 +41,6 @@ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_ import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; @@ -69,7 +68,6 @@ import android.view.SurfaceControl; import android.view.View; import android.view.WindowManager; import android.window.ClientWindowFrames; -import android.window.ScreenCapture; import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; @@ -425,45 +423,6 @@ public class WindowManagerServiceTests extends WindowTestsBase { LETTERBOX_BACKGROUND_SOLID_COLOR)).isFalse(); } - @Test - public void testCaptureDisplay() { - Rect displayBounds = new Rect(0, 0, 100, 200); - spyOn(mDisplayContent); - when(mDisplayContent.getBounds()).thenReturn(displayBounds); - - // Null captureArgs - ScreenCapture.LayerCaptureArgs resultingArgs = - mWm.getCaptureArgs(DEFAULT_DISPLAY, null /* captureArgs */); - assertEquals(displayBounds, resultingArgs.mSourceCrop); - - // Non null captureArgs, didn't set rect - ScreenCapture.CaptureArgs captureArgs = new ScreenCapture.CaptureArgs.Builder<>().build(); - resultingArgs = mWm.getCaptureArgs(DEFAULT_DISPLAY, captureArgs); - assertEquals(displayBounds, resultingArgs.mSourceCrop); - - // Non null captureArgs, invalid rect - captureArgs = new ScreenCapture.CaptureArgs.Builder<>() - .setSourceCrop(new Rect(0, 0, -1, -1)) - .build(); - resultingArgs = mWm.getCaptureArgs(DEFAULT_DISPLAY, captureArgs); - assertEquals(displayBounds, resultingArgs.mSourceCrop); - - // Non null captureArgs, null rect - captureArgs = new ScreenCapture.CaptureArgs.Builder<>() - .setSourceCrop(null) - .build(); - resultingArgs = mWm.getCaptureArgs(DEFAULT_DISPLAY, captureArgs); - assertEquals(displayBounds, resultingArgs.mSourceCrop); - - // Non null captureArgs, valid rect - Rect validRect = new Rect(0, 0, 10, 50); - captureArgs = new ScreenCapture.CaptureArgs.Builder<>() - .setSourceCrop(validRect) - .build(); - resultingArgs = mWm.getCaptureArgs(DEFAULT_DISPLAY, captureArgs); - assertEquals(validRect, resultingArgs.mSourceCrop); - } - private void setupActivityWithLaunchCookie(IBinder launchCookie, WindowContainerToken wct) { final WindowContainer.RemoteToken remoteToken = mock(WindowContainer.RemoteToken.class); when(remoteToken.toWindowContainerToken()).thenReturn(wct); diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java index 9562c1a5bcf9..e90a376e90f8 100644 --- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java +++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java @@ -514,6 +514,8 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser private boolean mConfigured; private boolean mAudioAccessoryConnected; private boolean mAudioAccessorySupported; + private boolean mConnectedToDataDisabledPort; + private int mPowerBrickConnectionStatus; private UsbAccessory mCurrentAccessory; private int mUsbNotificationId; @@ -952,12 +954,19 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser && status.isRoleCombinationSupported(POWER_ROLE_SOURCE, DATA_ROLE_DEVICE) && status.isRoleCombinationSupported(POWER_ROLE_SINK, DATA_ROLE_DEVICE); + + boolean usbDataDisabled = + status.getUsbDataStatus() != UsbPortStatus.DATA_STATUS_ENABLED; + mConnectedToDataDisabledPort = status.isConnected() && usbDataDisabled; + mPowerBrickConnectionStatus = status.getPowerBrickConnectionStatus(); } else { mHostConnected = false; mSourcePower = false; mSinkPower = false; mAudioAccessoryConnected = false; mSupportsAllCombinations = false; + mConnectedToDataDisabledPort = false; + mPowerBrickConnectionStatus = UsbPortStatus.POWER_BRICK_STATUS_UNKNOWN; } if (mHostConnected) { @@ -1265,6 +1274,12 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser } else if (mHostConnected && mSinkPower && (mUsbCharging || mUsbAccessoryConnected)) { titleRes = com.android.internal.R.string.usb_charging_notification_title; id = SystemMessage.NOTE_USB_CHARGING; + } else if (mSinkPower && mConnectedToDataDisabledPort + && mPowerBrickConnectionStatus != UsbPortStatus.POWER_BRICK_STATUS_CONNECTED) { + // Show charging notification when USB Data is disabled on the port, and not + // connected to a wall charger. + titleRes = com.android.internal.R.string.usb_charging_notification_title; + id = SystemMessage.NOTE_USB_CHARGING; } if (id != mUsbNotificationId || force) { // clear notification if title needs changing diff --git a/telephony/java/android/telephony/AnomalyReporter.java b/telephony/java/android/telephony/AnomalyReporter.java index 58974eec838b..061b71b25275 100644 --- a/telephony/java/android/telephony/AnomalyReporter.java +++ b/telephony/java/android/telephony/AnomalyReporter.java @@ -105,17 +105,23 @@ public final class AnomalyReporter { * @param carrierId the carrier of the id associated with this event. */ public static void reportAnomaly(@NonNull UUID eventId, String description, int carrierId) { - // Don't report if the server-side flag isn't loaded, as it implies other anomaly report - // related config hasn't loaded. - boolean isAnomalyReportEnabledFromServer = DeviceConfig.getBoolean( - DeviceConfig.NAMESPACE_TELEPHONY, KEY_IS_TELEPHONY_ANOMALY_REPORT_ENABLED, false); - if (!isAnomalyReportEnabledFromServer) return; - if (sContext == null) { Rlog.w(TAG, "AnomalyReporter not yet initialized, dropping event=" + eventId); return; } + // Don't report if the server-side flag isn't loaded, as it implies other anomaly report + // related config hasn't loaded. + try { + boolean isAnomalyReportEnabledFromServer = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_TELEPHONY, KEY_IS_TELEPHONY_ANOMALY_REPORT_ENABLED, + false); + if (!isAnomalyReportEnabledFromServer) return; + } catch (Exception e) { + Rlog.w(TAG, "Unable to read device config, dropping event=" + eventId); + return; + } + TelephonyStatsLog.write( TELEPHONY_ANOMALY_DETECTED, carrierId, diff --git a/telephony/java/android/telephony/NetworkService.java b/telephony/java/android/telephony/NetworkService.java index c75de42707a6..ac892da765e7 100644 --- a/telephony/java/android/telephony/NetworkService.java +++ b/telephony/java/android/telephony/NetworkService.java @@ -265,7 +265,7 @@ public abstract class NetworkService extends Service { /** @hide */ @Override public void onDestroy() { - mHandlerThread.quit(); + mHandlerThread.quitSafely(); super.onDestroy(); } diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java index cb985bf2cda5..eb96d3739119 100644 --- a/telephony/java/android/telephony/SubscriptionInfo.java +++ b/telephony/java/android/telephony/SubscriptionInfo.java @@ -18,6 +18,7 @@ package android.telephony; import static android.text.TextUtils.formatSimple; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; @@ -37,6 +38,9 @@ import android.os.Build; import android.os.Parcel; import android.os.ParcelUuid; import android.os.Parcelable; +import android.telephony.SubscriptionManager.ProfileClass; +import android.telephony.SubscriptionManager.SimDisplayNameSource; +import android.telephony.SubscriptionManager.SubscriptionType; import android.telephony.SubscriptionManager.UsageSetting; import android.text.TextUtils; import android.util.DisplayMetrics; @@ -55,7 +59,6 @@ import java.util.Objects; * A Parcelable class for Subscription Information. */ public class SubscriptionInfo implements Parcelable { - /** * Size of text to render on the icon. */ @@ -65,162 +68,180 @@ public class SubscriptionInfo implements Parcelable { * Subscription Identifier, this is a device unique number * and not an index into an array */ - private int mId; + private final int mId; /** - * The GID for a SIM that maybe associated with this subscription, empty if unknown + * The ICCID of the SIM that is associated with this subscription, empty if unknown. */ - private String mIccId; + @NonNull + private final String mIccId; /** - * The index of the slot that currently contains the subscription - * and not necessarily unique and maybe INVALID_SLOT_ID if unknown + * The index of the SIM slot that currently contains the subscription and not necessarily unique + * and maybe {@link SubscriptionManager#INVALID_SIM_SLOT_INDEX} if unknown or the subscription + * is inactive. */ - private int mSimSlotIndex; + private final int mSimSlotIndex; /** - * The name displayed to the user that identifies this subscription + * The name displayed to the user that identifies this subscription. This name is used + * in Settings page and can be renamed by the user. */ - private CharSequence mDisplayName; + @NonNull + private final CharSequence mDisplayName; /** - * String that identifies SPN/PLMN - * TODO : Add a new field that identifies only SPN for a sim + * The name displayed to the user that identifies subscription provider name. This name is the + * SPN displayed in status bar and many other places. Can't be renamed by the user. */ - private CharSequence mCarrierName; + @NonNull + private final CharSequence mCarrierName; /** * The subscription carrier id. + * * @see TelephonyManager#getSimCarrierId() */ - private int mCarrierId; + private final int mCarrierId; /** - * The source of the name, NAME_SOURCE_DEFAULT_SOURCE, NAME_SOURCE_SIM_SPN, - * NAME_SOURCE_SIM_PNN, or NAME_SOURCE_USER_INPUT. + * The source of the {@link #mCarrierName}. */ - private int mNameSource; + @SimDisplayNameSource + private final int mNameSource; /** - * The color to be used for tinting the icon when displaying to the user + * The color to be used for tinting the icon when displaying to the user. */ - private int mIconTint; + private final int mIconTint; /** - * A number presented to the user identify this subscription + * The number presented to the user identify this subscription. */ - private String mNumber; + @NonNull + private final String mNumber; /** - * Data roaming state, DATA_ROAMING_ENABLE, DATA_ROAMING_DISABLE + * Whether user enables data roaming for this subscription or not. Either + * {@link SubscriptionManager#DATA_ROAMING_ENABLE} or + * {@link SubscriptionManager#DATA_ROAMING_DISABLE}. */ - private int mDataRoaming; + private final int mDataRoaming; /** - * SIM icon bitmap cache + * SIM icon bitmap cache. */ - @Nullable private Bitmap mIconBitmap; + @Nullable + private Bitmap mIconBitmap; /** - * Mobile Country Code + * Mobile Country Code. */ - private String mMcc; + @Nullable + private final String mMcc; /** - * Mobile Network Code + * Mobile Network Code. */ - private String mMnc; + @Nullable + private final String mMnc; /** - * EHPLMNs associated with the subscription + * EHPLMNs associated with the subscription. */ - private String[] mEhplmns; + @NonNull + private final String[] mEhplmns; /** - * HPLMNs associated with the subscription + * HPLMNs associated with the subscription. */ - private String[] mHplmns; + @NonNull + private final String[] mHplmns; /** - * ISO Country code for the subscription's provider + * ISO Country code for the subscription's provider. */ - private String mCountryIso; + @NonNull + private final String mCountryIso; /** - * Whether the subscription is an embedded one. + * Whether the subscription is from eSIM. */ - private boolean mIsEmbedded; + private final boolean mIsEmbedded; /** - * The access rules for this subscription, if it is embedded and defines any. - * This does not include access rules for non-embedded subscriptions. + * The access rules for this subscription, if it is embedded and defines any. This does not + * include access rules for non-embedded subscriptions. */ @Nullable - private UiccAccessRule[] mNativeAccessRules; + private final UiccAccessRule[] mNativeAccessRules; /** * The carrier certificates for this subscription that are saved in carrier configs. * This does not include access rules from the Uicc, whether embedded or non-embedded. */ @Nullable - private UiccAccessRule[] mCarrierConfigAccessRules; + private final UiccAccessRule[] mCarrierConfigAccessRules; /** * The string ID of the SIM card. It is the ICCID of the active profile for a UICC card and the * EID for an eUICC card. */ - private String mCardString; + @NonNull + private final String mCardString; /** - * The card ID of the SIM card. This maps uniquely to the card string. + * The card ID of the SIM card. This maps uniquely to {@link #mCardString}. */ - private int mCardId; + private final int mCardId; /** * Whether the subscription is opportunistic. */ - private boolean mIsOpportunistic; + private final boolean mIsOpportunistic; /** - * A UUID assigned to the subscription group. It returns null if not assigned. - * Check {@link SubscriptionManager#createSubscriptionGroup(List)} for more details. + * A UUID assigned to the subscription group. {@code null} if not assigned. + * + * @see SubscriptionManager#createSubscriptionGroup(List) */ @Nullable - private ParcelUuid mGroupUUID; + private final ParcelUuid mGroupUuid; /** - * A package name that specifies who created the group. Null if mGroupUUID is null. + * A package name that specifies who created the group. Empty if not available. */ - private String mGroupOwner; + @NonNull + private final String mGroupOwner; /** - * Whether group of the subscription is disabled. - * This is only useful if it's a grouped opportunistic subscription. In this case, if all - * primary (non-opportunistic) subscriptions in the group are deactivated (unplugged pSIM - * or deactivated eSIM profile), we should disable this opportunistic subscription. + * Whether group of the subscription is disabled. This is only useful if it's a grouped + * opportunistic subscription. In this case, if all primary (non-opportunistic) subscriptions + * in the group are deactivated (unplugged pSIM or deactivated eSIM profile), we should disable + * this opportunistic subscription. */ - private boolean mIsGroupDisabled = false; + private final boolean mIsGroupDisabled; /** - * Profile class, PROFILE_CLASS_TESTING, PROFILE_CLASS_OPERATIONAL - * PROFILE_CLASS_PROVISIONING, or PROFILE_CLASS_UNSET. - * A profile on the eUICC can be defined as test, operational, provisioning, or unset. - * The profile class will be populated from the profile metadata if present. Otherwise, - * the profile class defaults to unset if there is no profile metadata or the subscription - * is not on an eUICC ({@link #isEmbedded} returns false). + * The profile class populated from the profile metadata if present. Otherwise, + * the profile class defaults to {@link SubscriptionManager#PROFILE_CLASS_UNSET} if there is no + * profile metadata or the subscription is not on an eUICC ({@link #isEmbedded} returns + * {@code false}). */ - private int mProfileClass; + @ProfileClass + private final int mProfileClass; /** - * Type of subscription + * Type of the subscription. */ - private int mSubscriptionType; + @SubscriptionType + private final int mType; /** * Whether uicc applications are configured to enable or disable. * By default it's true. */ - private boolean mAreUiccApplicationsEnabled = true; + private final boolean mAreUiccApplicationsEnabled; /** * The port index of the Uicc card. @@ -230,25 +251,16 @@ public class SubscriptionInfo implements Parcelable { /** * Subscription's preferred usage setting. */ - private @UsageSetting int mUsageSetting = SubscriptionManager.USAGE_SETTING_UNKNOWN; - - /** - * Public copy constructor. - * @hide - */ - public SubscriptionInfo(SubscriptionInfo info) { - this(info.mId, info.mIccId, info.mSimSlotIndex, info.mDisplayName, info.mCarrierName, - info.mNameSource, info.mIconTint, info.mNumber, info.mDataRoaming, info.mIconBitmap, - info.mMcc, info.mMnc, info.mCountryIso, info.mIsEmbedded, info.mNativeAccessRules, - info.mCardString, info.mCardId, info.mIsOpportunistic, - info.mGroupUUID == null ? null : info.mGroupUUID.toString(), info.mIsGroupDisabled, - info.mCarrierId, info.mProfileClass, info.mSubscriptionType, info.mGroupOwner, - info.mCarrierConfigAccessRules, info.mAreUiccApplicationsEnabled); - } + @UsageSetting + private final int mUsageSetting; /** * @hide + * + * @deprecated Use {@link SubscriptionInfo.Builder}. */ + // TODO: Clean up after external usages moved to builder model. + @Deprecated public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName, CharSequence carrierName, int nameSource, int iconTint, String number, int roaming, Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded, @@ -262,7 +274,11 @@ public class SubscriptionInfo implements Parcelable { /** * @hide + * + * @deprecated Use {@link SubscriptionInfo.Builder}. */ + // TODO: Clean up after external usages moved to builder model. + @Deprecated public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName, CharSequence carrierName, int nameSource, int iconTint, String number, int roaming, Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded, @@ -276,7 +292,11 @@ public class SubscriptionInfo implements Parcelable { /** * @hide + * + * @deprecated Use {@link SubscriptionInfo.Builder}. */ + // TODO: Clean up after external usages moved to builder model. + @Deprecated public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName, CharSequence carrierName, int nameSource, int iconTint, String number, int roaming, Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded, @@ -293,7 +313,11 @@ public class SubscriptionInfo implements Parcelable { /** * @hide + * + * @deprecated Use {@link SubscriptionInfo.Builder}. */ + // TODO: Clean up after external usages moved to builder model. + @Deprecated public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName, CharSequence carrierName, int nameSource, int iconTint, String number, int roaming, Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded, @@ -311,49 +335,94 @@ public class SubscriptionInfo implements Parcelable { /** * @hide + * + * @deprecated Use {@link SubscriptionInfo.Builder}. */ + // TODO: Clean up after external usages moved to builder model. + @Deprecated public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName, CharSequence carrierName, int nameSource, int iconTint, String number, int roaming, Bitmap icon, String mcc, String mnc, String countryIso, boolean isEmbedded, @Nullable UiccAccessRule[] nativeAccessRules, String cardString, int cardId, - boolean isOpportunistic, @Nullable String groupUUID, boolean isGroupDisabled, + boolean isOpportunistic, @Nullable String groupUuid, boolean isGroupDisabled, int carrierId, int profileClass, int subType, @Nullable String groupOwner, @Nullable UiccAccessRule[] carrierConfigAccessRules, boolean areUiccApplicationsEnabled, int portIndex, @UsageSetting int usageSetting) { this.mId = id; this.mIccId = iccId; this.mSimSlotIndex = simSlotIndex; - this.mDisplayName = displayName; + this.mDisplayName = displayName; this.mCarrierName = carrierName; this.mNameSource = nameSource; this.mIconTint = iconTint; this.mNumber = number; this.mDataRoaming = roaming; this.mIconBitmap = icon; - this.mMcc = mcc; - this.mMnc = mnc; - this.mCountryIso = countryIso; + this.mMcc = TextUtils.emptyIfNull(mcc); + this.mMnc = TextUtils.emptyIfNull(mnc); + this.mHplmns = null; + this.mEhplmns = null; + this.mCountryIso = TextUtils.emptyIfNull(countryIso); this.mIsEmbedded = isEmbedded; this.mNativeAccessRules = nativeAccessRules; - this.mCardString = cardString; + this.mCardString = TextUtils.emptyIfNull(cardString); this.mCardId = cardId; this.mIsOpportunistic = isOpportunistic; - this.mGroupUUID = groupUUID == null ? null : ParcelUuid.fromString(groupUUID); + this.mGroupUuid = groupUuid == null ? null : ParcelUuid.fromString(groupUuid); this.mIsGroupDisabled = isGroupDisabled; this.mCarrierId = carrierId; this.mProfileClass = profileClass; - this.mSubscriptionType = subType; - this.mGroupOwner = groupOwner; + this.mType = subType; + this.mGroupOwner = TextUtils.emptyIfNull(groupOwner); this.mCarrierConfigAccessRules = carrierConfigAccessRules; this.mAreUiccApplicationsEnabled = areUiccApplicationsEnabled; this.mPortIndex = portIndex; this.mUsageSetting = usageSetting; } + /** - * @return the subscription ID. + * Constructor from builder. + * + * @param builder Builder of {@link SubscriptionInfo}. + */ + private SubscriptionInfo(@NonNull Builder builder) { + this.mId = builder.mId; + this.mIccId = builder.mIccId; + this.mSimSlotIndex = builder.mSimSlotIndex; + this.mDisplayName = builder.mDisplayName; + this.mCarrierName = builder.mCarrierName; + this.mNameSource = builder.mNameSource; + this.mIconTint = builder.mIconTint; + this.mNumber = builder.mNumber; + this.mDataRoaming = builder.mDataRoaming; + this.mIconBitmap = builder.mIconBitmap; + this.mMcc = builder.mMcc; + this.mMnc = builder.mMnc; + this.mEhplmns = builder.mEhplmns; + this.mHplmns = builder.mHplmns; + this.mCountryIso = builder.mCountryIso; + this.mIsEmbedded = builder.mIsEmbedded; + this.mNativeAccessRules = builder.mNativeAccessRules; + this.mCardString = builder.mCardString; + this.mCardId = builder.mCardId; + this.mIsOpportunistic = builder.mIsOpportunistic; + this.mGroupUuid = builder.mGroupUuid; + this.mIsGroupDisabled = builder.mIsGroupDisabled; + this.mCarrierId = builder.mCarrierId; + this.mProfileClass = builder.mProfileClass; + this.mType = builder.mType; + this.mGroupOwner = builder.mGroupOwner; + this.mCarrierConfigAccessRules = builder.mCarrierConfigAccessRules; + this.mAreUiccApplicationsEnabled = builder.mAreUiccApplicationsEnabled; + this.mPortIndex = builder.mPortIndex; + this.mUsageSetting = builder.mUsageSetting; + } + + /** + * @return The subscription ID. */ public int getSubscriptionId() { - return this.mId; + return mId; } /** @@ -370,78 +439,56 @@ public class SubscriptionInfo implements Parcelable { * @return the ICC ID, or an empty string if one of these requirements is not met */ public String getIccId() { - return this.mIccId; + return mIccId; } /** - * @hide - */ - public void clearIccId() { - this.mIccId = ""; - } - - /** - * @return the slot index of this Subscription's SIM card. + * @return The index of the SIM slot that currently contains the subscription and not + * necessarily unique and maybe {@link SubscriptionManager#INVALID_SIM_SLOT_INDEX} if unknown or + * the subscription is inactive. */ public int getSimSlotIndex() { - return this.mSimSlotIndex; + return mSimSlotIndex; } /** - * @return the carrier id of this Subscription carrier. + * @return The carrier id of this subscription carrier. + * * @see TelephonyManager#getSimCarrierId() */ public int getCarrierId() { - return this.mCarrierId; + return mCarrierId; } /** - * @return the name displayed to the user that identifies this subscription + * @return The name displayed to the user that identifies this subscription. This name is + * used in Settings page and can be renamed by the user. + * + * @see #getCarrierName() */ public CharSequence getDisplayName() { - return this.mDisplayName; + return mDisplayName; } /** - * Sets the name displayed to the user that identifies this subscription - * @hide - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public void setDisplayName(CharSequence name) { - this.mDisplayName = name; - } - - /** - * @return the name displayed to the user that identifies Subscription provider name + * @return The name displayed to the user that identifies subscription provider name. This name + * is the SPN displayed in status bar and many other places. Can't be renamed by the user. + * + * @see #getDisplayName() */ public CharSequence getCarrierName() { - return this.mCarrierName; - } - - /** - * Sets the name displayed to the user that identifies Subscription provider name - * @hide - */ - public void setCarrierName(CharSequence name) { - this.mCarrierName = name; + return mCarrierName; } /** - * @return the source of the name, eg NAME_SOURCE_DEFAULT_SOURCE, NAME_SOURCE_SIM_SPN or - * NAME_SOURCE_USER_INPUT. + * @return The source of the {@link #getCarrierName()}. + * * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @SimDisplayNameSource public int getNameSource() { - return this.mNameSource; - } - - /** - * @hide - */ - public void setAssociatedPlmns(String[] ehplmns, String[] hplmns) { - mEhplmns = ehplmns; - mHplmns = hplmns; + return mNameSource; } /** @@ -499,15 +546,6 @@ public class SubscriptionInfo implements Parcelable { } /** - * Sets the color displayed to the user that identifies this subscription - * @hide - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public void setIconTint(int iconTint) { - this.mIconTint = iconTint; - } - - /** * Returns the number of this subscription. * * Starting with API level 30, returns the number of this subscription if the calling app meets @@ -533,28 +571,23 @@ public class SubscriptionInfo implements Parcelable { } /** - * @hide - */ - public void clearNumber() { - mNumber = ""; - } - - /** - * @return the data roaming state for this subscription, either - * {@link SubscriptionManager#DATA_ROAMING_ENABLE} or {@link SubscriptionManager#DATA_ROAMING_DISABLE}. + * Whether user enables data roaming for this subscription or not. Either + * {@link SubscriptionManager#DATA_ROAMING_ENABLE} or + * {@link SubscriptionManager#DATA_ROAMING_DISABLE}. */ public int getDataRoaming() { - return this.mDataRoaming; + return mDataRoaming; } /** - * @return the MCC. + * @return The mobile country code. + * * @deprecated Use {@link #getMccString()} instead. */ @Deprecated public int getMcc() { try { - return this.mMcc == null ? 0 : Integer.valueOf(this.mMcc); + return mMcc == null ? 0 : Integer.parseInt(mMcc); } catch (NumberFormatException e) { Log.w(SubscriptionInfo.class.getSimpleName(), "MCC string is not a number"); return 0; @@ -562,13 +595,14 @@ public class SubscriptionInfo implements Parcelable { } /** - * @return the MNC. + * @return The mobile network code. + * * @deprecated Use {@link #getMncString()} instead. */ @Deprecated public int getMnc() { try { - return this.mMnc == null ? 0 : Integer.valueOf(this.mMnc); + return mMnc == null ? 0 : Integer.parseInt(mMnc); } catch (NumberFormatException e) { Log.w(SubscriptionInfo.class.getSimpleName(), "MNC string is not a number"); return 0; @@ -576,36 +610,40 @@ public class SubscriptionInfo implements Parcelable { } /** - * @return The MCC, as a string. + * @return The mobile country code. */ - public @Nullable String getMccString() { - return this.mMcc; + @Nullable + public String getMccString() { + return mMcc; } /** - * @return The MNC, as a string. + * @return The mobile network code. */ - public @Nullable String getMncString() { - return this.mMnc; + @Nullable + public String getMncString() { + return mMnc; } /** - * @return the ISO country code + * @return The ISO country code. Empty if not available. */ public String getCountryIso() { - return this.mCountryIso; + return mCountryIso; } - /** @return whether the subscription is an eUICC one. */ + /** + * @return {@code true} if the subscription is from eSIM. + */ public boolean isEmbedded() { - return this.mIsEmbedded; + return mIsEmbedded; } /** * An opportunistic subscription connects to a network that is * limited in functionality and / or coverage. * - * @return whether subscription is opportunistic. + * @return Whether subscription is opportunistic. */ public boolean isOpportunistic() { return mIsOpportunistic; @@ -617,23 +655,18 @@ public class SubscriptionInfo implements Parcelable { * Such that those subscriptions will have some affiliated behaviors such as opportunistic * subscription may be invisible to the user. * - * @return group UUID a String of group UUID if it belongs to a group. Otherwise - * it will return null. + * @return Group UUID a String of group UUID if it belongs to a group. Otherwise + * {@code null}. */ - public @Nullable ParcelUuid getGroupUuid() { - return mGroupUUID; - } - - /** - * @hide - */ - public void clearGroupUuid() { - this.mGroupUUID = null; + @Nullable + public ParcelUuid getGroupUuid() { + return mGroupUuid; } /** * @hide */ + @NonNull public List<String> getEhplmns() { return mEhplmns == null ? Collections.emptyList() : Arrays.asList(mEhplmns); } @@ -641,36 +674,45 @@ public class SubscriptionInfo implements Parcelable { /** * @hide */ + @NonNull public List<String> getHplmns() { return mHplmns == null ? Collections.emptyList() : Arrays.asList(mHplmns); } /** - * Return owner package of group the subscription belongs to. + * @return The owner package of group the subscription belongs to. * * @hide */ - public @Nullable String getGroupOwner() { + @NonNull + public String getGroupOwner() { return mGroupOwner; } /** - * @return the profile class of this subscription. + * @return The profile class populated from the profile metadata if present. Otherwise, + * the profile class defaults to {@link SubscriptionManager#PROFILE_CLASS_UNSET} if there is no + * profile metadata or the subscription is not on an eUICC ({@link #isEmbedded} return + * {@code false}). + * * @hide */ @SystemApi - public @SubscriptionManager.ProfileClass int getProfileClass() { - return this.mProfileClass; + @ProfileClass + public int getProfileClass() { + return mProfileClass; } /** * This method returns the type of a subscription. It can be * {@link SubscriptionManager#SUBSCRIPTION_TYPE_LOCAL_SIM} or * {@link SubscriptionManager#SUBSCRIPTION_TYPE_REMOTE_SIM}. - * @return the type of subscription + * + * @return The type of the subscription. */ - public @SubscriptionManager.SubscriptionType int getSubscriptionType() { - return mSubscriptionType; + @SubscriptionType + public int getSubscriptionType() { + return mType; } /** @@ -679,7 +721,7 @@ public class SubscriptionInfo implements Parcelable { * returns true). * * @param context Context of the application to check. - * @return whether the app is authorized to manage this subscription per its metadata. + * @return Whether the app is authorized to manage this subscription per its metadata. * @hide * @deprecated - Do not use. */ @@ -700,7 +742,7 @@ public class SubscriptionInfo implements Parcelable { */ @Deprecated public boolean canManageSubscription(Context context, String packageName) { - List<UiccAccessRule> allAccessRules = getAllAccessRules(); + List<UiccAccessRule> allAccessRules = getAccessRules(); if (allAccessRules == null) { return false; } @@ -723,27 +765,17 @@ public class SubscriptionInfo implements Parcelable { } /** - * @return the {@link UiccAccessRule}s that are stored in Uicc, dictating who - * is authorized to manage this subscription. - * TODO and fix it properly in R / master: either deprecate this and have 3 APIs - * native + carrier + all, or have this return all by default. + * @return The {@link UiccAccessRule}s that are stored in Uicc, dictating who is authorized to + * manage this subscription. + * * @hide */ @SystemApi - public @Nullable List<UiccAccessRule> getAccessRules() { - if (mNativeAccessRules == null) return null; - return Arrays.asList(mNativeAccessRules); - } - - /** - * @return the {@link UiccAccessRule}s that are both stored on Uicc and in carrierConfigs - * dictating who is authorized to manage this subscription. - * @hide - */ - public @Nullable List<UiccAccessRule> getAllAccessRules() { + @Nullable + public List<UiccAccessRule> getAccessRules() { List<UiccAccessRule> merged = new ArrayList<>(); if (mNativeAccessRules != null) { - merged.addAll(getAccessRules()); + merged.addAll(Arrays.asList(mNativeAccessRules)); } if (mCarrierConfigAccessRules != null) { merged.addAll(Arrays.asList(mCarrierConfigAccessRules)); @@ -762,50 +794,38 @@ public class SubscriptionInfo implements Parcelable { * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile * owner access is deprecated and will be removed in a future release. * - * @return the card string of the SIM card which contains the subscription or an empty string + * @return The card string of the SIM card which contains the subscription or an empty string * if these requirements are not met. The card string is the ICCID for UICCs or the EID for * eUICCs. + * * @hide - * //TODO rename usages in LPA: UiccSlotUtil.java, UiccSlotsManager.java, UiccSlotInfoTest.java */ + @NonNull public String getCardString() { - return this.mCardString; + return mCardString; } /** - * @hide - */ - public void clearCardString() { - this.mCardString = ""; - } - - /** - * Returns the card ID of the SIM card which contains the subscription (see - * {@link UiccCardInfo#getCardId()}. - * @return the cardId + * @return The card ID of the SIM card which contains the subscription. + * + * @see UiccCardInfo#getCardId(). */ public int getCardId() { - return this.mCardId; + return mCardId; } /** - * Returns the port index of the SIM card which contains the subscription. - * - * @return the portIndex + * @return The port index of the SIM card which contains the subscription. */ public int getPortIndex() { - return this.mPortIndex; - } - - /** - * Set whether the subscription's group is disabled. - * @hide - */ - public void setGroupDisabled(boolean isGroupDisabled) { - this.mIsGroupDisabled = isGroupDisabled; + return mPortIndex; } /** - * Return whether the subscription's group is disabled. + * @return {@code true} if the group of the subscription is disabled. This is only useful if + * it's a grouped opportunistic subscription. In this case, if all primary (non-opportunistic) + * subscriptions in the group are deactivated (unplugged pSIM or deactivated eSIM profile), we + * should disable this opportunistic subscription. + * * @hide */ @SystemApi @@ -814,7 +834,7 @@ public class SubscriptionInfo implements Parcelable { } /** - * Return whether uicc applications are set to be enabled or disabled. + * @return {@code true} if Uicc applications are set to be enabled or disabled. * @hide */ @SystemApi @@ -825,56 +845,50 @@ public class SubscriptionInfo implements Parcelable { /** * Get the usage setting for this subscription. * - * @return the usage setting used for this subscription. + * @return The usage setting used for this subscription. */ - public @UsageSetting int getUsageSetting() { + @UsageSetting + public int getUsageSetting() { return mUsageSetting; } - public static final @android.annotation.NonNull - Parcelable.Creator<SubscriptionInfo> CREATOR = - new Parcelable.Creator<SubscriptionInfo>() { + @NonNull + public static final Parcelable.Creator<SubscriptionInfo> CREATOR = + new Parcelable.Creator<SubscriptionInfo>() { @Override public SubscriptionInfo createFromParcel(Parcel source) { - int id = source.readInt(); - String iccId = source.readString(); - int simSlotIndex = source.readInt(); - CharSequence displayName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); - CharSequence carrierName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); - int nameSource = source.readInt(); - int iconTint = source.readInt(); - String number = source.readString(); - int dataRoaming = source.readInt(); - String mcc = source.readString(); - String mnc = source.readString(); - String countryIso = source.readString(); - boolean isEmbedded = source.readBoolean(); - UiccAccessRule[] nativeAccessRules = source.createTypedArray(UiccAccessRule.CREATOR); - String cardString = source.readString(); - int cardId = source.readInt(); - int portId = source.readInt(); - boolean isOpportunistic = source.readBoolean(); - String groupUUID = source.readString(); - boolean isGroupDisabled = source.readBoolean(); - int carrierid = source.readInt(); - int profileClass = source.readInt(); - int subType = source.readInt(); - String[] ehplmns = source.createStringArray(); - String[] hplmns = source.createStringArray(); - String groupOwner = source.readString(); - UiccAccessRule[] carrierConfigAccessRules = source.createTypedArray( - UiccAccessRule.CREATOR); - boolean areUiccApplicationsEnabled = source.readBoolean(); - int usageSetting = source.readInt(); - - SubscriptionInfo info = new SubscriptionInfo(id, iccId, simSlotIndex, displayName, - carrierName, nameSource, iconTint, number, dataRoaming, /* icon= */ null, - mcc, mnc, countryIso, isEmbedded, nativeAccessRules, cardString, cardId, - isOpportunistic, groupUUID, isGroupDisabled, carrierid, profileClass, subType, - groupOwner, carrierConfigAccessRules, areUiccApplicationsEnabled, - portId, usageSetting); - info.setAssociatedPlmns(ehplmns, hplmns); - return info; + return new Builder() + .setId(source.readInt()) + .setIccId(source.readString()) + .setSimSlotIndex(source.readInt()) + .setDisplayName(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source)) + .setCarrierName(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source)) + .setNameSource(source.readInt()) + .setIconTint(source.readInt()) + .setNumber(source.readString()) + .setDataRoaming(source.readInt()) + .setMcc(source.readString()) + .setMnc(source.readString()) + .setCountryIso(source.readString()) + .setEmbedded(source.readBoolean()) + .setNativeAccessRules(source.createTypedArray(UiccAccessRule.CREATOR)) + .setCardString(source.readString()) + .setCardId(source.readInt()) + .setPortIndex(source.readInt()) + .setOpportunistic(source.readBoolean()) + .setGroupUuid(source.readString8()) + .setGroupDisabled(source.readBoolean()) + .setCarrierId(source.readInt()) + .setProfileClass(source.readInt()) + .setType(source.readInt()) + .setEhplmns(source.createStringArray()) + .setHplmns(source.createStringArray()) + .setGroupOwner(source.readString()) + .setCarrierConfigAccessRules(source.createTypedArray( + UiccAccessRule.CREATOR)) + .setUiccApplicationsEnabled(source.readBoolean()) + .setUsageSetting(source.readInt()) + .build(); } @Override @@ -904,11 +918,11 @@ public class SubscriptionInfo implements Parcelable { dest.writeInt(mCardId); dest.writeInt(mPortIndex); dest.writeBoolean(mIsOpportunistic); - dest.writeString(mGroupUUID == null ? null : mGroupUUID.toString()); + dest.writeString8(mGroupUuid == null ? null : mGroupUuid.toString()); dest.writeBoolean(mIsGroupDisabled); dest.writeInt(mCarrierId); dest.writeInt(mProfileClass); - dest.writeInt(mSubscriptionType); + dest.writeInt(mType); dest.writeStringArray(mEhplmns); dest.writeStringArray(mHplmns); dest.writeString(mGroupOwner); @@ -923,6 +937,11 @@ public class SubscriptionInfo implements Parcelable { } /** + * Get ICCID stripped PII information on user build. + * + * @param iccId The original ICCID. + * @return The stripped string. + * * @hide */ public static String givePrintableIccid(String iccId) { @@ -951,12 +970,12 @@ public class SubscriptionInfo implements Parcelable { + " nativeAccessRules=" + Arrays.toString(mNativeAccessRules) + " cardString=" + cardStringToPrint + " cardId=" + mCardId + " portIndex=" + mPortIndex - + " isOpportunistic=" + mIsOpportunistic + " groupUUID=" + mGroupUUID + + " isOpportunistic=" + mIsOpportunistic + " groupUuid=" + mGroupUuid + " isGroupDisabled=" + mIsGroupDisabled + " profileClass=" + mProfileClass + " ehplmns=" + Arrays.toString(mEhplmns) + " hplmns=" + Arrays.toString(mHplmns) - + " subscriptionType=" + mSubscriptionType + + " mType=" + mType + " groupOwner=" + mGroupOwner + " carrierConfigAccessRules=" + Arrays.toString(mCarrierConfigAccessRules) + " areUiccApplicationsEnabled=" + mAreUiccApplicationsEnabled @@ -966,7 +985,7 @@ public class SubscriptionInfo implements Parcelable { @Override public int hashCode() { return Objects.hash(mId, mSimSlotIndex, mNameSource, mIconTint, mDataRoaming, mIsEmbedded, - mIsOpportunistic, mGroupUUID, mIccId, mNumber, mMcc, mMnc, mCountryIso, mCardString, + mIsOpportunistic, mGroupUuid, mIccId, mNumber, mMcc, mMnc, mCountryIso, mCardString, mCardId, mDisplayName, mCarrierName, Arrays.hashCode(mNativeAccessRules), mIsGroupDisabled, mCarrierId, mProfileClass, mGroupOwner, mAreUiccApplicationsEnabled, mPortIndex, mUsageSetting); @@ -974,16 +993,9 @@ public class SubscriptionInfo implements Parcelable { @Override public boolean equals(Object obj) { - if (obj == null) return false; - if (obj == this) return true; - - SubscriptionInfo toCompare; - try { - toCompare = (SubscriptionInfo) obj; - } catch (ClassCastException ex) { - return false; - } - + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + SubscriptionInfo toCompare = (SubscriptionInfo) obj; return mId == toCompare.mId && mSimSlotIndex == toCompare.mSimSlotIndex && mNameSource == toCompare.mNameSource @@ -994,7 +1006,7 @@ public class SubscriptionInfo implements Parcelable { && mIsGroupDisabled == toCompare.mIsGroupDisabled && mAreUiccApplicationsEnabled == toCompare.mAreUiccApplicationsEnabled && mCarrierId == toCompare.mCarrierId - && Objects.equals(mGroupUUID, toCompare.mGroupUUID) + && Objects.equals(mGroupUuid, toCompare.mGroupUuid) && Objects.equals(mIccId, toCompare.mIccId) && Objects.equals(mNumber, toCompare.mNumber) && Objects.equals(mMcc, toCompare.mMcc) @@ -1012,4 +1024,629 @@ public class SubscriptionInfo implements Parcelable { && Arrays.equals(mHplmns, toCompare.mHplmns) && mUsageSetting == toCompare.mUsageSetting; } + + /** + * The builder class of {@link SubscriptionInfo}. + * + * @hide + */ + public static class Builder { + /** + * The subscription id. + */ + private int mId = 0; + + /** + * The ICCID of the SIM that is associated with this subscription, empty if unknown. + */ + @NonNull + private String mIccId = ""; + + /** + * The index of the SIM slot that currently contains the subscription and not necessarily + * unique and maybe {@link SubscriptionManager#INVALID_SIM_SLOT_INDEX} if unknown or the + * subscription is inactive. + */ + private int mSimSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX; + + /** + * The name displayed to the user that identifies this subscription. This name is used + * in Settings page and can be renamed by the user. + */ + @NonNull + private CharSequence mDisplayName = ""; + + /** + * The name displayed to the user that identifies subscription provider name. This name + * is the SPN displayed in status bar and many other places. Can't be renamed by the user. + */ + @NonNull + private CharSequence mCarrierName = ""; + + /** + * The source of the carrier name. + */ + @SimDisplayNameSource + private int mNameSource = SubscriptionManager.NAME_SOURCE_CARRIER_ID; + + /** + * The color to be used for tinting the icon when displaying to the user. + */ + private int mIconTint = 0; + + /** + * The number presented to the user identify this subscription. + */ + @NonNull + private String mNumber = ""; + + /** + * Whether user enables data roaming for this subscription or not. Either + * {@link SubscriptionManager#DATA_ROAMING_ENABLE} or + * {@link SubscriptionManager#DATA_ROAMING_DISABLE}. + */ + private int mDataRoaming = SubscriptionManager.DATA_ROAMING_DISABLE; + + /** + * SIM icon bitmap cache. + */ + @Nullable + private Bitmap mIconBitmap = null; + + /** + * The mobile country code. + */ + @Nullable + private String mMcc = null; + + /** + * The mobile network code. + */ + @Nullable + private String mMnc = null; + + /** + * EHPLMNs associated with the subscription. + */ + @NonNull + private String[] mEhplmns = new String[0]; + + /** + * HPLMNs associated with the subscription. + */ + @NonNull + private String[] mHplmns = new String[0]; + + /** + * The ISO Country code for the subscription's provider. + */ + @NonNull + private String mCountryIso = ""; + + /** + * Whether the subscription is from eSIM. + */ + private boolean mIsEmbedded = false; + + /** + * The native access rules for this subscription, if it is embedded and defines any. This + * does not include access rules for non-embedded subscriptions. + */ + @Nullable + private UiccAccessRule[] mNativeAccessRules = null; + + /** + * The card string of the SIM card. + */ + @NonNull + private String mCardString = ""; + + /** + * The card ID of the SIM card which contains the subscription. + */ + private int mCardId = -1; + + /** + * Whether the subscription is opportunistic or not. + */ + private boolean mIsOpportunistic = false; + + /** + * The group UUID of the subscription group. + */ + @Nullable + private ParcelUuid mGroupUuid = null; + + /** + * Whether group of the subscription is disabled. This is only useful if it's a grouped + * opportunistic subscription. In this case, if all primary (non-opportunistic) + * subscriptions in the group are deactivated (unplugged pSIM or deactivated eSIM profile), + * we should disable this opportunistic subscription. + */ + private boolean mIsGroupDisabled = false; + + /** + * The carrier id. + * + * @see TelephonyManager#getSimCarrierId() + */ + private int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID; + + /** + * The profile class populated from the profile metadata if present. Otherwise, the profile + * class defaults to {@link SubscriptionManager#PROFILE_CLASS_UNSET} if there is no profile + * metadata or the subscription is not on an eUICC ({@link #isEmbedded} returns + * {@code false}). + */ + @ProfileClass + private int mProfileClass = SubscriptionManager.PROFILE_CLASS_UNSET; + + /** + * The subscription type. + */ + @SubscriptionType + private int mType = SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM; + + /** + * The owner package of group the subscription belongs to. + */ + @NonNull + private String mGroupOwner = ""; + + /** + * The carrier certificates for this subscription that are saved in carrier configs. + * This does not include access rules from the Uicc, whether embedded or non-embedded. + */ + @Nullable + private UiccAccessRule[] mCarrierConfigAccessRules = null; + + /** + * Whether Uicc applications are configured to enable or not. + */ + private boolean mAreUiccApplicationsEnabled = true; + + /** + * the port index of the Uicc card. + */ + private int mPortIndex = 0; + + /** + * Subscription's preferred usage setting. + */ + @UsageSetting + private int mUsageSetting = SubscriptionManager.USAGE_SETTING_UNKNOWN; + + /** + * Default constructor. + */ + public Builder() { + } + + /** + * Constructor from {@link SubscriptionInfo}. + * + * @param info The subscription info. + */ + public Builder(@NonNull SubscriptionInfo info) { + mId = info.mId; + mIccId = info.mIccId; + mSimSlotIndex = info.mSimSlotIndex; + mDisplayName = info.mDisplayName; + mCarrierName = info.mCarrierName; + mNameSource = info.mNameSource; + mIconTint = info.mIconTint; + mNumber = info.mNumber; + mDataRoaming = info.mDataRoaming; + mIconBitmap = info.mIconBitmap; + mMcc = info.mMcc; + mMnc = info.mMnc; + mEhplmns = info.mEhplmns; + mHplmns = info.mHplmns; + mCountryIso = info.mCountryIso; + mIsEmbedded = info.mIsEmbedded; + mNativeAccessRules = info.mNativeAccessRules; + mCardString = info.mCardString; + mCardId = info.mCardId; + mIsOpportunistic = info.mIsOpportunistic; + mGroupUuid = info.mGroupUuid; + mIsGroupDisabled = info.mIsGroupDisabled; + mCarrierId = info.mCarrierId; + mProfileClass = info.mProfileClass; + mType = info.mType; + mGroupOwner = info.mGroupOwner; + mCarrierConfigAccessRules = info.mCarrierConfigAccessRules; + mAreUiccApplicationsEnabled = info.mAreUiccApplicationsEnabled; + mPortIndex = info.mPortIndex; + mUsageSetting = info.mUsageSetting; + } + + /** + * Set the subscription id. + * + * @param id The subscription id. + * @return The builder. + */ + @NonNull + public Builder setId(int id) { + mId = id; + return this; + } + + /** + * Set the ICCID of the SIM that is associated with this subscription. + * + * @param iccId The ICCID of the SIM that is associated with this subscription. + * @return The builder. + */ + @NonNull + public Builder setIccId(@Nullable String iccId) { + mIccId = TextUtils.emptyIfNull(iccId); + return this; + } + + /** + * Set the SIM index of the slot that currently contains the subscription. Set to + * {@link SubscriptionManager#INVALID_SIM_SLOT_INDEX} if the subscription is inactive. + * + * @param simSlotIndex The SIM slot index. + * @return The builder. + */ + @NonNull + public Builder setSimSlotIndex(int simSlotIndex) { + mSimSlotIndex = simSlotIndex; + return this; + } + + /** + * The name displayed to the user that identifies this subscription. This name is used + * in Settings page and can be renamed by the user. + * + * @param displayName The display name. + * @return The builder. + */ + @NonNull + public Builder setDisplayName(@Nullable CharSequence displayName) { + mDisplayName = displayName == null ? "" : displayName; + return this; + } + + /** + * The name displayed to the user that identifies subscription provider name. This name + * is the SPN displayed in status bar and many other places. Can't be renamed by the user. + * + * @param carrierName The carrier name. + * @return The builder. + */ + @NonNull + public Builder setCarrierName(@Nullable CharSequence carrierName) { + mCarrierName = carrierName == null ? "" : carrierName; + return this; + } + + /** + * Set the source of the carrier name. + * + * @param nameSource The source of the carrier name. + * @return The builder. + */ + @NonNull + public Builder setNameSource(@SimDisplayNameSource int nameSource) { + mNameSource = nameSource; + return this; + } + + /** + * Set the color to be used for tinting the icon when displaying to the user. + * + * @param iconTint The color to be used for tinting the icon when displaying to the user. + * @return The builder. + */ + @NonNull + public Builder setIconTint(int iconTint) { + mIconTint = iconTint; + return this; + } + + /** + * Set the number presented to the user identify this subscription. + * + * @param number the number presented to the user identify this subscription. + * @return The builder. + */ + @NonNull + public Builder setNumber(@Nullable String number) { + mNumber = TextUtils.emptyIfNull(number); + return this; + } + + /** + * Set whether user enables data roaming for this subscription or not. + * + * @param dataRoaming Data roaming mode. Either + * {@link SubscriptionManager#DATA_ROAMING_ENABLE} or + * {@link SubscriptionManager#DATA_ROAMING_DISABLE} + * @return The builder. + */ + @NonNull + public Builder setDataRoaming(int dataRoaming) { + mDataRoaming = dataRoaming; + return this; + } + + /** + * Set SIM icon bitmap cache. + * + * @param iconBitmap SIM icon bitmap cache. + * @return The builder. + */ + @NonNull + public Builder setIcon(@Nullable Bitmap iconBitmap) { + mIconBitmap = iconBitmap; + return this; + } + + /** + * Set the mobile country code. + * + * @param mcc The mobile country code. + * @return The builder. + */ + @NonNull + public Builder setMcc(@Nullable String mcc) { + mMcc = mcc; + return this; + } + + /** + * Set the mobile network code. + * + * @param mnc Mobile network code. + * @return The builder. + */ + @NonNull + public Builder setMnc(@Nullable String mnc) { + mMnc = mnc; + return this; + } + + /** + * Set EHPLMNs associated with the subscription. + * + * @param ehplmns EHPLMNs associated with the subscription. + * @return The builder. + */ + @NonNull + public Builder setEhplmns(@Nullable String[] ehplmns) { + mEhplmns = ehplmns == null ? new String[0] : ehplmns; + return this; + } + + /** + * Set HPLMNs associated with the subscription. + * + * @param hplmns HPLMNs associated with the subscription. + * @return The builder. + */ + @NonNull + public Builder setHplmns(@Nullable String[] hplmns) { + mHplmns = hplmns == null ? new String[0] : hplmns; + return this; + } + + /** + * Set the ISO Country code for the subscription's provider. + * + * @param countryIso The ISO Country code for the subscription's provider. + * @return The builder. + */ + @NonNull + public Builder setCountryIso(@Nullable String countryIso) { + mCountryIso = TextUtils.emptyIfNull(countryIso); + return this; + } + + /** + * Set whether the subscription is from eSIM or not. + * + * @param isEmbedded {@code true} if the subscription is from eSIM. + * @return The builder. + */ + @NonNull + public Builder setEmbedded(boolean isEmbedded) { + mIsEmbedded = isEmbedded; + return this; + } + + /** + * Set the native access rules for this subscription, if it is embedded and defines any. + * This does not include access rules for non-embedded subscriptions. + * + * @param nativeAccessRules The native access rules for this subscription. + * @return The builder. + */ + @NonNull + public Builder setNativeAccessRules(@Nullable UiccAccessRule[] nativeAccessRules) { + mNativeAccessRules = nativeAccessRules; + return this; + } + + /** + * Set the card string of the SIM card. + * + * @param cardString The card string of the SIM card. + * @return The builder. + * + * @see #getCardString() + */ + @NonNull + public Builder setCardString(@Nullable String cardString) { + mCardString = TextUtils.emptyIfNull(cardString); + return this; + } + + /** + * Set the card ID of the SIM card which contains the subscription. + * + * @param cardId The card ID of the SIM card which contains the subscription. + * @return The builder. + */ + @NonNull + public Builder setCardId(int cardId) { + mCardId = cardId; + return this; + } + + /** + * Set whether the subscription is opportunistic or not. + * + * @param isOpportunistic {@code true} if the subscription is opportunistic. + * @return The builder. + */ + @NonNull + public Builder setOpportunistic(boolean isOpportunistic) { + mIsOpportunistic = isOpportunistic; + return this; + } + + /** + * Set the group UUID of the subscription group. + * + * @param groupUuid The group UUID. + * @return The builder. + * + * @see #getGroupUuid() + */ + @NonNull + public Builder setGroupUuid(@Nullable String groupUuid) { + mGroupUuid = groupUuid == null ? null : ParcelUuid.fromString(groupUuid); + return this; + } + + /** + * Whether group of the subscription is disabled. This is only useful if it's a grouped + * opportunistic subscription. In this case, if all primary (non-opportunistic) + * subscriptions in the group are deactivated (unplugged pSIM or deactivated eSIM profile), + * we should disable this opportunistic subscription. + * + * @param isGroupDisabled {@code true} if group of the subscription is disabled. + * @return The builder. + */ + @NonNull + public Builder setGroupDisabled(boolean isGroupDisabled) { + mIsGroupDisabled = isGroupDisabled; + return this; + } + + /** + * Set the subscription carrier id. + * + * @param carrierId The carrier id. + * @return The builder + * + * @see TelephonyManager#getSimCarrierId() + */ + @NonNull + public Builder setCarrierId(int carrierId) { + mCarrierId = carrierId; + return this; + } + + /** + * Set the profile class populated from the profile metadata if present. + * + * @param profileClass the profile class populated from the profile metadata if present. + * @return The builder + * + * @see #getProfileClass() + */ + @NonNull + public Builder setProfileClass(@ProfileClass int profileClass) { + mProfileClass = profileClass; + return this; + } + + /** + * Set the subscription type. + * + * @param type Subscription type. + * @return The builder. + */ + @NonNull + public Builder setType(@SubscriptionType int type) { + mType = type; + return this; + } + + /** + * Set the owner package of group the subscription belongs to. + * + * @param groupOwner Owner package of group the subscription belongs to. + * @return The builder. + */ + @NonNull + public Builder setGroupOwner(@Nullable String groupOwner) { + mGroupOwner = TextUtils.emptyIfNull(groupOwner); + return this; + } + + /** + * Set the carrier certificates for this subscription that are saved in carrier configs. + * This does not include access rules from the Uicc, whether embedded or non-embedded. + * + * @param carrierConfigAccessRules The carrier certificates for this subscription + * @return The builder. + */ + @NonNull + public Builder setCarrierConfigAccessRules( + @Nullable UiccAccessRule[] carrierConfigAccessRules) { + mCarrierConfigAccessRules = carrierConfigAccessRules; + return this; + } + + /** + * Set whether Uicc applications are configured to enable or not. + * + * @param uiccApplicationsEnabled {@code true} if Uicc applications are configured to + * enable. + * @return The builder. + */ + @NonNull + public Builder setUiccApplicationsEnabled(boolean uiccApplicationsEnabled) { + mAreUiccApplicationsEnabled = uiccApplicationsEnabled; + return this; + } + + /** + * Set the port index of the Uicc card. + * + * @param portIndex The port index of the Uicc card. + * @return The builder. + */ + @NonNull + public Builder setPortIndex(int portIndex) { + mPortIndex = portIndex; + return this; + } + + /** + * Set subscription's preferred usage setting. + * + * @param usageSetting Subscription's preferred usage setting. + * @return The builder. + */ + @NonNull + public Builder setUsageSetting(@UsageSetting int usageSetting) { + mUsageSetting = usageSetting; + return this; + } + + /** + * Build the {@link SubscriptionInfo}. + * + * @return The {@link SubscriptionInfo} instance. + */ + public SubscriptionInfo build() { + return new SubscriptionInfo(this); + } + } } diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 6bc14bfd0cc2..87e651b478e1 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1071,6 +1071,13 @@ public class SubscriptionManager { public static final String ALLOWED_NETWORK_TYPES = SimInfo.COLUMN_ALLOWED_NETWORK_TYPES_FOR_REASONS; + /** + * TelephonyProvider column name for user handle associated with a sim. + * <P>Type: INTEGER (int)</P> + * @hide + */ + public static final String USER_HANDLE = SimInfo.COLUMN_USER_HANDLE; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"USAGE_SETTING_"}, @@ -3099,7 +3106,7 @@ public class SubscriptionManager { @SystemApi public boolean canManageSubscription(@NonNull SubscriptionInfo info, @NonNull String packageName) { - if (info == null || info.getAllAccessRules() == null || packageName == null) { + if (info == null || info.getAccessRules() == null || packageName == null) { return false; } PackageManager packageManager = mContext.getPackageManager(); @@ -3111,7 +3118,7 @@ public class SubscriptionManager { logd("Unknown package: " + packageName); return false; } - for (UiccAccessRule rule : info.getAllAccessRules()) { + for (UiccAccessRule rule : info.getAccessRules()) { if (rule.getCarrierPrivilegeStatus(packageInfo) == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) { return true; diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 12b4114930ca..b4244dd09bbd 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -191,9 +191,6 @@ public class TelephonyManager { @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q) private static final long CALLBACK_ON_MORE_ERROR_CODE_CHANGE = 130595455L; - // Null IMEI anomaly uuid - private static final UUID IMEI_ANOMALY_UUID = UUID.fromString( - "83905f14-6455-450c-be29-8206f0427fe9"); /** * The key to use when placing the result of {@link #requestModemActivityInfo(ResultReceiver)} * into the ResultReceiver Bundle. @@ -2184,11 +2181,7 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM) public String getImei() { - String imei = getImei(getSlotIndex()); - if (imei == null) { - AnomalyReporter.reportAnomaly(IMEI_ANOMALY_UUID, "getImei: IMEI is null."); - } - return imei; + return getImei(getSlotIndex()); } /** @@ -2231,10 +2224,7 @@ public class TelephonyManager { @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM) public String getImei(int slotIndex) { ITelephony telephony = getITelephony(); - if (telephony == null) { - AnomalyReporter.reportAnomaly(IMEI_ANOMALY_UUID, "getImei: telephony is null"); - return null; - } + if (telephony == null) return null; try { return telephony.getImeiForSlot(slotIndex, getOpPackageName(), getAttributionTag()); diff --git a/telephony/java/android/telephony/data/DataService.java b/telephony/java/android/telephony/data/DataService.java index 700d61597d00..d8b2cbebdf28 100644 --- a/telephony/java/android/telephony/data/DataService.java +++ b/telephony/java/android/telephony/data/DataService.java @@ -725,7 +725,7 @@ public abstract class DataService extends Service { @Override public void onDestroy() { - mHandlerThread.quit(); + mHandlerThread.quitSafely(); super.onDestroy(); } diff --git a/tests/FlickerTests/AndroidManifest.xml b/tests/FlickerTests/AndroidManifest.xml index e173eba0a393..487a0c3bd465 100644 --- a/tests/FlickerTests/AndroidManifest.xml +++ b/tests/FlickerTests/AndroidManifest.xml @@ -40,6 +40,8 @@ <uses-permission android:name="android.permission.READ_LOGS"/> <!-- ATM.removeRootTasksWithActivityTypes() --> <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <!-- ActivityOptions.makeCustomTaskAnimation() --> + <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS" /> <!-- Allow the test to write directly to /sdcard/ --> <application android:requestLegacyExternalStorage="true"> <uses-library android:name="android.test.runner"/> diff --git a/services/tests/wmtests/res/values/styles.xml b/tests/FlickerTests/res/anim/show_2000ms.xml index 6857ff99e9b8..76e375f12e31 100644 --- a/services/tests/wmtests/res/values/styles.xml +++ b/tests/FlickerTests/res/anim/show_2000ms.xml @@ -1,3 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> <!-- ~ Copyright (C) 2022 The Android Open Source Project ~ @@ -14,11 +15,7 @@ ~ limitations under the License. --> -<resources> - <style name="WhiteBackgroundTheme" parent="@android:style/Theme.DeviceDefault"> - <item name="android:windowNoTitle">true</item> - <item name="android:windowFullscreen">true</item> - <item name="android:windowIsTranslucent">true</item> - <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> - </style> -</resources> +<translate xmlns:android="http://schemas.android.com/apk/res/android" + android:duration="2000" + android:fromXDelta="0" + android:toXDelta="0" />
\ No newline at end of file diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt index 1e798f3ed12e..f77ec8a4a853 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt @@ -21,13 +21,14 @@ import android.platform.test.annotations.Presubmit import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.traces.common.ComponentNameMatcher import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import org.junit.Assume import org.junit.Test /** - * Base test class containing common assertions for [ComponentMatcher.NAV_BAR], - * [ComponentMatcher.TASK_BAR], [ComponentMatcher.STATUS_BAR], and general assertions + * Base test class containing common assertions for [ComponentNameMatcher.NAV_BAR], + * [ComponentNameMatcher.TASK_BAR], [ComponentNameMatcher.STATUS_BAR], and general assertions * (layers visible in consecutive states, entire screen covered, etc.) */ abstract class BaseTest @JvmOverloads constructor( @@ -72,7 +73,7 @@ abstract class BaseTest @JvmOverloads constructor( open fun entireScreenCovered() = testSpec.entireScreenCovered() /** - * Checks that the [ComponentMatcher.NAV_BAR] layer is visible during the whole transition + * Checks that the [ComponentNameMatcher.NAV_BAR] layer is visible during the whole transition * * Note: Phones only */ @@ -84,7 +85,8 @@ abstract class BaseTest @JvmOverloads constructor( } /** - * Checks the position of the [ComponentMatcher.NAV_BAR] at the start and end of the transition + * Checks the position of the [ComponentNameMatcher.NAV_BAR] at the start and end of the + * transition * * Note: Phones only */ @@ -96,7 +98,7 @@ abstract class BaseTest @JvmOverloads constructor( } /** - * Checks that the [ComponentMatcher.NAV_BAR] window is visible during the whole transition + * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible during the whole transition * * Note: Phones only */ @@ -108,8 +110,8 @@ abstract class BaseTest @JvmOverloads constructor( } /** - * Checks that the [ComponentMatcher.TASK_BAR] window is visible at the start and end of the - * transition + * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible at the start and end of + * the transition * * Note: Large screen only */ @@ -121,7 +123,8 @@ abstract class BaseTest @JvmOverloads constructor( } /** - * Checks that the [ComponentMatcher.TASK_BAR] window is visible during the whole transition + * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible during the whole + * transition * * Note: Large screen only */ @@ -133,7 +136,7 @@ abstract class BaseTest @JvmOverloads constructor( } /** - * Checks that the [ComponentMatcher.STATUS_BAR] layer is visible at the start and end + * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible at the start and end * of the transition */ @Presubmit @@ -142,7 +145,7 @@ abstract class BaseTest @JvmOverloads constructor( testSpec.statusBarLayerIsVisibleAtStartAndEnd() /** - * Checks the position of the [ComponentMatcher.STATUS_BAR] at the start and end of the + * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the * transition */ @Presubmit @@ -150,7 +153,8 @@ abstract class BaseTest @JvmOverloads constructor( open fun statusBarLayerPositionAtStartAndEnd() = testSpec.statusBarLayerPositionAtStartAndEnd() /** - * Checks that the [ComponentMatcher.STATUS_BAR] window is visible during the whole transition + * Checks that the [ComponentNameMatcher.STATUS_BAR] window is visible during the whole + * transition */ @Presubmit @Test diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt index b8fe9f9df882..ef5cec29f898 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt @@ -33,12 +33,12 @@ import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelpe import org.junit.Assume.assumeNotNull class ActivityEmbeddingAppHelper @JvmOverloads constructor( - instr: Instrumentation, - launcherName: String = ActivityOptions.ACTIVITY_EMBEDDING_LAUNCHER_NAME, - component: ComponentNameMatcher = MAIN_ACTIVITY_COMPONENT, - launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy + instr: Instrumentation, + launcherName: String = ActivityOptions.ActivityEmbedding.MainActivity.LABEL, + component: ComponentNameMatcher = MAIN_ACTIVITY_COMPONENT, + launcherStrategy: ILauncherStrategy = LauncherStrategyFactory + .getInstance(instr) + .launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) { /** @@ -47,8 +47,8 @@ class ActivityEmbeddingAppHelper @JvmOverloads constructor( */ fun launchPlaceholderSplit(wmHelper: WindowManagerStateHelper) { val launchButton = uiDevice.wait( - Until.findObject(By.res(getPackage(), "launch_placeholder_split_button")), - FIND_TIMEOUT) + Until.findObject(By.res(getPackage(), "launch_placeholder_split_button")), + FIND_TIMEOUT) require(launchButton != null) { "Can't find launch placeholder split button on screen." } @@ -62,14 +62,15 @@ class ActivityEmbeddingAppHelper @JvmOverloads constructor( companion object { private const val TAG = "ActivityEmbeddingAppHelper" - val MAIN_ACTIVITY_COMPONENT = ActivityOptions - .ACTIVITY_EMBEDDING_MAIN_ACTIVITY_COMPONENT_NAME.toFlickerComponent() + val MAIN_ACTIVITY_COMPONENT = + ActivityOptions.ActivityEmbedding.MainActivity.COMPONENT.toFlickerComponent() - val PLACEHOLDER_PRIMARY_COMPONENT = ActivityOptions - .ACTIVITY_EMBEDDING_PLACEHOLDER_PRIMARY_ACTIVITY_COMPONENT_NAME.toFlickerComponent() + val PLACEHOLDER_PRIMARY_COMPONENT = + ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT + .toFlickerComponent() - val PLACEHOLDER_SECONDARY_COMPONENT = ActivityOptions - .ACTIVITY_EMBEDDING_PLACEHOLDER_SECONDARY_ACTIVITY_COMPONENT_NAME + val PLACEHOLDER_SECONDARY_COMPONENT = + ActivityOptions.ActivityEmbedding.PlaceholderSecondaryActivity.COMPONENT .toFlickerComponent() @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AppPairsHelper.kt index 826cc2ef16d6..4ff4e316476b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AppPairsHelper.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.helpers +package com.android.server.wm.flicker.helpers import android.app.Instrumentation import com.android.server.wm.traces.common.ComponentNameMatcher @@ -23,4 +23,4 @@ class AppPairsHelper( instrumentation: Instrumentation, activityLabel: String, component: ComponentNameMatcher -) : BaseAppHelper(instrumentation, activityLabel, component) +) : StandardAppHelper(instrumentation, activityLabel, component) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt index b696fc30c19f..132e7b6fef6d 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt @@ -24,11 +24,11 @@ import com.android.server.wm.traces.common.ComponentNameMatcher import com.android.server.wm.traces.parser.toFlickerComponent class FixedOrientationAppHelper @JvmOverloads constructor( - instr: Instrumentation, - launcherName: String = ActivityOptions.PORTRAIT_ONLY_ACTIVITY_LAUNCHER_NAME, - component: ComponentNameMatcher = - ActivityOptions.PORTRAIT_ONLY_ACTIVITY_COMPONENT_NAME.toFlickerComponent(), - launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy - ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) + instr: Instrumentation, + launcherName: String = ActivityOptions.PortraitOnlyActivity.LABEL, + component: ComponentNameMatcher = + ActivityOptions.PortraitOnlyActivity.COMPONENT.toFlickerComponent(), + launcherStrategy: ILauncherStrategy = LauncherStrategyFactory + .getInstance(instr) + .launcherStrategy +) : StandardAppHelper(instr, launcherName, component, launcherStrategy) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt index ea5a5f8b3927..2d81e0d9e165 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt @@ -29,9 +29,8 @@ import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelpe class GameAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.GAME_ACTIVITY_LAUNCHER_NAME, - component: ComponentNameMatcher = - ActivityOptions.GAME_ACTIVITY_COMPONENT_NAME.toFlickerComponent(), + launcherName: String = ActivityOptions.Game.LABEL, + component: ComponentNameMatcher = ActivityOptions.Game.COMPONENT.toFlickerComponent(), launcherStrategy: ILauncherStrategy = LauncherStrategyFactory.getInstance(instr).launcherStrategy, ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) { 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 e01cceb5d636..b5b0da934ad9 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 @@ -32,9 +32,9 @@ class ImeAppAutoFocusHelper @JvmOverloads constructor( instr: Instrumentation, private val rotation: Int, private val imePackageName: String = IME_PACKAGE, - launcherName: String = ActivityOptions.IME_ACTIVITY_AUTO_FOCUS_LAUNCHER_NAME, + launcherName: String = ActivityOptions.Ime.AutoFocusActivity.LABEL, component: ComponentNameMatcher = - ActivityOptions.IME_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME.toFlickerComponent() + ActivityOptions.Ime.AutoFocusActivity.COMPONENT.toFlickerComponent() ) : ImeAppHelper(instr, launcherName, component) { override fun openIME(wmHelper: WindowManagerStateHelper) { // do nothing (the app is focused automatically) @@ -62,21 +62,22 @@ class ImeAppAutoFocusHelper @JvmOverloads constructor( fun startDialogThemedActivity(wmHelper: WindowManagerStateHelper) { val button = uiDevice.wait(Until.findObject(By.res(getPackage(), - "start_dialog_themed_activity_btn")), FIND_TIMEOUT) + "start_dialog_themed_activity_btn")), FIND_TIMEOUT) requireNotNull(button) { "Button not found, this usually happens when the device " + - "was left in an unknown state (e.g. Screen turned off)" + "was left in an unknown state (e.g. Screen turned off)" } button.click() wmHelper.StateSyncBuilder() .withFullScreenApp( - ActivityOptions.DIALOG_THEMED_ACTIVITY_COMPONENT_NAME.toFlickerComponent()) + ActivityOptions.DialogThemedActivity.COMPONENT.toFlickerComponent()) .waitForAndVerify() } + fun dismissDialog(wmHelper: WindowManagerStateHelper) { val dialog = uiDevice.wait( - Until.findObject(By.text("Dialog for test")), FIND_TIMEOUT) + Until.findObject(By.text("Dialog for test")), FIND_TIMEOUT) // Pressing back key to dismiss the dialog if (dialog != null) { @@ -86,9 +87,10 @@ class ImeAppAutoFocusHelper @JvmOverloads constructor( .waitForAndVerify() } } + fun getInsetsVisibleFromDialog(type: Int): Boolean { val insetsVisibilityTextView = uiDevice.wait( - Until.findObject(By.res("android:id/text1")), FIND_TIMEOUT) + Until.findObject(By.res("android:id/text1")), FIND_TIMEOUT) if (insetsVisibilityTextView != null) { val visibility = insetsVisibilityTextView.text.toString() val matcher = when (type) { 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 b672b1bc0da5..56b6b92dd6ef 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 @@ -28,12 +28,11 @@ import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelpe open class ImeAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.IME_ACTIVITY_LAUNCHER_NAME, + launcherName: String = ActivityOptions.Ime.Default.LABEL, component: ComponentNameMatcher = - ActivityOptions.IME_ACTIVITY_COMPONENT_NAME.toFlickerComponent(), - launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy + ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent(), + launcherStrategy: ILauncherStrategy = + LauncherStrategyFactory.getInstance(instr).launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) { /** * Opens the IME and wait for it to be displayed @@ -73,8 +72,8 @@ open class ImeAppHelper @JvmOverloads constructor( open fun finishActivity(wmHelper: WindowManagerStateHelper) { val finishButton = uiDevice.wait( - Until.findObject(By.res(getPackage(), "finish_activity_btn")), - FIND_TIMEOUT) + Until.findObject(By.res(getPackage(), "finish_activity_btn")), + FIND_TIMEOUT) requireNotNull(finishButton) { "Finish activity button not found, probably IME activity is not on the screen?" } 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 df47e9dfacd9..513103ab9ec9 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 @@ -26,16 +26,16 @@ import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelpe class ImeEditorPopupDialogAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.EDITOR_POPUP_DIALOG_ACTIVITY_LAUNCHER_NAME, + launcherName: String = ActivityOptions.Ime.AutoFocusActivity.LABEL, component: ComponentNameMatcher = - ActivityOptions.EDITOR_POPUP_DIALOG_ACTIVITY_COMPONENT_NAME.toFlickerComponent() + ActivityOptions.Ime.AutoFocusActivity.COMPONENT.toFlickerComponent() ) : ImeAppHelper(instr, launcherName, component) { override fun openIME(wmHelper: WindowManagerStateHelper) { val editText = uiDevice.wait(Until.findObject(By.text("focused editText")), 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)" + "was left in an unknown state (e.g. in split screen)" } editText.click() waitIMEShown(wmHelper) @@ -43,7 +43,7 @@ class ImeEditorPopupDialogAppHelper @JvmOverloads constructor( fun dismissDialog(wmHelper: WindowManagerStateHelper) { val dismissButton = uiDevice.wait( - Until.findObject(By.text("Dismiss")), FIND_TIMEOUT) + Until.findObject(By.text("Dismiss")), FIND_TIMEOUT) // Pressing back key to dismiss the dialog if (dismissButton != null) { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt index d3945c1749e3..3b8d3c3440a8 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt @@ -25,10 +25,9 @@ import com.android.server.wm.traces.parser.toFlickerComponent class ImeStateInitializeHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.IME_ACTIVITY_INITIALIZE_LAUNCHER_NAME, + launcherName: String = ActivityOptions.Ime.StateInitializeActivity.LABEL, component: ComponentNameMatcher = - ActivityOptions.IME_ACTIVITY_INITIALIZE_COMPONENT_NAME.toFlickerComponent(), - launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy + ActivityOptions.Ime.StateInitializeActivity.COMPONENT.toFlickerComponent(), + launcherStrategy: ILauncherStrategy = + LauncherStrategyFactory.getInstance(instr).launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LaunchBubbleHelper.kt index 4d0fbc4a0e38..cb54b5732c8d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LaunchBubbleHelper.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.helpers +package com.android.server.wm.flicker.helpers import android.app.Instrumentation +import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.server.wm.traces.parser.toFlickerComponent -import com.android.wm.shell.flicker.testapp.Components -class SimpleAppHelper(instrumentation: Instrumentation) : BaseAppHelper( +class LaunchBubbleHelper(instrumentation: Instrumentation) : StandardAppHelper( instrumentation, - Components.SimpleActivity.LABEL, - Components.SimpleActivity.COMPONENT.toFlickerComponent() -)
\ No newline at end of file + ActivityOptions.Bubbles.LaunchBubble.LABEL, + ActivityOptions.Bubbles.LaunchBubble.COMPONENT.toFlickerComponent() +) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt index 807e6729c2d0..dde0b3e6873d 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt @@ -28,13 +28,13 @@ import com.android.server.wm.traces.common.ComponentNameMatcher import com.android.server.wm.traces.parser.toFlickerComponent class MailAppHelper @JvmOverloads constructor( - instr: Instrumentation, - launcherName: String = ActivityOptions.MAIL_ACTIVITY_LAUNCHER_NAME, - component: ComponentNameMatcher = - ActivityOptions.MAIL_ACTIVITY_COMPONENT_NAME.toFlickerComponent(), - launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy + instr: Instrumentation, + launcherName: String = ActivityOptions.Mail.LABEL, + component: ComponentNameMatcher = + ActivityOptions.Mail.COMPONENT.toFlickerComponent(), + launcherStrategy: ILauncherStrategy = LauncherStrategyFactory + .getInstance(instr) + .launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) { fun openMail(rowIdx: Int) { @@ -46,7 +46,7 @@ class MailAppHelper @JvmOverloads constructor( if (row != null) break scrollDown() } - require(row != null) {""} + require(row != null) { "" } row.click() uiDevice.wait(Until.gone(By.res(getPackage(), MAIL_LIST_RES_ID)), FIND_TIMEOUT) } @@ -57,9 +57,9 @@ class MailAppHelper @JvmOverloads constructor( } fun waitForMailList(): UiObject2 { - var sel = By.res(getPackage(), MAIL_LIST_RES_ID).scrollable(true) + val sel = By.res(getPackage(), MAIL_LIST_RES_ID).scrollable(true) val ret = uiDevice.wait(Until.findObject(sel), FIND_TIMEOUT) - require(ret != null) {""} + requireNotNull(ret) { "Unable to find $MAIL_LIST_RES_ID object" } return ret } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MultiWindowUtils.kt index 245a82f938b3..9bdfadb35ff3 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MultiWindowUtils.kt @@ -14,20 +14,31 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.helpers +package com.android.server.wm.flicker.helpers import android.app.Instrumentation import android.content.Context import android.provider.Settings +import android.util.Log +import com.android.compatibility.common.util.SystemUtil import com.android.server.wm.traces.common.ComponentNameMatcher +import java.io.IOException -class MultiWindowHelper( +class MultiWindowUtils( instrumentation: Instrumentation, activityLabel: String, componentsInfo: ComponentNameMatcher -) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) { +) : StandardAppHelper(instrumentation, activityLabel, componentsInfo) { companion object { + fun executeShellCommand(instrumentation: Instrumentation, cmd: String) { + try { + SystemUtil.runShellCommand(instrumentation, cmd) + } catch (e: IOException) { + Log.e(MultiWindowUtils::class.simpleName, "executeShellCommand error! $e") + } + } + fun getDevEnableNonResizableMultiWindow(context: Context): Int = Settings.Global.getInt(context.contentResolver, Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW) 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 9fb574cf1c04..f3386afa610c 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 @@ -29,9 +29,9 @@ import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelpe class NewTasksAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.LAUNCH_NEW_TASK_ACTIVITY_LAUNCHER_NAME, + launcherName: String = ActivityOptions.LaunchNewTask.LABEL, component: ComponentNameMatcher = - ActivityOptions.LAUNCH_NEW_TASK_ACTIVITY_COMPONENT_NAME.toFlickerComponent(), + ActivityOptions.LaunchNewTask.COMPONENT.toFlickerComponent(), launcherStrategy: ILauncherStrategy = LauncherStrategyFactory .getInstance(instr) .launcherStrategy @@ -43,7 +43,7 @@ class NewTasksAppHelper @JvmOverloads constructor( requireNotNull(button) { "Button not found, this usually happens when the device " + - "was left in an unknown state (e.g. in split screen)" + "was left in an unknown state (e.g. in split screen)" } button.click() wmHelper.StateSyncBuilder() diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt index a1dbeeaef5cc..19ce3ba21812 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt @@ -25,10 +25,9 @@ import com.android.server.wm.traces.parser.toFlickerComponent class NonResizeableAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.NON_RESIZEABLE_ACTIVITY_LAUNCHER_NAME, + launcherName: String = ActivityOptions.NonResizeableActivity.LABEL, component: ComponentNameMatcher = - ActivityOptions.NON_RESIZEABLE_ACTIVITY_COMPONENT_NAME.toFlickerComponent(), - launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy + ActivityOptions.NonResizeableActivity.COMPONENT.toFlickerComponent(), + launcherStrategy: ILauncherStrategy = + LauncherStrategyFactory.getInstance(instr).launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) 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 b031a4523ab0..97642f1617af 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 @@ -28,25 +28,28 @@ import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelpe class NotificationAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.NOTIFICATION_ACTIVITY_LAUNCHER_NAME, + launcherName: String = ActivityOptions.Notification.LABEL, component: ComponentNameMatcher = - ActivityOptions.NOTIFICATION_ACTIVITY_COMPONENT_NAME.toFlickerComponent(), + ActivityOptions.Notification.COMPONENT.toFlickerComponent(), launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy + .getInstance(instr) + .launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) { fun postNotification(wmHelper: WindowManagerStateHelper) { val button = uiDevice.wait( - Until.findObject(By.res(getPackage(), "post_notification")), - FIND_TIMEOUT) + Until.findObject(By.res(getPackage(), "post_notification")), + FIND_TIMEOUT) - require(button != null) { + requireNotNull(button) { "Post notification button not found, this usually happens when the device " + - "was left in an unknown state (e.g. in split screen)" + "was left in an unknown state (e.g. in split screen)" } button.click() uiDevice.wait(Until.findObject(By.text("Flicker Test Notification")), FIND_TIMEOUT) - ?: error("Flicker Notification not found") + ?: error("Flicker Notification not found") + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .waitForAndVerify() } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt index bdc05e7ecb52..4d801c9032cb 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt @@ -14,28 +14,23 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.helpers +package com.android.server.wm.flicker.helpers import android.app.Instrumentation import android.media.session.MediaController import android.media.session.MediaSessionManager 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.flicker.testapp.ActivityOptions 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 -import com.android.wm.shell.flicker.pip.tv.isFocusedOrHasFocusedChild -import com.android.wm.shell.flicker.testapp.Components -class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( +open class PipAppHelper(instrumentation: Instrumentation) : StandardAppHelper( instrumentation, - Components.PipActivity.LABEL, - Components.PipActivity.COMPONENT.toFlickerComponent() + ActivityOptions.Pip.LABEL, + ActivityOptions.Pip.COMPONENT.toFlickerComponent() ) { private val mediaSessionManager: MediaSessionManager get() = context.getSystemService(MediaSessionManager::class.java) @@ -46,16 +41,11 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( it.packageName == `package` } - fun clickObject(resId: String) { + open fun clickObject(resId: String) { val selector = By.res(`package`, resId) val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") - if (!isTelevision) { - obj.click() - } else { - focusOnObject(selector) || error("Could not focus on `$resId` object") - uiDevice.pressDPadCenter() - } + obj.click() } /** @@ -85,20 +75,6 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( fun exitPipToFullScreenViaIntent(wmHelper: WindowManagerStateHelper) = launchViaIntentAndWaitShown(wmHelper) - private fun focusOnObject(selector: BySelector): Boolean { - // We expect all the focusable UI elements to be arranged in a way so that it is possible - // to "cycle" over all them by clicking the D-Pad DOWN button, going back up to "the top" - // from "the bottom". - repeat(FOCUS_ATTEMPTS) { - uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true } - ?: error("The object we try to focus on is gone.") - - uiDevice.pressDPadDown() - uiDevice.waitForIdle() - } - return false - } - fun clickEnterPipButton(wmHelper: WindowManagerStateHelper) { clickObject(ENTER_PIP_BUTTON_ID) @@ -140,12 +116,8 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( "Use PipAppHelper.closePipWindow(wmHelper) instead", ReplaceWith("closePipWindow(wmHelper)") ) - fun closePipWindow() { - if (isTelevision) { - uiDevice.closeTvPipWindow() - } else { - closePipWindow(WindowManagerStateHelper(mInstrumentation)) - } + open fun closePipWindow() { + closePipWindow(WindowManagerStateHelper(mInstrumentation)) } private fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect { @@ -159,20 +131,16 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( /** * Taps the pip window and dismisses it by clicking on the X button. */ - fun closePipWindow(wmHelper: WindowManagerStateHelper) { - if (isTelevision) { - uiDevice.closeTvPipWindow() - } else { - val windowRect = getWindowRect(wmHelper) - uiDevice.click(windowRect.centerX(), windowRect.centerY()) - // search and interact with the dismiss button - val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") - uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) - val dismissPipObject = uiDevice.findObject(dismissSelector) - ?: error("PIP window dismiss button not found") - val dismissButtonBounds = dismissPipObject.visibleBounds - uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY()) - } + open fun closePipWindow(wmHelper: WindowManagerStateHelper) { + val windowRect = getWindowRect(wmHelper) + uiDevice.click(windowRect.centerX(), windowRect.centerY()) + // search and interact with the dismiss button + val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") + uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) + val dismissPipObject = uiDevice.findObject(dismissSelector) + ?: error("PIP window dismiss button not found") + val dismissButtonBounds = dismissPipObject.visibleBounds + uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY()) // Wait for animation to complete. wmHelper.StateSyncBuilder() @@ -213,7 +181,6 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } companion object { - private const val FOCUS_ATTEMPTS = 20 private const val ENTER_PIP_BUTTON_ID = "enter_pip" private const val WITH_CUSTOM_ACTIONS_BUTTON_ID = "with_custom_actions" private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start" diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt index 6d466d72d7dd..fc1ff7c84675 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt @@ -25,10 +25,9 @@ import com.android.server.wm.traces.parser.toFlickerComponent class SeamlessRotationAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.SEAMLESS_ACTIVITY_LAUNCHER_NAME, + launcherName: String = ActivityOptions.SeamlessRotation.LABEL, component: ComponentNameMatcher = - ActivityOptions.SEAMLESS_ACTIVITY_COMPONENT_NAME.toFlickerComponent(), - launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy + ActivityOptions.SeamlessRotation.COMPONENT.toFlickerComponent(), + launcherStrategy: ILauncherStrategy = + LauncherStrategyFactory.getInstance(instr).launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt index 804ab3864591..0e1df411d394 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt @@ -25,10 +25,9 @@ import com.android.server.wm.traces.parser.toFlickerComponent class ShowWhenLockedAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.SHOW_WHEN_LOCKED_ACTIVITY_LAUNCHER_NAME, + launcherName: String = ActivityOptions.ShowWhenLockedActivity.LABEL, component: ComponentNameMatcher = - ActivityOptions.SHOW_WHEN_LOCKED_ACTIVITY_COMPONENT_NAME.toFlickerComponent(), - launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy + ActivityOptions.ShowWhenLockedActivity.COMPONENT.toFlickerComponent(), + launcherStrategy: ILauncherStrategy = + LauncherStrategyFactory.getInstance(instr).launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt index 5da273a7f1dc..e3461a74b4ce 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt @@ -25,10 +25,9 @@ import com.android.server.wm.traces.parser.toFlickerComponent class SimpleAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.SIMPLE_ACTIVITY_LAUNCHER_NAME, + launcherName: String = ActivityOptions.SimpleActivity.LABEL, component: ComponentNameMatcher = - ActivityOptions.SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME.toFlickerComponent(), - launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy + ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent(), + launcherStrategy: ILauncherStrategy = + LauncherStrategyFactory.getInstance(instr).launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) 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 060e9af4a51c..f4ea37f2127a 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 @@ -29,16 +29,15 @@ import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelpe class TwoActivitiesAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.BUTTON_ACTIVITY_LAUNCHER_NAME, + launcherName: String = ActivityOptions.LaunchNewActivity.LABEL, component: ComponentNameMatcher = - ActivityOptions.BUTTON_ACTIVITY_COMPONENT_NAME.toFlickerComponent(), - launcherStrategy: ILauncherStrategy = LauncherStrategyFactory - .getInstance(instr) - .launcherStrategy + ActivityOptions.LaunchNewActivity.COMPONENT.toFlickerComponent(), + launcherStrategy: ILauncherStrategy = + LauncherStrategyFactory.getInstance(instr).launcherStrategy ) : StandardAppHelper(instr, launcherName, component, launcherStrategy) { private val secondActivityComponent = - ActivityOptions.SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME.toFlickerComponent() + ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent() fun openSecondActivity(device: UiDevice, wmHelper: WindowManagerStateHelper) { val launchActivityButton = By.res(getPackage(), LAUNCH_SECOND_ACTIVITY) @@ -46,7 +45,7 @@ class TwoActivitiesAppHelper @JvmOverloads constructor( requireNotNull(button) { "Button not found, this usually happens when the device " + - "was left in an unknown state (e.g. in split screen)" + "was left in an unknown state (e.g. in split screen)" } button.click() 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 08d7be2ac662..f019acc29316 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 @@ -83,19 +83,17 @@ class ActivitiesTransitionTest(testSpec: FlickerTestParameter) : BaseTest(testSp override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() /** - * Checks that the [ActivityOptions.BUTTON_ACTIVITY_COMPONENT_NAME] activity is visible at - * the start of the transition, that - * [ActivityOptions.SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME] becomes visible during the - * transition, and that [ActivityOptions.BUTTON_ACTIVITY_COMPONENT_NAME] is again visible - * at the end + * Checks that the [ActivityOptions.LaunchNewActivity] activity is visible at + * the start of the transition, that [ActivityOptions.SimpleActivity] becomes visible during + * the transition, and that [ActivityOptions.LaunchNewActivity] is again visible at the end */ @Presubmit @Test fun finishSubActivity() { - val buttonActivityComponent = ActivityOptions - .BUTTON_ACTIVITY_COMPONENT_NAME.toFlickerComponent() - val imeAutoFocusActivityComponent = ActivityOptions - .SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME.toFlickerComponent() + val buttonActivityComponent = + ActivityOptions.LaunchNewActivity.COMPONENT.toFlickerComponent() + val imeAutoFocusActivityComponent = + ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent() testSpec.assertWm { this.isAppWindowOnTop(buttonActivityComponent) .then() @@ -106,7 +104,7 @@ class ActivitiesTransitionTest(testSpec: FlickerTestParameter) : BaseTest(testSp } /** - * Checks that the [ComponentMatcher.LAUNCHER] window is not on top. The launcher cannot be + * Checks that the [ComponentNameMatcher.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 */ @@ -119,7 +117,7 @@ class ActivitiesTransitionTest(testSpec: FlickerTestParameter) : BaseTest(testSp } /** - * Checks that the [ComponentMatcher.LAUNCHER] layer is never visible during the transition + * Checks that the [ComponentNameMatcher.LAUNCHER] layer is never visible during the transition */ @Presubmit @Test diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt new file mode 100644 index 000000000000..4f21412a1959 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt @@ -0,0 +1,112 @@ +/* + * 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.app.Instrumentation +import android.os.Bundle +import android.os.Handler +import android.platform.test.annotations.Presubmit +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.R +import com.android.server.wm.flicker.annotation.Group4 +import com.android.server.wm.flicker.dsl.FlickerBuilder +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.rules.RemoveAllTasksButHomeRule +import com.android.server.wm.traces.common.ComponentNameMatcher +import com.android.server.wm.traces.common.WindowManagerConditionsFactory +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test the [android.app.ActivityOptions.makeCustomTaskAnimation]. + * + * To run this test: `atest FlickerTests:OverrideTaskTransitionTest` + * + * Actions: + * Launches SimpleActivity with alpha_2000ms animation + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group4 +class OverrideTaskTransitionTest(val testSpec: FlickerTestParameter) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val testApp: StandardAppHelper = SimpleAppHelper(instrumentation) + + @FlickerBuilderProvider + fun buildFlicker(): FlickerBuilder { + return FlickerBuilder(instrumentation).apply { + setup { + device.wakeUpAndGoToHomeScreen() + RemoveAllTasksButHomeRule.removeAllTasksButHome() + setRotation(testSpec.startRotation) + } + transitions { + instrumentation.context.startActivity( + testApp.openAppIntent, createCustomTaskAnimation()) + wmHelper.StateSyncBuilder() + .add(WindowManagerConditionsFactory.isWMStateComplete()) + .withAppTransitionIdle() + .withWindowSurfaceAppeared(testApp) + .waitForAndVerify() + } + teardown { + testApp.exit() + } + } + } + + @Presubmit + @Test + fun testSimpleActivityIsShownDirectly() { + testSpec.assertLayers { + isVisible(ComponentNameMatcher.LAUNCHER) + .isInvisible(ComponentNameMatcher.SPLASH_SCREEN) + .isInvisible(testApp) + .then() + // The custom animation should block the entire launcher from the very beginning + .isInvisible(ComponentNameMatcher.LAUNCHER) + } + } + + private fun createCustomTaskAnimation(): Bundle { + return android.app.ActivityOptions.makeCustomTaskAnimation(instrumentation.context, + R.anim.show_2000ms, 0, Handler.getMain(), null, null).toBundle() + } + + companion object { + @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/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt index 26f46cd073f1..63b78b687ae2 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 @@ -28,8 +28,7 @@ import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.NewTasksAppHelper import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.testapp.ActivityOptions.LAUNCH_NEW_TASK_ACTIVITY_COMPONENT_NAME -import com.android.server.wm.flicker.testapp.ActivityOptions.SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME +import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.server.wm.traces.common.ComponentNameMatcher import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.SPLASH_SCREEN import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER @@ -105,7 +104,7 @@ class TaskTransitionTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) { } /** - * Check that the [ComponentMatcher.LAUNCHER] window is never visible when performing task + * Check that the [ComponentNameMatcher.LAUNCHER] window is never visible when performing task * transitions. * A solid color background should be shown above it. */ @@ -118,7 +117,7 @@ class TaskTransitionTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) { } /** - * Checks that the [ComponentMatcher.LAUNCHER] layer is never visible when performing task + * Checks that the [ComponentNameMatcher.LAUNCHER] layer is never visible when performing task * transitions. * A solid color background should be shown above it. */ @@ -246,8 +245,8 @@ class TaskTransitionTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) { companion object { private val LAUNCH_NEW_TASK_ACTIVITY = - LAUNCH_NEW_TASK_ACTIVITY_COMPONENT_NAME.toFlickerComponent() - private val SIMPLE_ACTIVITY = SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME.toFlickerComponent() + ActivityOptions.LaunchNewActivity.COMPONENT.toFlickerComponent() + private val SIMPLE_ACTIVITY = ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent() private fun getWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher? { val wallpaperManager = WallpaperManager.getInstance(instrumentation.targetContext) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt index 16ad6309af40..9d27079c6a86 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt @@ -90,7 +90,7 @@ open class SeamlessAppRotationTest( testApp.launchViaIntent( wmHelper, stringExtras = mapOf( - ActivityOptions.EXTRA_STARVE_UI_THREAD + ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD to testSpec.starveUiThread.toString() ) ) @@ -164,20 +164,23 @@ open class SeamlessAppRotationTest( /** {@inheritDoc} */ @Test @Ignore("Not applicable to this CUJ. App is full screen") - override fun statusBarLayerPositionAtStartAndEnd() { } + override fun statusBarLayerPositionAtStartAndEnd() { + } /** {@inheritDoc} */ @Test @Ignore("Not applicable to this CUJ. App is full screen") - override fun statusBarLayerIsVisibleAtStartAndEnd() { } + override fun statusBarLayerIsVisibleAtStartAndEnd() { + } /** {@inheritDoc} */ @Test @Ignore("Not applicable to this CUJ. App is full screen") - override fun statusBarWindowIsAlwaysVisible() { } + override fun statusBarWindowIsAlwaysVisible() { + } /** - * Checks that the [ComponentMatcher.STATUS_BAR] window is invisible during the whole + * Checks that the [ComponentNameMatcher.STATUS_BAR] window is invisible during the whole * transition */ @Presubmit @@ -189,7 +192,7 @@ open class SeamlessAppRotationTest( } /** - * Checks that the [ComponentMatcher.STATUS_BAR] layer is invisible during the whole + * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is invisible during the whole * transition */ @Presubmit @@ -218,14 +221,17 @@ open class SeamlessAppRotationTest( companion object { private val FlickerTestParameter.starveUiThread - get() = config.getOrDefault(ActivityOptions.EXTRA_STARVE_UI_THREAD, false) as Boolean + get() = config.getOrDefault( + ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD, false) as Boolean private fun createConfig( sourceConfig: FlickerTestParameter, starveUiThread: Boolean ): FlickerTestParameter { val newConfig = sourceConfig.config.toMutableMap() - .also { it[ActivityOptions.EXTRA_STARVE_UI_THREAD] = starveUiThread } + .also { + it[ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD] = starveUiThread + } val nameExt = if (starveUiThread) "_BUSY_UI_THREAD" else "" return FlickerTestParameter(newConfig, nameOverride = "$sourceConfig$nameExt") } @@ -233,8 +239,8 @@ open class SeamlessAppRotationTest( /** * Creates the test configurations for seamless rotation based on the default rotation * tests from [FlickerTestParameterFactory.getConfigRotationTests], but adding an - * additional flag ([ActivityOptions.EXTRA_STARVE_UI_THREAD]) to indicate if the app - * should starve the UI thread of not + * additional flag ([ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD]) to indicate + * if the app should starve the UI thread of not */ @JvmStatic private fun getConfigurations(): List<FlickerTestParameter> { diff --git a/tests/FlickerTests/test-apps/Android.bp b/tests/FlickerTests/test-apps/Android.bp deleted file mode 100644 index e69de29bb2d1..000000000000 --- a/tests/FlickerTests/test-apps/Android.bp +++ /dev/null diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml index 725963bc7ca3..6e935d11411e 100644 --- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -15,41 +15,41 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.server.wm.flicker.testapp"> + package="com.android.server.wm.flicker.testapp"> <uses-sdk android:minSdkVersion="29" - android:targetSdkVersion="29"/> + android:targetSdkVersion="29"/> <application android:allowBackup="false" - android:supportsRtl="true"> + android:supportsRtl="true"> <uses-library android:name="androidx.window.extensions" android:required="false"/> <activity android:name=".SimpleActivity" - android:taskAffinity="com.android.server.wm.flicker.testapp.SimpleActivity" - android:theme="@style/CutoutShortEdges" - android:label="SimpleApp" - android:exported="true"> + android:taskAffinity="com.android.server.wm.flicker.testapp.SimpleActivity" + android:theme="@style/CutoutShortEdges" + android:label="SimpleActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".ImeActivity" - android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivity" - android:theme="@style/CutoutShortEdges" - android:label="ImeApp" - android:exported="true"> + android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivity" + android:theme="@style/CutoutShortEdges" + android:label="ImeActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".ImeActivityAutoFocus" - android:theme="@style/CutoutShortEdges" - android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivityAutoFocus" - android:windowSoftInputMode="stateVisible" - android:configChanges="orientation|screenSize" - android:label="ImeAppAutoFocus" - android:exported="true"> + android:theme="@style/CutoutShortEdges" + android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivityAutoFocus" + android:windowSoftInputMode="stateVisible" + android:configChanges="orientation|screenSize" + android:label="ImeAppAutoFocus" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> @@ -66,34 +66,34 @@ </intent-filter> </activity> <activity android:name=".SeamlessRotationActivity" - android:taskAffinity="com.android.server.wm.flicker.testapp.SeamlessRotationActivity" - android:theme="@style/CutoutShortEdges" - android:configChanges="orientation|screenSize" - android:label="SeamlessApp" - android:exported="true"> + android:taskAffinity="com.android.server.wm.flicker.testapp.SeamlessRotationActivity" + android:theme="@style/CutoutShortEdges" + android:configChanges="orientation|screenSize" + android:label="SeamlessActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".NonResizeableActivity" - android:theme="@style/CutoutShortEdges" - android:resizeableActivity="false" - android:taskAffinity="com.android.server.wm.flicker.testapp.NonResizeableActivity" - android:label="NonResizeableApp" - android:exported="true" - android:showOnLockScreen="true"> + android:theme="@style/CutoutShortEdges" + android:resizeableActivity="false" + android:taskAffinity="com.android.server.wm.flicker.testapp.NonResizeableActivity" + android:label="NonResizeableActivity" + android:exported="true" + android:showOnLockScreen="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> - <activity android:name=".ButtonActivity" - android:taskAffinity="com.android.server.wm.flicker.testapp.ButtonActivity" - android:theme="@style/CutoutShortEdges" - android:configChanges="orientation|screenSize" - android:label="ButtonActivity" - android:exported="true"> + <activity android:name=".LaunchNewActivity" + android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchNewActivity" + android:theme="@style/CutoutShortEdges" + android:configChanges="orientation|screenSize" + android:label="LaunchNewActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> @@ -111,55 +111,56 @@ </intent-filter> </activity> <activity android:name=".DialogThemedActivity" - android:taskAffinity="com.android.server.wm.flicker.testapp.DialogThemedActivity" - android:configChanges="orientation|screenSize" - android:theme="@style/DialogTheme" - android:label="DialogThemedActivity" - android:exported="true"> + android:taskAffinity="com.android.server.wm.flicker.testapp.DialogThemedActivity" + android:configChanges="orientation|screenSize" + android:theme="@style/DialogTheme" + android:label="DialogThemedActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".PortraitOnlyActivity" - android:taskAffinity="com.android.server.wm.flicker.testapp.PortraitOnlyActivity" - android:theme="@style/CutoutShortEdges" - android:screenOrientation="portrait" - android:configChanges="orientation|screenSize" - android:exported="true"> + android:taskAffinity="com.android.server.wm.flicker.testapp.PortraitOnlyActivity" + android:theme="@style/CutoutShortEdges" + android:screenOrientation="portrait" + android:configChanges="orientation|screenSize" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".ImeEditorPopupDialogActivity" - android:taskAffinity="com.android.server.wm.flicker.testapp.ImeEditorPopupDialogActivity" - android:configChanges="orientation|screenSize" - android:theme="@style/CutoutShortEdges" - android:label="ImeEditorPopupDialogActivity" - android:exported="true"> + android:taskAffinity="com.android.server.wm.flicker.testapp.ImeEditorPopupDialogActivity" + android:configChanges="orientation|screenSize" + android:theme="@style/CutoutShortEdges" + android:label="ImeEditorPopupDialogActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".ShowWhenLockedActivity" - android:taskAffinity="com.android.server.wm.flicker.testapp.ShowWhenLockedActivity" - android:theme="@style/CutoutShortEdges" - android:configChanges="orientation|screenSize" - android:label="ShowWhenLockedActivity" - android:showWhenLocked="true" - android:exported="true"> + android:taskAffinity="com.android.server.wm.flicker.testapp.ShowWhenLockedActivity" + android:theme="@style/CutoutShortEdges" + android:configChanges="orientation|screenSize" + android:label="ShowWhenLockedActivity" + android:showWhenLocked="true" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".NotificationActivity" - android:taskAffinity="com.android.server.wm.flicker.testapp.NotificationActivity" - android:theme="@style/CutoutShortEdges" - android:configChanges="orientation|screenSize" - android:exported="true"> + android:taskAffinity="com.android.server.wm.flicker.testapp.NotificationActivity" + android:theme="@style/CutoutShortEdges" + android:configChanges="orientation|screenSize" + android:label="NotificationActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> @@ -173,8 +174,8 @@ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:exported="true"> <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity @@ -193,42 +194,110 @@ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:exported="false"/> <activity android:name=".MailActivity" - android:exported="true" - android:label="MailApp" - android:taskAffinity="com.android.server.wm.flicker.testapp.MailActivity" - android:theme="@style/Theme.AppCompat.Light"> + android:exported="true" + android:label="MailActivity" + android:taskAffinity="com.android.server.wm.flicker.testapp.MailActivity" + android:theme="@style/Theme.AppCompat.Light"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".GameActivity" - android:taskAffinity="com.android.server.wm.flicker.testapp.GameActivity" - android:immersive="true" - android:theme="@android:style/Theme.NoTitleBar" - android:configChanges="screenSize" - android:label="GameApp" - android:exported="true"> + android:taskAffinity="com.android.server.wm.flicker.testapp.GameActivity" + android:immersive="true" + android:theme="@android:style/Theme.NoTitleBar" + android:configChanges="screenSize" + android:label="GameActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity android:name=".PipActivity" + android:resizeableActivity="true" + android:supportsPictureInPicture="true" + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" + android:taskAffinity="com.android.server.wm.flicker.testapp.PipActivity" + android:theme="@style/CutoutShortEdges" + android:launchMode="singleTop" + android:label="PipActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> + </intent-filter> + </activity> + <activity android:name=".SplitScreenActivity" + android:resizeableActivity="true" + android:taskAffinity="com.android.server.wm.flicker.testapp.SplitScreenActivity" + android:theme="@style/CutoutShortEdges" + android:label="SplitScreenPrimaryActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + <activity android:name=".SplitScreenSecondaryActivity" + android:resizeableActivity="true" + android:taskAffinity="com.android.server.wm.flicker.testapp.SplitScreenSecondaryActivity" + android:theme="@style/CutoutShortEdges" + android:label="SplitScreenSecondaryActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity android:name=".SendNotificationActivity" + android:taskAffinity="com.android.server.wm.flicker.testapp.SendNotificationActivity" + android:theme="@style/CutoutShortEdges" + android:label="SendNotificationActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity + android:name=".LaunchBubbleActivity" + android:label="LaunchBubbleActivity" + android:exported="true" + android:theme="@style/CutoutShortEdges" + android:launchMode="singleTop"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <action android:name="android.intent.action.VIEW"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity + android:name=".BubbleActivity" + android:label="BubbleActivity" + android:exported="false" + android:theme="@style/CutoutShortEdges" + android:resizeableActivity="true"/> <service android:name=".AssistantInteractionSessionService" android:exported="true" - android:permission="android.permission.BIND_VOICE_INTERACTION" /> + android:permission="android.permission.BIND_VOICE_INTERACTION"/> <service android:name=".AssistantRecognitionService" android:exported="true" android:label="Test Voice Interaction Service"> <intent-filter> - <action android:name="android.speech.RecognitionService" /> - <category android:name="android.intent.category.DEFAULT" /> + <action android:name="android.speech.RecognitionService"/> + <category android:name="android.intent.category.DEFAULT"/> </intent-filter> <meta-data android:name="android.speech" - android:resource="@xml/recognition_service" /> + android:resource="@xml/recognition_service"/> </service> <service android:name=".AssistantInteractionService" @@ -236,12 +305,12 @@ android:label="Test Voice Interaction Service" android:permission="android.permission.BIND_VOICE_INTERACTION"> <intent-filter> - <action android:name="android.service.voice.VoiceInteractionService" /> + <action android:name="android.service.voice.VoiceInteractionService"/> </intent-filter> <meta-data android:name="android.voice_interaction" - android:resource="@xml/interaction_service" /> + android:resource="@xml/interaction_service"/> </service> </application> - <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" /> + <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/> </manifest> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png b/tests/FlickerTests/test-apps/flickerapp/res/drawable/bg.png Binary files differindex d424a17b4157..d424a17b4157 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png +++ b/tests/FlickerTests/test-apps/flickerapp/res/drawable/bg.png diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml b/tests/FlickerTests/test-apps/flickerapp/res/drawable/ic_bubble.xml index b43f31da748d..4ea156de9f40 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/drawable/ic_bubble.xml @@ -1,19 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - Copyright 2021 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. ---> + ~ 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. + --> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml b/tests/FlickerTests/test-apps/flickerapp/res/drawable/ic_message.xml index 0e8c7a0fe64a..45ed98c6e503 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/drawable/ic_message.xml @@ -1,19 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - Copyright 2021 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. ---> + ~ 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. + --> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bubble.xml index f8b0ca3da26e..7c7b2cacaefb 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bubble.xml @@ -1,19 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - Copyright 2021 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. ---> + ~ 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:layout_width="match_parent" diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_button.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_launch_new.xml index fe7bced690f9..fe7bced690f9 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_button.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_launch_new.xml diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_main.xml index f23c46455c63..553c7fe08096 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_main.xml @@ -1,19 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - Copyright 2021 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. ---> + ~ 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="match_parent" diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_notification.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_notification.xml new file mode 100644 index 000000000000..7c1198416334 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_notification.xml @@ -0,0 +1,30 @@ +<?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="match_parent" + android:orientation="vertical" + android:background="@android:color/black"> + + <Button + android:id="@+id/button_send_notification" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:text="Send Notification" /> +</RelativeLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml index 229098313afa..f7ba45b25d48 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml @@ -1,19 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - Copyright 2020 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. ---> + ~ 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:layout_width="match_parent" diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen.xml index 642a08b5bbe0..79e88e49b99e 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen.xml @@ -1,19 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - Copyright 2020 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. ---> + ~ 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:layout_width="match_parent" diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml index 23b51cc06f04..ed9feaf1eade 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml @@ -14,21 +14,19 @@ ~ 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:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="@android:color/holo_blue_light"> -<resources> - <style name="DefaultTheme" parent="@android:style/Theme.DeviceDefault"> - <item name="android:windowBackground">@android:color/darker_gray</item> - </style> + <TextView + android:id="@+id/SplitScreenTest" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:gravity="center_vertical|center_horizontal" + android:text="SecondaryActivity" + android:textAppearance="?android:attr/textAppearanceLarge"/> - <style name="CutoutDefault" parent="@style/DefaultTheme"> - <item name="android:windowLayoutInDisplayCutoutMode">default</item> - </style> - - <style name="CutoutShortEdges" parent="@style/DefaultTheme"> - <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> - </style> - - <style name="CutoutNever" parent="@style/DefaultTheme"> - <item name="android:windowLayoutInDisplayCutoutMode">never</item> - </style> -</resources>
\ No newline at end of file +</LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java index 166e3ca2a156..04a590daf32f 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java @@ -16,9 +16,6 @@ package com.android.server.wm.flicker.testapp; -import static com.android.server.wm.flicker.testapp.ActivityOptions.ACTIVITY_EMBEDDING_PLACEHOLDER_PRIMARY_ACTIVITY_COMPONENT_NAME; -import static com.android.server.wm.flicker.testapp.ActivityOptions.ACTIVITY_EMBEDDING_PLACEHOLDER_SECONDARY_ACTIVITY_COMPONENT_NAME; - import android.app.Activity; import android.content.Intent; import android.os.Bundle; @@ -39,8 +36,6 @@ public class ActivityEmbeddingMainActivity extends Activity { private static final String TAG = "ActivityEmbeddingMainActivity"; private static final float DEFAULT_SPLIT_RATIO = 0.5f; - private ActivityEmbeddingComponent mEmbeddingComponent; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -51,20 +46,24 @@ public class ActivityEmbeddingMainActivity extends Activity { /** R.id.launch_placeholder_split_button onClick */ public void launchPlaceholderSplit(View view) { - startActivity(new Intent().setComponent( - ACTIVITY_EMBEDDING_PLACEHOLDER_PRIMARY_ACTIVITY_COMPONENT_NAME)); + startActivity( + new Intent().setComponent( + ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT + ) + ); } private void initializeSplitRules() { - mEmbeddingComponent = ActivityEmbeddingAppHelper.getActivityEmbeddingComponent(); - if (mEmbeddingComponent == null) { + ActivityEmbeddingComponent embeddingComponent = + ActivityEmbeddingAppHelper.getActivityEmbeddingComponent(); + if (embeddingComponent == null) { // Embedding not supported Log.d(TAG, "ActivityEmbedding is not supported on this device"); finish(); return; } - mEmbeddingComponent.setEmbeddingRules(getSplitRules()); + embeddingComponent.setEmbeddingRules(getSplitRules()); } private Set<EmbeddingRule> getSplitRules() { @@ -72,10 +71,10 @@ public class ActivityEmbeddingMainActivity extends Activity { final SplitPlaceholderRule placeholderRule = new SplitPlaceholderRule.Builder( new Intent().setComponent( - ACTIVITY_EMBEDDING_PLACEHOLDER_SECONDARY_ACTIVITY_COMPONENT_NAME), + ActivityOptions.ActivityEmbedding.PlaceholderSecondaryActivity.COMPONENT), activity -> activity instanceof ActivityEmbeddingPlaceholderPrimaryActivity, intent -> intent.getComponent().equals( - ACTIVITY_EMBEDDING_PLACEHOLDER_PRIMARY_ACTIVITY_COMPONENT_NAME), + ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT), windowMetrics -> true) .setSplitRatio(DEFAULT_SPLIT_RATIO) .build(); diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java index cae3df4f9486..9583f972d872 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java @@ -19,94 +19,181 @@ package com.android.server.wm.flicker.testapp; import android.content.ComponentName; public class ActivityOptions { - public static final String EXTRA_STARVE_UI_THREAD = "StarveUiThread"; public static final String FLICKER_APP_PACKAGE = "com.android.server.wm.flicker.testapp"; - public static final String SEAMLESS_ACTIVITY_LAUNCHER_NAME = "SeamlessApp"; - public static final ComponentName SEAMLESS_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".SeamlessRotationActivity"); + public static class SimpleActivity { + public static final String LABEL = "SimpleActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".SimpleActivity"); + } - public static final String IME_ACTIVITY_AUTO_FOCUS_LAUNCHER_NAME = "ImeAppAutoFocus"; - public static final ComponentName IME_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".ImeActivityAutoFocus"); + public static class SeamlessRotation { + public static final String LABEL = "SeamlessRotationActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".SeamlessRotationActivity"); + + public static final String EXTRA_STARVE_UI_THREAD = "StarveUiThread"; + } - public static final String IME_ACTIVITY_LAUNCHER_NAME = "ImeActivity"; - public static final ComponentName IME_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, + public static class Ime { + public static class Default { + public static final String LABEL = "ImeActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".ImeActivity"); + } + + public static class AutoFocusActivity { + public static final String LABEL = "ImeAppAutoFocus"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".ImeActivityAutoFocus"); + } - public static final String IME_ACTIVITY_INITIALIZE_LAUNCHER_NAME = "ImeStateInitializeActivity"; - public static final ComponentName IME_ACTIVITY_INITIALIZE_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, + public static class StateInitializeActivity { + public static final String LABEL = "ImeStateInitializeActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".ImeStateInitializeActivity"); + } - public static final String SIMPLE_ACTIVITY_LAUNCHER_NAME = "SimpleApp"; - public static final ComponentName SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".SimpleActivity"); - - public static final String NON_RESIZEABLE_ACTIVITY_LAUNCHER_NAME = "NonResizeableApp"; - public static final ComponentName NON_RESIZEABLE_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".NonResizeableActivity"); - - public static final String BUTTON_ACTIVITY_LAUNCHER_NAME = "ButtonApp"; - public static final ComponentName BUTTON_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".ButtonActivity"); - - public static final String LAUNCH_NEW_TASK_ACTIVITY_LAUNCHER_NAME = "LaunchNewTaskApp"; - public static final ComponentName LAUNCH_NEW_TASK_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".LaunchNewTaskActivity"); - - public static final String DIALOG_THEMED_ACTIVITY = "DialogThemedActivity"; - public static final ComponentName DIALOG_THEMED_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".DialogThemedActivity"); - - public static final String PORTRAIT_ONLY_ACTIVITY_LAUNCHER_NAME = "PortraitOnlyActivity"; - public static final ComponentName PORTRAIT_ONLY_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".PortraitOnlyActivity"); - - public static final String EDITOR_POPUP_DIALOG_ACTIVITY_LAUNCHER_NAME = - "ImeEditorPopupDialogActivity"; - public static final ComponentName EDITOR_POPUP_DIALOG_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, + public static class EditorPopupDialogActivity { + public static final String LABEL = "ImeEditorPopupDialogActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".ImeEditorPopupDialogActivity"); - - public static final String SHOW_WHEN_LOCKED_ACTIVITY_LAUNCHER_NAME = "ShowWhenLockedApp"; - public static final ComponentName SHOW_WHEN_LOCKED_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".ShowWhenLockedActivity"); - - public static final String NOTIFICATION_ACTIVITY_LAUNCHER_NAME = "NotificationApp"; - public static final ComponentName NOTIFICATION_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".NotificationActivity"); - - public static final String ACTIVITY_EMBEDDING_LAUNCHER_NAME = "ActivityEmbeddingMainActivity"; - public static final ComponentName ACTIVITY_EMBEDDING_MAIN_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, + } + } + + public static class NonResizeableActivity { + public static final String LABEL = "NonResizeableActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".NonResizeableActivity"); + } + + public static class DialogThemedActivity { + public static final String LABEL = "DialogThemedActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".DialogThemedActivity"); + } + + public static class PortraitOnlyActivity { + public static final String LABEL = "PortraitOnlyActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".PortraitOnlyActivity"); + public static final String EXTRA_FIXED_ORIENTATION = "fixed_orientation"; + } + + public static class ActivityEmbedding { + public static class MainActivity { + public static final String LABEL = "ActivityEmbeddingMainActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".ActivityEmbeddingMainActivity"); - public static final ComponentName - ACTIVITY_EMBEDDING_PLACEHOLDER_PRIMARY_ACTIVITY_COMPONENT_NAME = new ComponentName( - FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".ActivityEmbeddingPlaceholderPrimaryActivity"); - public static final ComponentName - ACTIVITY_EMBEDDING_PLACEHOLDER_SECONDARY_ACTIVITY_COMPONENT_NAME = new ComponentName( - FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".ActivityEmbeddingPlaceholderSecondaryActivity"); - - public static final String MAIL_ACTIVITY_LAUNCHER_NAME = "MailActivity"; - public static final ComponentName MAIL_ACTIVITY_COMPONENT_NAME = new ComponentName( - FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".MailActivity"); - - public static final String GAME_ACTIVITY_LAUNCHER_NAME = "GameApp"; - public static final ComponentName GAME_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".GameActivity"); + } + + public static class PlaceholderPrimaryActivity { + public static final String LABEL = "ActivityEmbeddingPlaceholderPrimaryActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".ActivityEmbeddingPlaceholderPrimaryActivity"); + } + + public static class PlaceholderSecondaryActivity { + public static final String LABEL = "ActivityEmbeddingPlaceholderSecondaryActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".ActivityEmbeddingPlaceholderSecondaryActivity"); + } + } + + public static class Notification { + public static final String LABEL = "NotificationActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".NotificationActivity"); + } + + public static class Mail { + public static final String LABEL = "MailActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".MailActivity"); + } + + public static class ShowWhenLockedActivity { + public static final String LABEL = "ShowWhenLockedActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".ShowWhenLockedActivity"); + } + + public static class LaunchNewTask { + public static final String LABEL = "LaunchNewTaskActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".LaunchNewTaskActivity"); + } + + public static class Game { + public static final String LABEL = "GameActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".GameActivity"); + } + + public static class LaunchNewActivity { + public static final String LABEL = "LaunchNewActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".LaunchNewActivity"); + } + + public static class Pip { + // Test App > Pip Activity + public static final String LABEL = "PipActivity"; + public static final String MENU_ACTION_NO_OP = "No-Op"; + public static final String MENU_ACTION_ON = "On"; + public static final String MENU_ACTION_OFF = "Off"; + public static final String MENU_ACTION_CLEAR = "Clear"; + + // Intent action that this activity dynamically registers to enter picture-in-picture + public static final String ACTION_ENTER_PIP = + FLICKER_APP_PACKAGE + ".PipActivity.ENTER_PIP"; + // Intent action that this activity dynamically registers to set requested orientation. + // Will apply the oriention to the value set in the EXTRA_FIXED_ORIENTATION extra. + public static final String ACTION_SET_REQUESTED_ORIENTATION = + FLICKER_APP_PACKAGE + ".PipActivity.SET_REQUESTED_ORIENTATION"; + + // Calls enterPictureInPicture() on creation + public static final String EXTRA_ENTER_PIP = "enter_pip"; + // Sets the fixed orientation (can be one of {@link ActivityInfo.ScreenOrientation} + public static final String EXTRA_PIP_ORIENTATION = "fixed_orientation"; + // Adds a click listener to finish this activity when it is clicked + public static final String EXTRA_TAP_TO_FINISH = "tap_to_finish"; + + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".PipActivity"); + } + + public static class SplitScreen { + public static class Primary { + public static final String LABEL = "SplitScreenPrimaryActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".SplitScreenActivity"); + } + + public static class Secondary { + public static final String LABEL = "SplitScreenSecondaryActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".SplitScreenSecondaryActivity"); + } + } + + public static class SendNotificationActivity { + public static final String LABEL = "SendNotificationActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".SendNotificationActivity"); + } + + public static class Bubbles { + public static class LaunchBubble { + public static final String LABEL = "LaunchBubbleActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".LaunchBubbleActivity"); + } + + public static class BubbleActivity { + public static final String LABEL = "BubbleActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".BubbleActivity"); + } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BubbleActivity.java index bc3bc75ab903..58d7e670e908 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BubbleActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.server.wm.flicker.testapp; import android.app.Activity; diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BubbleHelper.java index 6cd93eff2803..c92b82b896f2 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BubbleHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.server.wm.flicker.testapp; import android.app.Notification; @@ -86,7 +86,7 @@ public class BubbleHelper { } - private int getNextNotifyId() { + private int getNextNotifyId() { int id = mNextNotifyId; mNextNotifyId++; return id; diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/FixedActivity.java index d4ae6c1313bf..722929fb81e5 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/FixedActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.server.wm.flicker.testapp; -import static com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION; +import static com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION; import android.os.Bundle; diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchBubbleActivity.java index 71fa66d8a61c..dea34442464d 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchBubbleActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.server.wm.flicker.testapp; import android.app.Activity; diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ButtonActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewActivity.java index b42ac2a6fd97..e5710c850f07 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ButtonActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewActivity.java @@ -22,7 +22,7 @@ import android.os.Bundle; import android.view.WindowManager; import android.widget.Button; -public class ButtonActivity extends Activity { +public class LaunchNewActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -30,11 +30,11 @@ public class ButtonActivity extends Activity { p.layoutInDisplayCutoutMode = WindowManager.LayoutParams .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; getWindow().setAttributes(p); - setContentView(R.layout.activity_button); + setContentView(R.layout.activity_launch_new); Button button = findViewById(R.id.launch_second_activity); button.setOnClickListener(v -> { - Intent intent = new Intent(ButtonActivity.this, SimpleActivity.class); + Intent intent = new Intent(LaunchNewActivity.this, SimpleActivity.class); startActivity(intent); }); } diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java index 615b1730579c..cdb1d42bd4f2 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.server.wm.flicker.testapp; import static android.media.MediaMetadata.METADATA_KEY_TITLE; import static android.media.session.PlaybackState.ACTION_PAUSE; @@ -24,10 +24,10 @@ import static android.media.session.PlaybackState.STATE_PAUSED; import static android.media.session.PlaybackState.STATE_PLAYING; import static android.media.session.PlaybackState.STATE_STOPPED; -import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP; -import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION; -import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP; -import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_PIP_ORIENTATION; +import static com.android.server.wm.flicker.testapp.ActivityOptions.Pip.ACTION_ENTER_PIP; +import static com.android.server.wm.flicker.testapp.ActivityOptions.Pip.ACTION_SET_REQUESTED_ORIENTATION; +import static com.android.server.wm.flicker.testapp.ActivityOptions.Pip.EXTRA_ENTER_PIP; +import static com.android.server.wm.flicker.testapp.ActivityOptions.Pip.EXTRA_PIP_ORIENTATION; import android.app.Activity; import android.app.PendingIntent; @@ -127,7 +127,6 @@ public class PipActivity extends Activity { break; default: Log.w(TAG, "Unhandled action=" + intent.getAction()); - return; } } } diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java index 5cf81cb90fbc..ce7a0059fa2d 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java @@ -18,7 +18,7 @@ package com.android.server.wm.flicker.testapp; import static android.os.SystemClock.sleep; -import static com.android.server.wm.flicker.testapp.ActivityOptions.EXTRA_STARVE_UI_THREAD; +import static com.android.server.wm.flicker.testapp.ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD; import android.app.Activity; import android.os.Bundle; diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SendNotificationActivity.java index 8020ef2270a0..8868488296be 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SendNotificationActivity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.server.wm.flicker.testapp; import android.app.Activity; import android.app.Notification; diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SplitScreenActivity.java index 9c82eea1e8b8..70196aee9ff1 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SplitScreenActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.server.wm.flicker.testapp; import android.app.Activity; import android.os.Bundle; diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SplitScreenSecondaryActivity.java index baa1e6fdd1e9..a8ce8ff8f589 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenSecondaryActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SplitScreenSecondaryActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.testapp; +package com.android.server.wm.flicker.testapp; import android.app.Activity; import android.os.Bundle; diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 54b3c400af4f..f924b2e9b932 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -23,6 +23,7 @@ import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE; import static android.net.vcn.VcnManager.VCN_STATUS_CODE_SAFE_MODE; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import static android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS; import static android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS; @@ -276,7 +277,6 @@ public class VcnManagementServiceTest { @Test public void testSystemReady() throws Exception { mVcnMgmtSvc.systemReady(); - mTestLooper.dispatchAll(); verify(mConnMgr).registerNetworkProvider(any(VcnNetworkProvider.class)); verify(mSubscriptionTracker).register(); @@ -494,8 +494,10 @@ public class VcnManagementServiceTest { mVcnMgmtSvc.addVcnUnderlyingNetworkPolicyListener(mMockPolicyListener); triggerSubscriptionTrackerCbAndGetSnapshot(null, Collections.emptySet()); - mTestLooper.dispatchAll(); + // Verify teardown after delay + mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); + mTestLooper.dispatchAll(); verify(vcn).teardownAsynchronously(); verify(mMockPolicyListener).onPolicyChanged(); } @@ -521,6 +523,92 @@ public class VcnManagementServiceTest { assertEquals(0, mVcnMgmtSvc.getAllVcns().size()); } + /** + * Tests an intermediate state where carrier privileges are marked as lost before active data + * subId changes during a SIM ejection. + * + * <p>The expected outcome is that the VCN is torn down after a delay, as opposed to + * immediately. + */ + @Test + public void testTelephonyNetworkTrackerCallbackLostCarrierPrivilegesBeforeActiveDataSubChanges() + throws Exception { + setupActiveSubscription(TEST_UUID_2); + + final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback(); + final Vcn vcn = startAndGetVcnInstance(TEST_UUID_2); + + // Simulate privileges lost + triggerSubscriptionTrackerCbAndGetSnapshot( + TEST_SUBSCRIPTION_ID, + TEST_UUID_2, + Collections.emptySet(), + Collections.emptyMap(), + false /* hasCarrierPrivileges */); + + // Verify teardown after delay + mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); + mTestLooper.dispatchAll(); + verify(vcn).teardownAsynchronously(); + } + + @Test + public void testTelephonyNetworkTrackerCallbackSimSwitchesDoNotKillVcnInstances() + throws Exception { + setupActiveSubscription(TEST_UUID_2); + + final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback(); + final Vcn vcn = startAndGetVcnInstance(TEST_UUID_2); + + // Simulate SIM unloaded + triggerSubscriptionTrackerCbAndGetSnapshot( + INVALID_SUBSCRIPTION_ID, + null /* activeDataSubscriptionGroup */, + Collections.emptySet(), + Collections.emptyMap(), + false /* hasCarrierPrivileges */); + + // Simulate new SIM loaded right during teardown delay. + mTestLooper.moveTimeForward( + VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS / 2); + mTestLooper.dispatchAll(); + triggerSubscriptionTrackerCbAndGetSnapshot(TEST_UUID_2, Collections.singleton(TEST_UUID_2)); + + // Verify that even after the full timeout duration, the VCN instance is not torn down + mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); + mTestLooper.dispatchAll(); + verify(vcn, never()).teardownAsynchronously(); + } + + @Test + public void testTelephonyNetworkTrackerCallbackDoesNotKillNewVcnInstances() throws Exception { + setupActiveSubscription(TEST_UUID_2); + + final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback(); + final Vcn oldInstance = startAndGetVcnInstance(TEST_UUID_2); + + // Simulate SIM unloaded + triggerSubscriptionTrackerCbAndGetSnapshot(null, Collections.emptySet()); + + // Config cleared, SIM reloaded & config re-added right before teardown delay, staring new + // vcnInstance. + mTestLooper.moveTimeForward( + VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS / 2); + mTestLooper.dispatchAll(); + mVcnMgmtSvc.clearVcnConfig(TEST_UUID_2, TEST_PACKAGE_NAME); + triggerSubscriptionTrackerCbAndGetSnapshot(TEST_UUID_2, Collections.singleton(TEST_UUID_2)); + final Vcn newInstance = startAndGetVcnInstance(TEST_UUID_2); + + // Verify that new instance was different, and the old one was torn down + assertTrue(oldInstance != newInstance); + verify(oldInstance).teardownAsynchronously(); + + // Verify that even after the full timeout duration, the new VCN instance is not torn down + mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); + mTestLooper.dispatchAll(); + verify(newInstance, never()).teardownAsynchronously(); + } + @Test public void testPackageChangeListenerRegistered() throws Exception { verify(mMockContext).registerReceiver(any(BroadcastReceiver.class), argThat(filter -> { @@ -910,8 +998,6 @@ public class VcnManagementServiceTest { private void setupSubscriptionAndStartVcn( int subId, ParcelUuid subGrp, boolean isVcnActive, boolean hasCarrierPrivileges) { mVcnMgmtSvc.systemReady(); - mTestLooper.dispatchAll(); - triggerSubscriptionTrackerCbAndGetSnapshot( subGrp, Collections.singleton(subGrp), @@ -1007,7 +1093,6 @@ public class VcnManagementServiceTest { private void setupTrackedCarrierWifiNetwork(NetworkCapabilities caps) { mVcnMgmtSvc.systemReady(); - mTestLooper.dispatchAll(); final ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class); @@ -1252,14 +1337,15 @@ public class VcnManagementServiceTest { true /* isActive */, true /* hasCarrierPrivileges */); - // VCN is currently active. Lose carrier privileges for TEST_PACKAGE so the VCN goes - // inactive. + // VCN is currently active. Lose carrier privileges for TEST_PACKAGE and hit teardown + // timeout so the VCN goes inactive. final TelephonySubscriptionSnapshot snapshot = triggerSubscriptionTrackerCbAndGetSnapshot( TEST_UUID_1, Collections.singleton(TEST_UUID_1), Collections.singletonMap(TEST_SUBSCRIPTION_ID, TEST_UUID_1), false /* hasCarrierPrivileges */); + mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); mTestLooper.dispatchAll(); // Giving TEST_PACKAGE privileges again will restart the VCN (which will indicate ACTIVE |