diff options
537 files changed, 16762 insertions, 6214 deletions
diff --git a/Android.bp b/Android.bp index d03128418eb4..cfab18eeb089 100644 --- a/Android.bp +++ b/Android.bp @@ -110,6 +110,7 @@ filegroup { ":android.security.maintenance-java-source", ":android.security.metrics-java-source", ":android.system.keystore2-V3-java-source", + ":android.hardware.cas-V1-java-source", ":credstore_aidl", ":dumpstate_aidl", ":framework_native_aidl", @@ -200,6 +201,7 @@ java_library { "updatable-driver-protos", "ota_metadata_proto_java", "android.hidl.base-V1.0-java", + "android.hardware.cas-V1-java", // AIDL "android.hardware.cas-V1.0-java", "android.hardware.cas-V1.1-java", "android.hardware.cas-V1.2-java", @@ -692,3 +694,12 @@ build = [ "ProtoLibraries.bp", "TestProtoLibraries.bp", ] + +java_api_contribution { + name: "api-stubs-docs-non-updatable-public-stubs", + api_surface: "public", + api_file: "core/api/current.txt", + visibility: [ + "//build/orchestrator/apis", + ], +} diff --git a/StubLibraries.bp b/StubLibraries.bp index 272b4f6e36e6..fc046fb2486f 100644 --- a/StubLibraries.bp +++ b/StubLibraries.bp @@ -378,6 +378,67 @@ java_library { }, } +java_library { + name: "android_stubs_private_jar", + defaults: ["android.jar_defaults"], + visibility: [ + "//visibility:override", + "//visibility:private", + ], + static_libs: [ + "stable.core.platform.api.stubs", + "core-lambda-stubs-for-system-modules", + "core-generated-annotation-stubs", + "framework", + "ext", + "framework-res-package-jar", + // The order of this matters, it has to be last to provide a + // package-private androidx.annotation.RecentlyNonNull without + // overriding the public android.annotation.Nullable in framework.jar + // with its own package-private android.annotation.Nullable. + "private-stub-annotations-jar", + ], +} + +java_genrule { + name: "android_stubs_private_hjar", + visibility: ["//visibility:private"], + srcs: [":android_stubs_private_jar{.hjar}"], + out: ["android_stubs_private.jar"], + cmd: "cp $(in) $(out)", +} + +java_library { + name: "android_stubs_private", + defaults: ["android_stubs_dists_default"], + visibility: ["//visibility:private"], + sdk_version: "none", + system_modules: "none", + static_libs: ["android_stubs_private_hjar"], + dist: { + dir: "apistubs/android/private", + }, +} + +java_genrule { + name: "android_stubs_private_framework_aidl", + visibility: ["//visibility:private"], + tools: ["sdkparcelables"], + srcs: [":android_stubs_private"], + out: ["framework.aidl"], + cmd: "rm -f $(genDir)/framework.aidl.merged && " + + "for i in $(in); do " + + " rm -f $(genDir)/framework.aidl.tmp && " + + " $(location sdkparcelables) $$i $(genDir)/framework.aidl.tmp && " + + " cat $(genDir)/framework.aidl.tmp >> $(genDir)/framework.aidl.merged; " + + "done && " + + "sort -u $(genDir)/framework.aidl.merged > $(out)", + dist: { + targets: ["sdk"], + dir: "apistubs/android/private", + }, +} + //////////////////////////////////////////////////////////////////////// // api-versions.xml generation, for public and system. This API database // also contains the android.test.* APIs. @@ -537,3 +598,12 @@ java_library { ], visibility: ["//visibility:public"], } + +java_api_contribution { + name: "frameworks-base-core-api-module-lib-stubs", + api_surface: "module-lib", + api_file: "core/api/module-lib-current.txt", + visibility: [ + "//build/orchestrator/apis", + ], +} diff --git a/apct-tests/perftests/packagemanager/AndroidManifest.xml b/apct-tests/perftests/packagemanager/AndroidManifest.xml index 3b9431f1b97a..b4a34b89d383 100644 --- a/apct-tests/perftests/packagemanager/AndroidManifest.xml +++ b/apct-tests/perftests/packagemanager/AndroidManifest.xml @@ -26,6 +26,7 @@ <permission android:name="com.android.perftests.packagemanager.TestPermission" /> <uses-permission android:name="com.android.perftests.packagemanager.TestPermission" /> + <uses-permission android:name="android.permission.GET_APP_METADATA" /> <queries> <package android:name="com.android.perftests.appenumeration0" /> diff --git a/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java b/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java index 673c6a39f4b3..4bcc8c499f0d 100644 --- a/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java +++ b/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java @@ -19,10 +19,15 @@ package android.os; import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; +import static org.junit.Assert.assertEquals; + +import android.Manifest; import android.compat.testing.PlatformCompatChangeRule; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; @@ -31,11 +36,19 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.compatibility.common.util.AdoptShellPermissionsRule; +import com.android.cts.install.lib.Install; +import com.android.cts.install.lib.InstallUtils; +import com.android.cts.install.lib.LocalIntentSender; +import com.android.cts.install.lib.TestApp; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; + @RunWith(AndroidJUnit4.class) @LargeTest public class PackageManagerPerfTest { @@ -46,6 +59,8 @@ public class PackageManagerPerfTest { private static final ComponentName TEST_ACTIVITY = new ComponentName("com.android.perftests.packagemanager", "android.perftests.utils.PerfTestActivity"); + private static final String TEST_FIELD = "test"; + private static final String TEST_VALUE = "value"; @Rule public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -53,9 +68,39 @@ public class PackageManagerPerfTest { @Rule public final PlatformCompatChangeRule mPlatformCompatChangeRule = new PlatformCompatChangeRule(); + @Rule + public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule( + InstrumentationRegistry.getInstrumentation().getUiAutomation(), + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES); + + final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private PackageInstaller mPackageInstaller; public PackageManagerPerfTest() throws PackageManager.NameNotFoundException { - final Context context = InstrumentationRegistry.getInstrumentation().getContext(); + mPackageInstaller = mContext.getPackageManager().getPackageInstaller(); + } + + private void installTestApp(TestApp testApp) throws IOException, InterruptedException { + Install install = Install.single(testApp); + final int expectedSessionId = install.createSession(); + PackageInstaller.Session session = + InstallUtils.openPackageInstallerSession(expectedSessionId); + PersistableBundle bundle = new PersistableBundle(); + bundle.putString(TEST_FIELD, TEST_VALUE); + session.setAppMetadata(bundle); + LocalIntentSender localIntentSender = new LocalIntentSender(); + session.commit(localIntentSender.getIntentSender()); + Intent intent = localIntentSender.getResult(); + InstallUtils.assertStatusSuccess(intent); + } + + private void uninstallTestApp(String packageName) throws InterruptedException { + LocalIntentSender localIntentSender = new LocalIntentSender(); + IntentSender intentSender = localIntentSender.getIntentSender(); + mPackageInstaller.uninstall(packageName, intentSender); + Intent intent = localIntentSender.getResult(); + InstallUtils.assertStatusSuccess(intent); } @Before @@ -65,6 +110,24 @@ public class PackageManagerPerfTest { } @Test + public void testGetAppMetadata() throws Exception { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + installTestApp(TestApp.A1); + final PackageManager pm = + InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager(); + final String packageName = TestApp.A1.getPackageName(); + + while (state.keepRunning()) { + PersistableBundle bundle = pm.getAppMetadata(packageName); + state.pauseTiming(); + assertEquals(bundle.size(), 1); + assertEquals(bundle.getString(TEST_FIELD), TEST_VALUE); + state.resumeTiming(); + } + uninstallTestApp(packageName); + } + + @Test @DisableCompatChanges(PackageManager.FILTER_APPLICATION_QUERY) public void testCheckPermissionExists() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index b806ef88f2d7..e0e0b4bd62e2 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -17,6 +17,7 @@ package com.android.server.job; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static android.util.DataUnit.GIGABYTES; import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; @@ -58,6 +59,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IBatteryStats; import com.android.internal.app.procstats.ProcessStats; +import com.android.internal.util.MemInfoReader; import com.android.internal.util.StatLogger; import com.android.server.JobSchedulerBackgroundThread; import com.android.server.LocalServices; @@ -85,11 +87,33 @@ class JobConcurrencyManager { private static final boolean DEBUG = JobSchedulerService.DEBUG; /** The maximum number of concurrent jobs we'll aim to run at one time. */ - public static final int STANDARD_CONCURRENCY_LIMIT = 16; + @VisibleForTesting + static final int MAX_CONCURRENCY_LIMIT = 64; /** The maximum number of objects we should retain in memory when not in use. */ - private static final int MAX_RETAINED_OBJECTS = (int) (1.5 * STANDARD_CONCURRENCY_LIMIT); + private static final int MAX_RETAINED_OBJECTS = (int) (1.5 * MAX_CONCURRENCY_LIMIT); static final String CONFIG_KEY_PREFIX_CONCURRENCY = "concurrency_"; + private static final String KEY_CONCURRENCY_LIMIT = CONFIG_KEY_PREFIX_CONCURRENCY + "limit"; + @VisibleForTesting + static final int DEFAULT_CONCURRENCY_LIMIT; + + static { + if (ActivityManager.isLowRamDeviceStatic()) { + DEFAULT_CONCURRENCY_LIMIT = 8; + } else { + final long ramBytes = new MemInfoReader().getTotalSize(); + if (ramBytes <= GIGABYTES.toBytes(6)) { + DEFAULT_CONCURRENCY_LIMIT = 16; + } else if (ramBytes <= GIGABYTES.toBytes(8)) { + DEFAULT_CONCURRENCY_LIMIT = 20; + } else if (ramBytes <= GIGABYTES.toBytes(12)) { + DEFAULT_CONCURRENCY_LIMIT = 32; + } else { + DEFAULT_CONCURRENCY_LIMIT = 40; + } + } + } + private static final String KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS = CONFIG_KEY_PREFIX_CONCURRENCY + "screen_off_adjustment_delay_ms"; private static final long DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS = 30_000; @@ -100,7 +124,7 @@ class JobConcurrencyManager { @VisibleForTesting static final String KEY_PKG_CONCURRENCY_LIMIT_REGULAR = CONFIG_KEY_PREFIX_CONCURRENCY + "pkg_concurrency_limit_regular"; - private static final int DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR = STANDARD_CONCURRENCY_LIMIT / 2; + private static final int DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR = DEFAULT_CONCURRENCY_LIMIT / 2; @VisibleForTesting static final String KEY_ENABLE_MAX_WAIT_TIME_BYPASS = CONFIG_KEY_PREFIX_CONCURRENCY + "enable_max_wait_time_bypass"; @@ -209,84 +233,100 @@ class JobConcurrencyManager { private static final WorkConfigLimitsPerMemoryTrimLevel CONFIG_LIMITS_SCREEN_ON = new WorkConfigLimitsPerMemoryTrimLevel( - new WorkTypeConfig("screen_on_normal", 11, + new WorkTypeConfig("screen_on_normal", DEFAULT_CONCURRENCY_LIMIT, + /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 3 / 4, // defaultMin - List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), - Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)), + List.of(Pair.create(WORK_TYPE_TOP, .4f), + Pair.create(WORK_TYPE_FGS, .2f), + Pair.create(WORK_TYPE_EJ, .2f), Pair.create(WORK_TYPE_BG, .1f), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f)), // defaultMax - List.of(Pair.create(WORK_TYPE_BG, 6), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 2), - Pair.create(WORK_TYPE_BGUSER, 3)) + List.of(Pair.create(WORK_TYPE_BG, .5f), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .25f), + Pair.create(WORK_TYPE_BGUSER, .2f)) ), - new WorkTypeConfig("screen_on_moderate", 9, + new WorkTypeConfig("screen_on_moderate", DEFAULT_CONCURRENCY_LIMIT, + /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT / 2, // defaultMin - List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), - Pair.create(WORK_TYPE_EJ, 2), Pair.create(WORK_TYPE_BG, 1), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)), + List.of(Pair.create(WORK_TYPE_TOP, .4f), + Pair.create(WORK_TYPE_FGS, .1f), + Pair.create(WORK_TYPE_EJ, .1f), Pair.create(WORK_TYPE_BG, .1f), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f)), // defaultMax - List.of(Pair.create(WORK_TYPE_BG, 4), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), - Pair.create(WORK_TYPE_BGUSER, 1)) + List.of(Pair.create(WORK_TYPE_BG, .4f), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f), + Pair.create(WORK_TYPE_BGUSER, .1f)) ), - new WorkTypeConfig("screen_on_low", 6, + new WorkTypeConfig("screen_on_low", DEFAULT_CONCURRENCY_LIMIT, + /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 4 / 10, // defaultMin - List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), - Pair.create(WORK_TYPE_EJ, 1)), + List.of(Pair.create(WORK_TYPE_TOP, 2.0f / 3), + Pair.create(WORK_TYPE_FGS, .1f), + Pair.create(WORK_TYPE_EJ, .1f)), // defaultMax - List.of(Pair.create(WORK_TYPE_BG, 2), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), - Pair.create(WORK_TYPE_BGUSER, 1)) + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1.0f / 6), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 6)) ), - new WorkTypeConfig("screen_on_critical", 6, + new WorkTypeConfig("screen_on_critical", DEFAULT_CONCURRENCY_LIMIT, + /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 4 / 10, // defaultMin - List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), - Pair.create(WORK_TYPE_EJ, 1)), + List.of(Pair.create(WORK_TYPE_TOP, 2.0f / 3), + Pair.create(WORK_TYPE_FGS, .1f), + Pair.create(WORK_TYPE_EJ, .1f)), // defaultMax - List.of(Pair.create(WORK_TYPE_BG, 1), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), - Pair.create(WORK_TYPE_BGUSER, 1)) + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 6), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1.0f / 6), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 6)) ) ); private static final WorkConfigLimitsPerMemoryTrimLevel CONFIG_LIMITS_SCREEN_OFF = new WorkConfigLimitsPerMemoryTrimLevel( - new WorkTypeConfig("screen_off_normal", 16, + new WorkTypeConfig("screen_off_normal", DEFAULT_CONCURRENCY_LIMIT, + /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT, // defaultMin - List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 2), - Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)), + List.of(Pair.create(WORK_TYPE_TOP, .3f), + Pair.create(WORK_TYPE_FGS, .2f), + Pair.create(WORK_TYPE_EJ, .3f), Pair.create(WORK_TYPE_BG, .2f), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f)), // defaultMax - List.of(Pair.create(WORK_TYPE_BG, 10), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 2), - Pair.create(WORK_TYPE_BGUSER, 3)) + List.of(Pair.create(WORK_TYPE_BG, .6f), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .2f), + Pair.create(WORK_TYPE_BGUSER, .2f)) ), - new WorkTypeConfig("screen_off_moderate", 14, + new WorkTypeConfig("screen_off_moderate", DEFAULT_CONCURRENCY_LIMIT, + /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 9 / 10, // defaultMin - List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 2), - Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)), + List.of(Pair.create(WORK_TYPE_TOP, .3f), + Pair.create(WORK_TYPE_FGS, .2f), + Pair.create(WORK_TYPE_EJ, .3f), Pair.create(WORK_TYPE_BG, .2f), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f)), // defaultMax - List.of(Pair.create(WORK_TYPE_BG, 7), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), - Pair.create(WORK_TYPE_BGUSER, 1)) + List.of(Pair.create(WORK_TYPE_BG, .5f), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f), + Pair.create(WORK_TYPE_BGUSER, .1f)) ), - new WorkTypeConfig("screen_off_low", 9, + new WorkTypeConfig("screen_off_low", DEFAULT_CONCURRENCY_LIMIT, + /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 6 / 10, // defaultMin - List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), - Pair.create(WORK_TYPE_EJ, 2), Pair.create(WORK_TYPE_BG, 1)), + List.of(Pair.create(WORK_TYPE_TOP, .4f), + Pair.create(WORK_TYPE_FGS, .1f), + Pair.create(WORK_TYPE_EJ, .2f), Pair.create(WORK_TYPE_BG, .1f)), // defaultMax - List.of(Pair.create(WORK_TYPE_BG, 3), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), - Pair.create(WORK_TYPE_BGUSER, 1)) + List.of(Pair.create(WORK_TYPE_BG, .25f), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f), + Pair.create(WORK_TYPE_BGUSER, .1f)) ), - new WorkTypeConfig("screen_off_critical", 6, + new WorkTypeConfig("screen_off_critical", DEFAULT_CONCURRENCY_LIMIT, + /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 4 / 10, // defaultMin - List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1), - Pair.create(WORK_TYPE_EJ, 1)), + List.of(Pair.create(WORK_TYPE_TOP, .5f), + Pair.create(WORK_TYPE_FGS, .1f), + Pair.create(WORK_TYPE_EJ, .1f)), // defaultMax - List.of(Pair.create(WORK_TYPE_BG, 1), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), - Pair.create(WORK_TYPE_BGUSER, 1)) + List.of(Pair.create(WORK_TYPE_BG, .1f), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f), + Pair.create(WORK_TYPE_BGUSER, .1f)) ) ); @@ -358,6 +398,12 @@ class JobConcurrencyManager { private long mScreenOffAdjustmentDelayMs = DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS; /** + * The maximum number of jobs we'll attempt to have running at one time. This may occasionally + * be exceeded based on other factors. + */ + private int mSteadyStateConcurrencyLimit = DEFAULT_CONCURRENCY_LIMIT; + + /** * The maximum number of expedited jobs a single userId-package can have running simultaneously. * TOP apps are not limited. */ @@ -451,7 +497,7 @@ class JobConcurrencyManager { void onThirdPartyAppsCanStart() { final IBatteryStats batteryStats = IBatteryStats.Stub.asInterface( ServiceManager.getService(BatteryStats.SERVICE_NAME)); - for (int i = 0; i < STANDARD_CONCURRENCY_LIMIT; i++) { + for (int i = 0; i < mSteadyStateConcurrencyLimit; ++i) { mIdleContexts.add( mInjector.createJobServiceContext(mService, this, mNotificationCoordinator, batteryStats, @@ -778,13 +824,14 @@ class JobConcurrencyManager { } preferredUidOnly.sort(sDeterminationComparator); stoppable.sort(sDeterminationComparator); - for (int i = numRunningJobs; i < STANDARD_CONCURRENCY_LIMIT; ++i) { + for (int i = numRunningJobs; i < mSteadyStateConcurrencyLimit; ++i) { final JobServiceContext jsc; final int numIdleContexts = mIdleContexts.size(); if (numIdleContexts > 0) { jsc = mIdleContexts.removeAt(numIdleContexts - 1); } else { - Slog.wtf(TAG, "Had fewer than " + STANDARD_CONCURRENCY_LIMIT + " in existence"); + // This could happen if the config is changed at runtime. + Slog.w(TAG, "Had fewer than " + mSteadyStateConcurrencyLimit + " in existence"); jsc = createNewJobServiceContext(); } @@ -850,7 +897,7 @@ class JobConcurrencyManager { ContextAssignment selectedContext = null; final int allWorkTypes = getJobWorkTypes(nextPending); final boolean pkgConcurrencyOkay = !isPkgConcurrencyLimitedLocked(nextPending); - final boolean isInOverage = projectedRunningCount > STANDARD_CONCURRENCY_LIMIT; + final boolean isInOverage = projectedRunningCount > mSteadyStateConcurrencyLimit; boolean startingJob = false; if (idle.size() > 0) { final int idx = idle.size() - 1; @@ -1398,7 +1445,7 @@ class JobConcurrencyManager { noteConcurrency(); return; } - if (mActiveServices.size() >= STANDARD_CONCURRENCY_LIMIT) { + if (mActiveServices.size() >= mSteadyStateConcurrencyLimit) { final boolean respectConcurrencyLimit; if (!mMaxWaitTimeBypassEnabled) { respectConcurrencyLimit = true; @@ -1801,23 +1848,27 @@ class JobConcurrencyManager { DeviceConfig.Properties properties = DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER); + // Concurrency limit should be in the range [8, MAX_CONCURRENCY_LIMIT]. + mSteadyStateConcurrencyLimit = Math.max(8, Math.min(MAX_CONCURRENCY_LIMIT, + properties.getInt(KEY_CONCURRENCY_LIMIT, DEFAULT_CONCURRENCY_LIMIT))); + mScreenOffAdjustmentDelayMs = properties.getLong( KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS, DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS); - CONFIG_LIMITS_SCREEN_ON.normal.update(properties); - CONFIG_LIMITS_SCREEN_ON.moderate.update(properties); - CONFIG_LIMITS_SCREEN_ON.low.update(properties); - CONFIG_LIMITS_SCREEN_ON.critical.update(properties); + CONFIG_LIMITS_SCREEN_ON.normal.update(properties, mSteadyStateConcurrencyLimit); + CONFIG_LIMITS_SCREEN_ON.moderate.update(properties, mSteadyStateConcurrencyLimit); + CONFIG_LIMITS_SCREEN_ON.low.update(properties, mSteadyStateConcurrencyLimit); + CONFIG_LIMITS_SCREEN_ON.critical.update(properties, mSteadyStateConcurrencyLimit); - CONFIG_LIMITS_SCREEN_OFF.normal.update(properties); - CONFIG_LIMITS_SCREEN_OFF.moderate.update(properties); - CONFIG_LIMITS_SCREEN_OFF.low.update(properties); - CONFIG_LIMITS_SCREEN_OFF.critical.update(properties); + CONFIG_LIMITS_SCREEN_OFF.normal.update(properties, mSteadyStateConcurrencyLimit); + CONFIG_LIMITS_SCREEN_OFF.moderate.update(properties, mSteadyStateConcurrencyLimit); + CONFIG_LIMITS_SCREEN_OFF.low.update(properties, mSteadyStateConcurrencyLimit); + CONFIG_LIMITS_SCREEN_OFF.critical.update(properties, mSteadyStateConcurrencyLimit); - // Package concurrency limits must in the range [1, STANDARD_CONCURRENCY_LIMIT]. - mPkgConcurrencyLimitEj = Math.max(1, Math.min(STANDARD_CONCURRENCY_LIMIT, + // Package concurrency limits must in the range [1, mSteadyStateConcurrencyLimit]. + mPkgConcurrencyLimitEj = Math.max(1, Math.min(mSteadyStateConcurrencyLimit, properties.getInt(KEY_PKG_CONCURRENCY_LIMIT_EJ, DEFAULT_PKG_CONCURRENCY_LIMIT_EJ))); - mPkgConcurrencyLimitRegular = Math.max(1, Math.min(STANDARD_CONCURRENCY_LIMIT, + mPkgConcurrencyLimitRegular = Math.max(1, Math.min(mSteadyStateConcurrencyLimit, properties.getInt( KEY_PKG_CONCURRENCY_LIMIT_REGULAR, DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR))); @@ -1838,6 +1889,7 @@ class JobConcurrencyManager { try { pw.println("Configuration:"); pw.increaseIndent(); + pw.print(KEY_CONCURRENCY_LIMIT, mSteadyStateConcurrencyLimit).println(); pw.print(KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS, mScreenOffAdjustmentDelayMs).println(); pw.print(KEY_PKG_CONCURRENCY_LIMIT_EJ, mPkgConcurrencyLimitEj).println(); pw.print(KEY_PKG_CONCURRENCY_LIMIT_REGULAR, mPkgConcurrencyLimitRegular).println(); @@ -2041,130 +2093,181 @@ class JobConcurrencyManager { @VisibleForTesting static class WorkTypeConfig { + private static final String KEY_PREFIX_MAX = CONFIG_KEY_PREFIX_CONCURRENCY + "max_"; + private static final String KEY_PREFIX_MIN = CONFIG_KEY_PREFIX_CONCURRENCY + "min_"; @VisibleForTesting - static final String KEY_PREFIX_MAX = CONFIG_KEY_PREFIX_CONCURRENCY + "max_"; + static final String KEY_PREFIX_MAX_TOTAL = CONFIG_KEY_PREFIX_CONCURRENCY + "max_total_"; @VisibleForTesting - static final String KEY_PREFIX_MIN = CONFIG_KEY_PREFIX_CONCURRENCY + "min_"; + static final String KEY_PREFIX_MAX_RATIO = KEY_PREFIX_MAX + "ratio_"; + private static final String KEY_PREFIX_MAX_RATIO_TOP = KEY_PREFIX_MAX_RATIO + "top_"; + private static final String KEY_PREFIX_MAX_RATIO_FGS = KEY_PREFIX_MAX_RATIO + "fgs_"; + private static final String KEY_PREFIX_MAX_RATIO_EJ = KEY_PREFIX_MAX_RATIO + "ej_"; + private static final String KEY_PREFIX_MAX_RATIO_BG = KEY_PREFIX_MAX_RATIO + "bg_"; + private static final String KEY_PREFIX_MAX_RATIO_BGUSER = KEY_PREFIX_MAX_RATIO + "bguser_"; + private static final String KEY_PREFIX_MAX_RATIO_BGUSER_IMPORTANT = + KEY_PREFIX_MAX_RATIO + "bguser_important_"; @VisibleForTesting - static final String KEY_PREFIX_MAX_TOTAL = CONFIG_KEY_PREFIX_CONCURRENCY + "max_total_"; - private static final String KEY_PREFIX_MAX_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "max_top_"; - private static final String KEY_PREFIX_MAX_FGS = CONFIG_KEY_PREFIX_CONCURRENCY + "max_fgs_"; - private static final String KEY_PREFIX_MAX_EJ = CONFIG_KEY_PREFIX_CONCURRENCY + "max_ej_"; - private static final String KEY_PREFIX_MAX_BG = CONFIG_KEY_PREFIX_CONCURRENCY + "max_bg_"; - private static final String KEY_PREFIX_MAX_BGUSER = - CONFIG_KEY_PREFIX_CONCURRENCY + "max_bguser_"; - private static final String KEY_PREFIX_MAX_BGUSER_IMPORTANT = - CONFIG_KEY_PREFIX_CONCURRENCY + "max_bguser_important_"; - private static final String KEY_PREFIX_MIN_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "min_top_"; - private static final String KEY_PREFIX_MIN_FGS = CONFIG_KEY_PREFIX_CONCURRENCY + "min_fgs_"; - private static final String KEY_PREFIX_MIN_EJ = CONFIG_KEY_PREFIX_CONCURRENCY + "min_ej_"; - private static final String KEY_PREFIX_MIN_BG = CONFIG_KEY_PREFIX_CONCURRENCY + "min_bg_"; - private static final String KEY_PREFIX_MIN_BGUSER = - CONFIG_KEY_PREFIX_CONCURRENCY + "min_bguser_"; - private static final String KEY_PREFIX_MIN_BGUSER_IMPORTANT = - CONFIG_KEY_PREFIX_CONCURRENCY + "min_bguser_important_"; + static final String KEY_PREFIX_MIN_RATIO = KEY_PREFIX_MIN + "ratio_"; + private static final String KEY_PREFIX_MIN_RATIO_TOP = KEY_PREFIX_MIN_RATIO + "top_"; + private static final String KEY_PREFIX_MIN_RATIO_FGS = KEY_PREFIX_MIN_RATIO + "fgs_"; + private static final String KEY_PREFIX_MIN_RATIO_EJ = KEY_PREFIX_MIN_RATIO + "ej_"; + private static final String KEY_PREFIX_MIN_RATIO_BG = KEY_PREFIX_MIN_RATIO + "bg_"; + private static final String KEY_PREFIX_MIN_RATIO_BGUSER = KEY_PREFIX_MIN_RATIO + "bguser_"; + private static final String KEY_PREFIX_MIN_RATIO_BGUSER_IMPORTANT = + KEY_PREFIX_MIN_RATIO + "bguser_important_"; private final String mConfigIdentifier; private int mMaxTotal; private final SparseIntArray mMinReservedSlots = new SparseIntArray(NUM_WORK_TYPES); private final SparseIntArray mMaxAllowedSlots = new SparseIntArray(NUM_WORK_TYPES); private final int mDefaultMaxTotal; - private final SparseIntArray mDefaultMinReservedSlots = new SparseIntArray(NUM_WORK_TYPES); - private final SparseIntArray mDefaultMaxAllowedSlots = new SparseIntArray(NUM_WORK_TYPES); - - WorkTypeConfig(@NonNull String configIdentifier, int defaultMaxTotal, - List<Pair<Integer, Integer>> defaultMin, List<Pair<Integer, Integer>> defaultMax) { + // We use SparseIntArrays to store floats because there is currently no SparseFloatArray + // available, and it doesn't seem worth it to add such a data structure just for this + // use case. We don't use SparseDoubleArrays because DeviceConfig only supports floats and + // converting between floats and ints is more straightforward than floats and doubles. + private final SparseIntArray mDefaultMinReservedSlotsRatio = + new SparseIntArray(NUM_WORK_TYPES); + private final SparseIntArray mDefaultMaxAllowedSlotsRatio = + new SparseIntArray(NUM_WORK_TYPES); + + WorkTypeConfig(@NonNull String configIdentifier, + int steadyStateConcurrencyLimit, int defaultMaxTotal, + List<Pair<Integer, Float>> defaultMinRatio, + List<Pair<Integer, Float>> defaultMaxRatio) { mConfigIdentifier = configIdentifier; - mDefaultMaxTotal = mMaxTotal = Math.min(defaultMaxTotal, STANDARD_CONCURRENCY_LIMIT); + mDefaultMaxTotal = mMaxTotal = Math.min(defaultMaxTotal, steadyStateConcurrencyLimit); int numReserved = 0; - for (int i = defaultMin.size() - 1; i >= 0; --i) { - mDefaultMinReservedSlots.put(defaultMin.get(i).first, defaultMin.get(i).second); - numReserved += defaultMin.get(i).second; + for (int i = defaultMinRatio.size() - 1; i >= 0; --i) { + final float ratio = defaultMinRatio.get(i).second; + final int wt = defaultMinRatio.get(i).first; + if (ratio < 0 || 1 <= ratio) { + // 1 means to reserve everything. This shouldn't be allowed. + // We only create new configs on boot, so this should trigger during development + // (before the code gets checked in), so this makes sure the hard-coded defaults + // make sense. DeviceConfig values will be handled gracefully in update(). + throw new IllegalArgumentException("Invalid default min ratio: wt=" + wt + + " minRatio=" + ratio); + } + mDefaultMinReservedSlotsRatio.put(wt, Float.floatToRawIntBits(ratio)); + numReserved += mMaxTotal * ratio; } if (mDefaultMaxTotal < 0 || numReserved > mDefaultMaxTotal) { // We only create new configs on boot, so this should trigger during development // (before the code gets checked in), so this makes sure the hard-coded defaults // make sense. DeviceConfig values will be handled gracefully in update(). throw new IllegalArgumentException("Invalid default config: t=" + defaultMaxTotal - + " min=" + defaultMin + " max=" + defaultMax); - } - for (int i = defaultMax.size() - 1; i >= 0; --i) { - mDefaultMaxAllowedSlots.put(defaultMax.get(i).first, defaultMax.get(i).second); + + " min=" + defaultMinRatio + " max=" + defaultMaxRatio); + } + for (int i = defaultMaxRatio.size() - 1; i >= 0; --i) { + final float ratio = defaultMaxRatio.get(i).second; + final int wt = defaultMaxRatio.get(i).first; + final float minRatio = + Float.intBitsToFloat(mDefaultMinReservedSlotsRatio.get(wt, 0)); + if (ratio < minRatio || ratio <= 0) { + // Max ratio shouldn't be <= 0 or less than minRatio. + throw new IllegalArgumentException("Invalid default config:" + + " t=" + defaultMaxTotal + + " min=" + defaultMinRatio + " max=" + defaultMaxRatio); + } + mDefaultMaxAllowedSlotsRatio.put(wt, Float.floatToRawIntBits(ratio)); } update(new DeviceConfig.Properties.Builder( - DeviceConfig.NAMESPACE_JOB_SCHEDULER).build()); + DeviceConfig.NAMESPACE_JOB_SCHEDULER).build(), steadyStateConcurrencyLimit); } - void update(@NonNull DeviceConfig.Properties properties) { - // Ensure total in the range [1, STANDARD_CONCURRENCY_LIMIT]. - mMaxTotal = Math.max(1, Math.min(STANDARD_CONCURRENCY_LIMIT, + void update(@NonNull DeviceConfig.Properties properties, int steadyStateConcurrencyLimit) { + // Ensure total in the range [1, mSteadyStateConcurrencyLimit]. + mMaxTotal = Math.max(1, Math.min(steadyStateConcurrencyLimit, properties.getInt(KEY_PREFIX_MAX_TOTAL + mConfigIdentifier, mDefaultMaxTotal))); + final int oneIntBits = Float.floatToIntBits(1); + mMaxAllowedSlots.clear(); // Ensure they're in the range [1, total]. - final int maxTop = Math.max(1, Math.min(mMaxTotal, - properties.getInt(KEY_PREFIX_MAX_TOP + mConfigIdentifier, - mDefaultMaxAllowedSlots.get(WORK_TYPE_TOP, mMaxTotal)))); + final int maxTop = getMaxValue(properties, + KEY_PREFIX_MAX_RATIO_TOP + mConfigIdentifier, WORK_TYPE_TOP, oneIntBits); mMaxAllowedSlots.put(WORK_TYPE_TOP, maxTop); - final int maxFgs = Math.max(1, Math.min(mMaxTotal, - properties.getInt(KEY_PREFIX_MAX_FGS + mConfigIdentifier, - mDefaultMaxAllowedSlots.get(WORK_TYPE_FGS, mMaxTotal)))); + final int maxFgs = getMaxValue(properties, + KEY_PREFIX_MAX_RATIO_FGS + mConfigIdentifier, WORK_TYPE_FGS, oneIntBits); mMaxAllowedSlots.put(WORK_TYPE_FGS, maxFgs); - final int maxEj = Math.max(1, Math.min(mMaxTotal, - properties.getInt(KEY_PREFIX_MAX_EJ + mConfigIdentifier, - mDefaultMaxAllowedSlots.get(WORK_TYPE_EJ, mMaxTotal)))); + final int maxEj = getMaxValue(properties, + KEY_PREFIX_MAX_RATIO_EJ + mConfigIdentifier, WORK_TYPE_EJ, oneIntBits); mMaxAllowedSlots.put(WORK_TYPE_EJ, maxEj); - final int maxBg = Math.max(1, Math.min(mMaxTotal, - properties.getInt(KEY_PREFIX_MAX_BG + mConfigIdentifier, - mDefaultMaxAllowedSlots.get(WORK_TYPE_BG, mMaxTotal)))); + final int maxBg = getMaxValue(properties, + KEY_PREFIX_MAX_RATIO_BG + mConfigIdentifier, WORK_TYPE_BG, oneIntBits); mMaxAllowedSlots.put(WORK_TYPE_BG, maxBg); - final int maxBgUserImp = Math.max(1, Math.min(mMaxTotal, - properties.getInt(KEY_PREFIX_MAX_BGUSER_IMPORTANT + mConfigIdentifier, - mDefaultMaxAllowedSlots.get(WORK_TYPE_BGUSER_IMPORTANT, mMaxTotal)))); + final int maxBgUserImp = getMaxValue(properties, + KEY_PREFIX_MAX_RATIO_BGUSER_IMPORTANT + mConfigIdentifier, + WORK_TYPE_BGUSER_IMPORTANT, oneIntBits); mMaxAllowedSlots.put(WORK_TYPE_BGUSER_IMPORTANT, maxBgUserImp); - final int maxBgUser = Math.max(1, Math.min(mMaxTotal, - properties.getInt(KEY_PREFIX_MAX_BGUSER + mConfigIdentifier, - mDefaultMaxAllowedSlots.get(WORK_TYPE_BGUSER, mMaxTotal)))); + final int maxBgUser = getMaxValue(properties, + KEY_PREFIX_MAX_RATIO_BGUSER + mConfigIdentifier, WORK_TYPE_BGUSER, oneIntBits); mMaxAllowedSlots.put(WORK_TYPE_BGUSER, maxBgUser); int remaining = mMaxTotal; mMinReservedSlots.clear(); // Ensure top is in the range [1, min(maxTop, total)] - final int minTop = Math.max(1, Math.min(Math.min(maxTop, mMaxTotal), - properties.getInt(KEY_PREFIX_MIN_TOP + mConfigIdentifier, - mDefaultMinReservedSlots.get(WORK_TYPE_TOP)))); + final int minTop = getMinValue(properties, + KEY_PREFIX_MIN_RATIO_TOP + mConfigIdentifier, WORK_TYPE_TOP, + 1, Math.min(maxTop, mMaxTotal)); mMinReservedSlots.put(WORK_TYPE_TOP, minTop); remaining -= minTop; // Ensure fgs is in the range [0, min(maxFgs, remaining)] - final int minFgs = Math.max(0, Math.min(Math.min(maxFgs, remaining), - properties.getInt(KEY_PREFIX_MIN_FGS + mConfigIdentifier, - mDefaultMinReservedSlots.get(WORK_TYPE_FGS)))); + final int minFgs = getMinValue(properties, + KEY_PREFIX_MIN_RATIO_FGS + mConfigIdentifier, WORK_TYPE_FGS, + 0, Math.min(maxFgs, remaining)); mMinReservedSlots.put(WORK_TYPE_FGS, minFgs); remaining -= minFgs; // Ensure ej is in the range [0, min(maxEj, remaining)] - final int minEj = Math.max(0, Math.min(Math.min(maxEj, remaining), - properties.getInt(KEY_PREFIX_MIN_EJ + mConfigIdentifier, - mDefaultMinReservedSlots.get(WORK_TYPE_EJ)))); + final int minEj = getMinValue(properties, + KEY_PREFIX_MIN_RATIO_EJ + mConfigIdentifier, WORK_TYPE_EJ, + 0, Math.min(maxEj, remaining)); mMinReservedSlots.put(WORK_TYPE_EJ, minEj); remaining -= minEj; // Ensure bg is in the range [0, min(maxBg, remaining)] - final int minBg = Math.max(0, Math.min(Math.min(maxBg, remaining), - properties.getInt(KEY_PREFIX_MIN_BG + mConfigIdentifier, - mDefaultMinReservedSlots.get(WORK_TYPE_BG)))); + final int minBg = getMinValue(properties, + KEY_PREFIX_MIN_RATIO_BG + mConfigIdentifier, WORK_TYPE_BG, + 0, Math.min(maxBg, remaining)); mMinReservedSlots.put(WORK_TYPE_BG, minBg); remaining -= minBg; // Ensure bg user imp is in the range [0, min(maxBgUserImp, remaining)] - final int minBgUserImp = Math.max(0, Math.min(Math.min(maxBgUserImp, remaining), - properties.getInt(KEY_PREFIX_MIN_BGUSER_IMPORTANT + mConfigIdentifier, - mDefaultMinReservedSlots.get(WORK_TYPE_BGUSER_IMPORTANT, 0)))); + final int minBgUserImp = getMinValue(properties, + KEY_PREFIX_MIN_RATIO_BGUSER_IMPORTANT + mConfigIdentifier, + WORK_TYPE_BGUSER_IMPORTANT, 0, Math.min(maxBgUserImp, remaining)); mMinReservedSlots.put(WORK_TYPE_BGUSER_IMPORTANT, minBgUserImp); + remaining -= minBgUserImp; // Ensure bg user is in the range [0, min(maxBgUser, remaining)] - final int minBgUser = Math.max(0, Math.min(Math.min(maxBgUser, remaining), - properties.getInt(KEY_PREFIX_MIN_BGUSER + mConfigIdentifier, - mDefaultMinReservedSlots.get(WORK_TYPE_BGUSER, 0)))); + final int minBgUser = getMinValue(properties, + KEY_PREFIX_MIN_RATIO_BGUSER + mConfigIdentifier, WORK_TYPE_BGUSER, + 0, Math.min(maxBgUser, remaining)); mMinReservedSlots.put(WORK_TYPE_BGUSER, minBgUser); } + /** + * Return the calculated max value for the work type. + * @param defaultFloatInIntBits A {@code float} value in int bits representation (using + * {@link Float#floatToIntBits(float)}. + */ + private int getMaxValue(@NonNull DeviceConfig.Properties properties, @NonNull String key, + int workType, int defaultFloatInIntBits) { + final float maxRatio = Math.min(1, properties.getFloat(key, + Float.intBitsToFloat( + mDefaultMaxAllowedSlotsRatio.get(workType, defaultFloatInIntBits)))); + // Max values should be in the range [1, total]. + return Math.max(1, (int) (mMaxTotal * maxRatio)); + } + + /** + * Return the calculated min value for the work type. + */ + private int getMinValue(@NonNull DeviceConfig.Properties properties, @NonNull String key, + int workType, int lowerLimit, int upperLimit) { + final float minRatio = Math.min(1, + properties.getFloat(key, + Float.intBitsToFloat(mDefaultMinReservedSlotsRatio.get(workType)))); + return Math.max(lowerLimit, Math.min(upperLimit, (int) (mMaxTotal * minRatio))); + } + int getMaxTotal() { return mMaxTotal; } @@ -2179,29 +2282,37 @@ class JobConcurrencyManager { void dump(IndentingPrintWriter pw) { pw.print(KEY_PREFIX_MAX_TOTAL + mConfigIdentifier, mMaxTotal).println(); - pw.print(KEY_PREFIX_MIN_TOP + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_TOP)) + pw.print(KEY_PREFIX_MIN_RATIO_TOP + mConfigIdentifier, + mMinReservedSlots.get(WORK_TYPE_TOP)) .println(); - pw.print(KEY_PREFIX_MAX_TOP + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_TOP)) + pw.print(KEY_PREFIX_MAX_RATIO_TOP + mConfigIdentifier, + mMaxAllowedSlots.get(WORK_TYPE_TOP)) .println(); - pw.print(KEY_PREFIX_MIN_FGS + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_FGS)) + pw.print(KEY_PREFIX_MIN_RATIO_FGS + mConfigIdentifier, + mMinReservedSlots.get(WORK_TYPE_FGS)) .println(); - pw.print(KEY_PREFIX_MAX_FGS + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_FGS)) + pw.print(KEY_PREFIX_MAX_RATIO_FGS + mConfigIdentifier, + mMaxAllowedSlots.get(WORK_TYPE_FGS)) .println(); - pw.print(KEY_PREFIX_MIN_EJ + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_EJ)) + pw.print(KEY_PREFIX_MIN_RATIO_EJ + mConfigIdentifier, + mMinReservedSlots.get(WORK_TYPE_EJ)) .println(); - pw.print(KEY_PREFIX_MAX_EJ + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_EJ)) + pw.print(KEY_PREFIX_MAX_RATIO_EJ + mConfigIdentifier, + mMaxAllowedSlots.get(WORK_TYPE_EJ)) .println(); - pw.print(KEY_PREFIX_MIN_BG + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_BG)) + pw.print(KEY_PREFIX_MIN_RATIO_BG + mConfigIdentifier, + mMinReservedSlots.get(WORK_TYPE_BG)) .println(); - pw.print(KEY_PREFIX_MAX_BG + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_BG)) + pw.print(KEY_PREFIX_MAX_RATIO_BG + mConfigIdentifier, + mMaxAllowedSlots.get(WORK_TYPE_BG)) .println(); - pw.print(KEY_PREFIX_MIN_BGUSER + mConfigIdentifier, + pw.print(KEY_PREFIX_MIN_RATIO_BGUSER + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_BGUSER_IMPORTANT)).println(); - pw.print(KEY_PREFIX_MAX_BGUSER + mConfigIdentifier, + pw.print(KEY_PREFIX_MAX_RATIO_BGUSER + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_BGUSER_IMPORTANT)).println(); - pw.print(KEY_PREFIX_MIN_BGUSER + mConfigIdentifier, + pw.print(KEY_PREFIX_MIN_RATIO_BGUSER + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_BGUSER)).println(); - pw.print(KEY_PREFIX_MAX_BGUSER + mConfigIdentifier, + pw.print(KEY_PREFIX_MAX_RATIO_BGUSER + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_BGUSER)).println(); } } diff --git a/core/api/current.txt b/core/api/current.txt index 1b54cc9cc973..c509c97cd29a 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -88,6 +88,7 @@ package android { field public static final String DIAGNOSTIC = "android.permission.DIAGNOSTIC"; field public static final String DISABLE_KEYGUARD = "android.permission.DISABLE_KEYGUARD"; field public static final String DUMP = "android.permission.DUMP"; + field public static final String ENFORCE_UPDATE_OWNERSHIP = "android.permission.ENFORCE_UPDATE_OWNERSHIP"; field public static final String EXPAND_STATUS_BAR = "android.permission.EXPAND_STATUS_BAR"; field public static final String FACTORY_TEST = "android.permission.FACTORY_TEST"; field public static final String FOREGROUND_SERVICE = "android.permission.FOREGROUND_SERVICE"; @@ -384,6 +385,7 @@ package android { field public static final int allowTaskReparenting = 16843268; // 0x1010204 field public static final int allowUndo = 16843999; // 0x10104df field public static final int allowUntrustedActivityEmbedding = 16844393; // 0x1010669 + field public static final int allowUpdateOwnership; field public static final int alpha = 16843551; // 0x101031f field public static final int alphabeticModifiers = 16844110; // 0x101054e field public static final int alphabeticShortcut = 16843235; // 0x10101e3 @@ -5903,7 +5905,7 @@ package android.app { ctor public LocaleConfig(@NonNull android.content.Context); ctor public LocaleConfig(@NonNull android.os.LocaleList); method public int describeContents(); - method @NonNull public static android.app.LocaleConfig fromResources(@NonNull android.content.Context); + method @NonNull public static android.app.LocaleConfig fromContextIgnoringOverride(@NonNull android.content.Context); method public int getStatus(); method @Nullable public android.os.LocaleList getSupportedLocales(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -9229,15 +9231,18 @@ package android.companion { } public final class CompanionDeviceManager { + method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public void addOnAssociationsChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.CompanionDeviceManager.OnAssociationsChangedListener); method @RequiresPermission(anyOf={android.Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH, android.Manifest.permission.REQUEST_COMPANION_PROFILE_COMPUTER, android.Manifest.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING, android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION}, conditional=true) public void associate(@NonNull android.companion.AssociationRequest, @NonNull android.companion.CompanionDeviceManager.Callback, @Nullable android.os.Handler); method @RequiresPermission(anyOf={android.Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH, android.Manifest.permission.REQUEST_COMPANION_PROFILE_COMPUTER, android.Manifest.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING, android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION}, conditional=true) public void associate(@NonNull android.companion.AssociationRequest, @NonNull java.util.concurrent.Executor, @NonNull android.companion.CompanionDeviceManager.Callback); method @Nullable public android.content.IntentSender buildAssociationCancellationIntent(); method @Nullable public android.content.IntentSender buildPermissionTransferUserConsentIntent(int) throws android.companion.DeviceNotAssociatedException; method @Deprecated public void disassociate(@NonNull String); method public void disassociate(int); + method @NonNull @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public java.util.List<android.companion.AssociationInfo> getAllAssociations(); method @Deprecated @NonNull public java.util.List<java.lang.String> getAssociations(); method @NonNull public java.util.List<android.companion.AssociationInfo> getMyAssociations(); method @Deprecated public boolean hasNotificationAccess(android.content.ComponentName); + method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public void removeOnAssociationsChangedListener(@NonNull android.companion.CompanionDeviceManager.OnAssociationsChangedListener); method public void requestNotificationAccess(android.content.ComponentName); method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException; method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException; @@ -9258,6 +9263,10 @@ package android.companion { method public abstract void onFailure(@Nullable CharSequence); } + public static interface CompanionDeviceManager.OnAssociationsChangedListener { + method public void onAssociationsChanged(@NonNull java.util.List<android.companion.AssociationInfo>); + } + public abstract class CompanionDeviceService extends android.app.Service { ctor public CompanionDeviceService(); method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent); @@ -11691,6 +11700,7 @@ package android.content.pm { method @Nullable public String getInstallingPackageName(); method @Nullable public String getOriginatingPackageName(); method public int getPackageSource(); + method @Nullable public String getUpdateOwnerPackageName(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.InstallSourceInfo> CREATOR; } @@ -11873,6 +11883,7 @@ package android.content.pm { public class PackageInstaller { method public void abandonSession(int); method public void checkInstallConstraints(@NonNull java.util.List<java.lang.String>, @NonNull android.content.pm.PackageInstaller.InstallConstraints, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.content.pm.PackageInstaller.InstallConstraintsResult>); + method public void commitSessionAfterInstallConstraintsAreMet(int, @NonNull android.content.IntentSender, @NonNull android.content.pm.PackageInstaller.InstallConstraints, long); method public int createSession(@NonNull android.content.pm.PackageInstaller.SessionParams) throws java.io.IOException; method @Deprecated @Nullable public android.content.pm.PackageInstaller.SessionInfo getActiveStagedSession(); method @NonNull public java.util.List<android.content.pm.PackageInstaller.SessionInfo> getActiveStagedSessions(); @@ -11917,6 +11928,7 @@ package android.content.pm { field public static final int STATUS_FAILURE_INCOMPATIBLE = 7; // 0x7 field public static final int STATUS_FAILURE_INVALID = 4; // 0x4 field public static final int STATUS_FAILURE_STORAGE = 6; // 0x6 + field public static final int STATUS_FAILURE_TIMEOUT = 8; // 0x8 field public static final int STATUS_PENDING_USER_ACTION = -1; // 0xffffffff field public static final int STATUS_SUCCESS = 0; // 0x0 } @@ -11981,6 +11993,7 @@ package android.content.pm { method public int getParentSessionId(); method public boolean isKeepApplicationEnabledSetting(); method public boolean isMultiPackage(); + method public boolean isRequestUpdateOwnership(); method public boolean isStaged(); method @NonNull public java.io.InputStream openRead(@NonNull String) throws java.io.IOException; method @NonNull public java.io.OutputStream openWrite(@NonNull String, long, long) throws java.io.IOException; @@ -12036,6 +12049,7 @@ package android.content.pm { method public boolean isCommitted(); method public boolean isKeepApplicationEnabledSetting(); method public boolean isMultiPackage(); + method public boolean isRequestUpdateOwnership(); method public boolean isSealed(); method public boolean isStaged(); method public boolean isStagedSessionActive(); @@ -12074,6 +12088,7 @@ package android.content.pm { method public void setOriginatingUri(@Nullable android.net.Uri); method public void setPackageSource(int); method public void setReferrerUri(@Nullable android.net.Uri); + method @RequiresPermission(android.Manifest.permission.ENFORCE_UPDATE_OWNERSHIP) public void setRequestUpdateOwnership(boolean); method public void setRequireUserAction(int); method public void setSize(long); method public void setWhitelistedRestrictedPermissions(@Nullable java.util.Set<java.lang.String>); @@ -12252,6 +12267,7 @@ package android.content.pm { method @NonNull public java.util.List<android.content.pm.PackageManager.Property> queryProviderProperty(@NonNull String); method @NonNull public java.util.List<android.content.pm.PackageManager.Property> queryReceiverProperty(@NonNull String); method @NonNull public java.util.List<android.content.pm.PackageManager.Property> queryServiceProperty(@NonNull String); + method public void relinquishUpdateOwnership(@NonNull String); method @Deprecated public abstract void removePackageFromPreferred(@NonNull String); method public abstract void removePermission(@NonNull String); method @RequiresPermission(value="android.permission.WHITELIST_RESTRICTED_PERMISSIONS", conditional=true) public boolean removeWhitelistedRestrictedPermission(@NonNull String, @NonNull String, int); @@ -12686,7 +12702,7 @@ package android.content.pm { field public static final int FLAG_USE_APP_ZYGOTE = 8; // 0x8 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10 - field @Deprecated @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 + field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 field @RequiresPermission(android.Manifest.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT) public static final int FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT = 4096; // 0x1000 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8 @@ -13251,6 +13267,7 @@ package android.credentials { ctor public ClearCredentialStateException(@NonNull String, @Nullable Throwable); ctor public ClearCredentialStateException(@NonNull String); method @NonNull public String getType(); + field @NonNull public static final String TYPE_UNKNOWN = "android.credentials.ClearCredentialStateException.TYPE_UNKNOWN"; } public final class ClearCredentialStateRequest implements android.os.Parcelable { @@ -13267,7 +13284,10 @@ package android.credentials { ctor public CreateCredentialException(@NonNull String, @Nullable Throwable); ctor public CreateCredentialException(@NonNull String); method @NonNull public String getType(); + field @NonNull public static final String TYPE_INTERRUPTED = "android.credentials.CreateCredentialException.TYPE_INTERRUPTED"; field @NonNull public static final String TYPE_NO_CREDENTIAL = "android.credentials.CreateCredentialException.TYPE_NO_CREDENTIAL"; + field @NonNull public static final String TYPE_UNKNOWN = "android.credentials.CreateCredentialException.TYPE_UNKNOWN"; + field @NonNull public static final String TYPE_USER_CANCELED = "android.credentials.CreateCredentialException.TYPE_USER_CANCELED"; } public final class CreateCredentialRequest implements android.os.Parcelable { @@ -13311,7 +13331,10 @@ package android.credentials { ctor public GetCredentialException(@NonNull String, @Nullable Throwable); ctor public GetCredentialException(@NonNull String); method @NonNull public String getType(); + field @NonNull public static final String TYPE_INTERRUPTED = "android.credentials.GetCredentialException.TYPE_INTERRUPTED"; field @NonNull public static final String TYPE_NO_CREDENTIAL = "android.credentials.GetCredentialException.TYPE_NO_CREDENTIAL"; + field @NonNull public static final String TYPE_UNKNOWN = "android.credentials.GetCredentialException.TYPE_UNKNOWN"; + field @NonNull public static final String TYPE_USER_CANCELED = "android.credentials.GetCredentialException.TYPE_USER_CANCELED"; } public final class GetCredentialOption implements android.os.Parcelable { @@ -18206,7 +18229,7 @@ package android.hardware.camera2 { method public int capture(@NonNull android.hardware.camera2.CaptureRequest, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback) throws android.hardware.camera2.CameraAccessException; method public void close() throws android.hardware.camera2.CameraAccessException; method @NonNull public android.hardware.camera2.CameraDevice getDevice(); - method @Nullable public android.util.Pair<java.lang.Long,java.lang.Long> getRealtimeStillCaptureLatency() throws android.hardware.camera2.CameraAccessException; + method @Nullable public android.hardware.camera2.CameraExtensionSession.StillCaptureLatency getRealtimeStillCaptureLatency() throws android.hardware.camera2.CameraAccessException; method public int setRepeatingRequest(@NonNull android.hardware.camera2.CaptureRequest, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback) throws android.hardware.camera2.CameraAccessException; method public void stopRepeating() throws android.hardware.camera2.CameraAccessException; } @@ -18229,6 +18252,12 @@ package android.hardware.camera2 { method public abstract void onConfigured(@NonNull android.hardware.camera2.CameraExtensionSession); } + public static final class CameraExtensionSession.StillCaptureLatency { + ctor public CameraExtensionSession.StillCaptureLatency(long, long); + method public long getCaptureLatency(); + method public long getProcessingLatency(); + } + public final class CameraManager { method @NonNull public android.hardware.camera2.CameraCharacteristics getCameraCharacteristics(@NonNull String) throws android.hardware.camera2.CameraAccessException; method @NonNull public android.hardware.camera2.CameraExtensionCharacteristics getCameraExtensionCharacteristics(@NonNull String) throws android.hardware.camera2.CameraAccessException; @@ -21959,6 +21988,7 @@ package android.media { field public static final int PLUGIN_STATUS_PHYSICAL_MODULE_CHANGED = 0; // 0x0 field public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = 1; // 0x1 field public static final int SCRAMBLING_MODE_AES128 = 9; // 0x9 + field public static final int SCRAMBLING_MODE_AES_CBC = 14; // 0xe field public static final int SCRAMBLING_MODE_AES_ECB = 10; // 0xa field public static final int SCRAMBLING_MODE_AES_SCTE52 = 11; // 0xb field public static final int SCRAMBLING_MODE_DVB_CISSA_V1 = 6; // 0x6 @@ -33197,6 +33227,7 @@ package android.os { field public static final String DISALLOW_CONFIG_CELL_BROADCASTS = "no_config_cell_broadcasts"; field public static final String DISALLOW_CONFIG_CREDENTIALS = "no_config_credentials"; field public static final String DISALLOW_CONFIG_DATE_TIME = "no_config_date_time"; + field public static final String DISALLOW_CONFIG_DEFAULT_APPS = "disallow_config_default_apps"; field public static final String DISALLOW_CONFIG_LOCALE = "no_config_locale"; field public static final String DISALLOW_CONFIG_LOCATION = "no_config_location"; field public static final String DISALLOW_CONFIG_MOBILE_NETWORKS = "no_config_mobile_networks"; @@ -39923,6 +39954,7 @@ package android.service.credentials { method @NonNull public final android.os.IBinder onBind(@NonNull android.content.Intent); method public abstract void onClearCredentialState(@NonNull android.service.credentials.ClearCredentialStateRequest, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException>); field public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; + field public static final String EXTRA_BEGIN_GET_CREDENTIAL_REQUEST = "android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST"; field public static final String EXTRA_CREATE_CREDENTIAL_EXCEPTION = "android.service.credentials.extra.CREATE_CREDENTIAL_EXCEPTION"; field public static final String EXTRA_CREATE_CREDENTIAL_REQUEST = "android.service.credentials.extra.CREATE_CREDENTIAL_REQUEST"; field public static final String EXTRA_CREATE_CREDENTIAL_RESPONSE = "android.service.credentials.extra.CREATE_CREDENTIAL_RESPONSE"; @@ -46619,8 +46651,8 @@ package android.text { field public static final int DONE = -1; // 0xffffffff } - public static class SegmentFinder.DefaultSegmentFinder extends android.text.SegmentFinder { - ctor public SegmentFinder.DefaultSegmentFinder(@NonNull int[]); + public static class SegmentFinder.PrescribedSegmentFinder extends android.text.SegmentFinder { + ctor public SegmentFinder.PrescribedSegmentFinder(@NonNull int[]); method public int nextEndBoundary(@IntRange(from=0) int); method public int nextStartBoundary(@IntRange(from=0) int); method public int previousEndBoundary(@IntRange(from=0) int); @@ -53772,6 +53804,7 @@ package android.view.accessibility { method public int getDisplayId(); method public int getId(); method public int getLayer(); + method @NonNull public android.os.LocaleList getLocales(); method public android.view.accessibility.AccessibilityWindowInfo getParent(); method public void getRegionInScreen(@NonNull android.graphics.Region); method public android.view.accessibility.AccessibilityNodeInfo getRoot(); @@ -54581,14 +54614,6 @@ package android.view.inputmethod { public abstract class HandwritingGesture { method @Nullable public final String getFallbackText(); - field public static final int GESTURE_TYPE_DELETE = 4; // 0x4 - field public static final int GESTURE_TYPE_DELETE_RANGE = 64; // 0x40 - field public static final int GESTURE_TYPE_INSERT = 2; // 0x2 - field public static final int GESTURE_TYPE_JOIN_OR_SPLIT = 16; // 0x10 - field public static final int GESTURE_TYPE_NONE = 0; // 0x0 - field public static final int GESTURE_TYPE_REMOVE_SPACE = 8; // 0x8 - field public static final int GESTURE_TYPE_SELECT = 1; // 0x1 - field public static final int GESTURE_TYPE_SELECT_RANGE = 32; // 0x20 field public static final int GRANULARITY_CHARACTER = 2; // 0x2 field public static final int GRANULARITY_WORD = 1; // 0x1 } @@ -55088,15 +55113,15 @@ package android.view.inputmethod { public final class TextBoundsInfo implements android.os.Parcelable { method public int describeContents(); method @IntRange(from=0, to=125) public int getCharacterBidiLevel(int); - method @NonNull public android.graphics.RectF getCharacterBounds(int); + method @NonNull public void getCharacterBounds(int, @NonNull android.graphics.RectF); method public int getCharacterFlags(int); - method public int getEnd(); + method public int getEndIndex(); method @NonNull public android.text.SegmentFinder getGraphemeSegmentFinder(); method @NonNull public android.text.SegmentFinder getLineSegmentFinder(); - method @NonNull public android.graphics.Matrix getMatrix(); + method @NonNull public void getMatrix(@NonNull android.graphics.Matrix); method public int getOffsetForPosition(float, float); method @Nullable public int[] getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy); - method public int getStart(); + method public int getStartIndex(); method @NonNull public android.text.SegmentFinder getWordSegmentFinder(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.TextBoundsInfo> CREATOR; @@ -55107,7 +55132,7 @@ package android.view.inputmethod { } public static final class TextBoundsInfo.Builder { - ctor public TextBoundsInfo.Builder(); + ctor public TextBoundsInfo.Builder(int, int); method @NonNull public android.view.inputmethod.TextBoundsInfo build(); method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder clear(); method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setCharacterBidiLevel(@NonNull int[]); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 3216bd12118b..55ef6de47aee 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -82,6 +82,7 @@ package android.content { } public abstract class Context { + method @NonNull public android.content.Context createContextForSdkInSandbox(@NonNull android.content.pm.ApplicationInfo, int) throws android.content.pm.PackageManager.NameNotFoundException; method @NonNull public android.os.IBinder getIApplicationThreadBinder(); method @NonNull public android.os.UserHandle getUser(); field public static final String PAC_PROXY_SERVICE = "pac_proxy"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 137751b17508..85f88135510b 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -806,10 +806,12 @@ package android.app { method @Nullable public android.content.IntentFilter getDeliveryGroupMatchingFilter(); method @Nullable public String getDeliveryGroupMatchingKey(); method public int getDeliveryGroupPolicy(); + method public boolean isDeferUntilActive(); method public boolean isPendingIntentBackgroundActivityLaunchAllowed(); method public static android.app.BroadcastOptions makeBasic(); method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS) public void recordResponseEventWhileInBackground(@IntRange(from=0) long); method @RequiresPermission(android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND) public void setBackgroundActivityStartsAllowed(boolean); + method @NonNull public android.app.BroadcastOptions setDeferUntilActive(boolean); method @NonNull public android.app.BroadcastOptions setDeliveryGroupMatchingFilter(@NonNull android.content.IntentFilter); method @NonNull public android.app.BroadcastOptions setDeliveryGroupMatchingKey(@NonNull String, @NonNull String); method @NonNull public android.app.BroadcastOptions setDeliveryGroupPolicy(int); @@ -2973,18 +2975,11 @@ package android.companion { } public final class CompanionDeviceManager { - method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public void addOnAssociationsChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.CompanionDeviceManager.OnAssociationsChangedListener); method @RequiresPermission(android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES) public void associate(@NonNull String, @NonNull android.net.MacAddress, @NonNull byte[]); method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public boolean canPairWithoutPrompt(@NonNull String, @NonNull String, @NonNull android.os.UserHandle); - method @NonNull @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public java.util.List<android.companion.AssociationInfo> getAllAssociations(); method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public boolean isDeviceAssociatedForWifiConnection(@NonNull String, @NonNull android.net.MacAddress, @NonNull android.os.UserHandle); method @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceAppeared(int); method @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceDisappeared(int); - method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public void removeOnAssociationsChangedListener(@NonNull android.companion.CompanionDeviceManager.OnAssociationsChangedListener); - } - - public static interface CompanionDeviceManager.OnAssociationsChangedListener { - method public void onAssociationsChanged(@NonNull java.util.List<android.companion.AssociationInfo>); } } @@ -3592,6 +3587,9 @@ package android.content.pm { field public static final int LOCATION_DATA_APP = 0; // 0x0 field public static final int LOCATION_MEDIA_DATA = 2; // 0x2 field public static final int LOCATION_MEDIA_OBB = 1; // 0x1 + field public static final int REASON_CONFIRM_PACKAGE_CHANGE = 0; // 0x0 + field public static final int REASON_OWNERSHIP_CHANGED = 1; // 0x1 + field public static final int REASON_REMIND_OWNERSHIP = 2; // 0x2 } public static class PackageInstaller.InstallInfo { @@ -3621,6 +3619,7 @@ package android.content.pm { method public boolean getInstallAsFullApp(boolean); method public boolean getInstallAsInstantApp(boolean); method public boolean getInstallAsVirtualPreload(); + method public int getPendingUserActionReason(); method public boolean getRequestDowngrade(); method public int getRollbackDataPolicy(); method @NonNull public java.util.Set<java.lang.String> getWhitelistedRestrictedPermissions(); @@ -11769,6 +11768,7 @@ package android.service.euicc { method public int onEraseSubscriptions(int, @android.telephony.euicc.EuiccCardManager.ResetOption int); method public abstract android.service.euicc.GetDefaultDownloadableSubscriptionListResult onGetDefaultDownloadableSubscriptionList(int, boolean); method public abstract android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, android.telephony.euicc.DownloadableSubscription, boolean); + method @NonNull public android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean); method public abstract String onGetEid(int); method @NonNull public abstract android.telephony.euicc.EuiccInfo onGetEuiccInfo(int); method @NonNull public abstract android.service.euicc.GetEuiccProfileInfoListResult onGetEuiccProfileInfoList(int); @@ -16531,6 +16531,7 @@ package android.view.accessibility { public abstract class AccessibilityDisplayProxy { ctor public AccessibilityDisplayProxy(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.List<android.accessibilityservice.AccessibilityServiceInfo>); + method @Nullable public android.view.accessibility.AccessibilityNodeInfo findFocus(int); method public int getDisplayId(); method @NonNull public final java.util.List<android.accessibilityservice.AccessibilityServiceInfo> getInstalledAndEnabledServices(); method @NonNull public java.util.List<android.view.accessibility.AccessibilityWindowInfo> getWindows(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index e3554a5aa043..f7d1fdaadd37 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1246,7 +1246,7 @@ package android.hardware.camera2 { public final class CameraManager { method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; - field public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; // 0xef10e60L + field public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; // 0xef10e60L } public abstract static class CameraManager.AvailabilityCallback { @@ -3322,6 +3322,14 @@ package android.view.inputmethod { public abstract class HandwritingGesture { method public final int getGestureType(); + field public static final int GESTURE_TYPE_DELETE = 4; // 0x4 + field public static final int GESTURE_TYPE_DELETE_RANGE = 64; // 0x40 + field public static final int GESTURE_TYPE_INSERT = 2; // 0x2 + field public static final int GESTURE_TYPE_JOIN_OR_SPLIT = 16; // 0x10 + field public static final int GESTURE_TYPE_NONE = 0; // 0x0 + field public static final int GESTURE_TYPE_REMOVE_SPACE = 8; // 0x8 + field public static final int GESTURE_TYPE_SELECT = 1; // 0x1 + field public static final int GESTURE_TYPE_SELECT_RANGE = 32; // 0x20 } public final class InlineSuggestion implements android.os.Parcelable { @@ -3662,22 +3670,22 @@ package android.window { public final class WindowContainerTransaction implements android.os.Parcelable { ctor public WindowContainerTransaction(); + method @NonNull public android.window.WindowContainerTransaction clearAdjacentTaskFragments(@NonNull android.os.IBinder); method @NonNull public android.window.WindowContainerTransaction clearLaunchAdjacentFlagRoot(@NonNull android.window.WindowContainerToken); method @NonNull public android.window.WindowContainerTransaction createTaskFragment(@NonNull android.window.TaskFragmentCreationParams); - method @NonNull public android.window.WindowContainerTransaction deleteTaskFragment(@NonNull android.window.WindowContainerToken); + method @NonNull public android.window.WindowContainerTransaction deleteTaskFragment(@NonNull android.os.IBinder); method public int describeContents(); method @NonNull public android.window.WindowContainerTransaction finishActivity(@NonNull android.os.IBinder); method @NonNull public android.window.WindowContainerTransaction removeTask(@NonNull android.window.WindowContainerToken); method @NonNull public android.window.WindowContainerTransaction reorder(@NonNull android.window.WindowContainerToken, boolean); method @NonNull public android.window.WindowContainerTransaction reparent(@NonNull android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken, boolean); method @NonNull public android.window.WindowContainerTransaction reparentActivityToTaskFragment(@NonNull android.os.IBinder, @NonNull android.os.IBinder); - method @NonNull public android.window.WindowContainerTransaction reparentChildren(@NonNull android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken); method @NonNull public android.window.WindowContainerTransaction reparentTasks(@Nullable android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken, @Nullable int[], @Nullable int[], boolean); method @NonNull public android.window.WindowContainerTransaction requestFocusOnTaskFragment(@NonNull android.os.IBinder); method @NonNull public android.window.WindowContainerTransaction scheduleFinishEnterPip(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect); method @NonNull public android.window.WindowContainerTransaction setActivityWindowingMode(@NonNull android.window.WindowContainerToken, int); method @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken); - method @NonNull public android.window.WindowContainerTransaction setAdjacentTaskFragments(@NonNull android.os.IBinder, @Nullable android.os.IBinder, @Nullable android.window.WindowContainerTransaction.TaskFragmentAdjacentParams); + method @NonNull public android.window.WindowContainerTransaction setAdjacentTaskFragments(@NonNull android.os.IBinder, @NonNull android.os.IBinder, @Nullable android.window.WindowContainerTransaction.TaskFragmentAdjacentParams); method @NonNull public android.window.WindowContainerTransaction setAppBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect); method @NonNull public android.window.WindowContainerTransaction setBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect); method @NonNull public android.window.WindowContainerTransaction setBoundsChangeTransaction(@NonNull android.window.WindowContainerToken, @NonNull android.view.SurfaceControl.Transaction); diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 6826b67316fb..3e21124aa66e 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -20,6 +20,8 @@ import static android.app.ActivityManager.StopUserOnSwitch; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.PermissionMethod; +import android.annotation.PermissionName; import android.annotation.UserIdInt; import android.app.ActivityManager.ProcessCapability; import android.app.ActivityManager.RestrictionLevel; @@ -31,8 +33,6 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ActivityPresentationInfo; import android.content.pm.ApplicationInfo; -import android.content.pm.PermissionMethod; -import android.content.pm.PermissionName; import android.content.pm.UserInfo; import android.net.Uri; import android.os.Bundle; diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 86482c013a14..421ba7cb63eb 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -2640,7 +2640,7 @@ public final class ActivityThread extends ClientTransactionHandler ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) { return getPackageInfo(aInfo, compatInfo, baseLoader, securityViolation, includeCode, - registerPackage, /*isSdkSandbox=*/false); + registerPackage, Process.isSdkSandbox()); } private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, @@ -4558,7 +4558,11 @@ public final class ActivityThread extends ClientTransactionHandler service.attach(context, this, data.info.name, data.token, app, ActivityManager.getService()); if (!service.isUiContext()) { // WindowProviderService is a UI Context. - service.updateDeviceId(mLastReportedDeviceId); + VirtualDeviceManager vdm = context.getSystemService(VirtualDeviceManager.class); + if (mLastReportedDeviceId == VirtualDeviceManager.DEVICE_ID_DEFAULT + || vdm.isValidVirtualDeviceId(mLastReportedDeviceId)) { + service.updateDeviceId(mLastReportedDeviceId); + } } service.onCreate(); mServicesData.put(data.token, data); diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index a7a4b356c8d5..c11961e4c18e 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -3890,4 +3890,14 @@ public class ApplicationPackageManager extends PackageManager { return Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.SHOW_NEW_APP_INSTALLED_NOTIFICATION_ENABLED, 0) == 1; } + + @Override + public void relinquishUpdateOwnership(String targetPackage) { + Objects.requireNonNull(targetPackage); + try { + mPM.relinquishUpdateOwnership(targetPackage); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index e202760c5a4a..5cf10d069f55 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -63,6 +63,7 @@ public class BroadcastOptions extends ComponentOptions { private long mRequireCompatChangeId = CHANGE_INVALID; private boolean mRequireCompatChangeEnabled = true; private boolean mIsAlarmBroadcast = false; + private boolean mIsDeferUntilActive = false; private long mIdForResponseEvent; private @Nullable IntentFilter mRemoveMatchingFilter; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; @@ -201,6 +202,12 @@ public class BroadcastOptions extends ComponentOptions { "android:broadcast.removeMatchingFilter"; /** + * Corresponds to {@link #setDeferUntilActive(boolean)}. + */ + private static final String KEY_DEFER_UNTIL_ACTIVE = + "android:broadcast.deferuntilactive"; + + /** * Corresponds to {@link #setDeliveryGroupPolicy(int)}. */ private static final String KEY_DELIVERY_GROUP_POLICY = @@ -320,6 +327,7 @@ public class BroadcastOptions extends ComponentOptions { BundleMerger.class); mDeliveryGroupMatchingFilter = opts.getParcelable(KEY_DELIVERY_GROUP_MATCHING_FILTER, IntentFilter.class); + mIsDeferUntilActive = opts.getBoolean(KEY_DEFER_UNTIL_ACTIVE, false); } /** @@ -700,6 +708,41 @@ public class BroadcastOptions extends ComponentOptions { } /** + * Sets whether the broadcast should not run until the process is in an active process state + * (ie, a process exists for the app and the app is not in a cached process state). + * + * Whether an app's process state is considered active is independent of its standby bucket. + * + * A broadcast that is deferred until the process is active will not execute until the process + * is brought to an active state by some other action, like a job, alarm, or service binding. As + * a result, the broadcast may be delayed indefinitely. This deferral only applies to runtime + * registered receivers of a broadcast. Any manifest receivers will run immediately, similar to + * how a manifest receiver would start a new process in order to run a broadcast receiver. + * + * Ordered broadcasts, alarm broadcasts, interactive broadcasts, and manifest broadcasts are + * never deferred. + * + * Unordered broadcasts and unordered broadcasts with completion callbacks may be + * deferred. Completion callbacks for broadcasts deferred until active are + * best-effort. Completion callbacks will run when all eligible processes have finished + * executing the broadcast. Processes in inactive process states that defer the broadcast are + * not considered eligible and may not execute the broadcast prior to the completion callback. + * + * @hide + */ + @SystemApi + public @NonNull BroadcastOptions setDeferUntilActive(boolean shouldDefer) { + mIsDeferUntilActive = shouldDefer; + return this; + } + + /** @hide */ + @SystemApi + public boolean isDeferUntilActive() { + return mIsDeferUntilActive; + } + + /** * When enqueuing this broadcast, remove all pending broadcasts previously * sent by this app which match the given filter. * <p> @@ -963,6 +1006,9 @@ public class BroadcastOptions extends ComponentOptions { if (mDeliveryGroupMatchingFilter != null) { b.putParcelable(KEY_DELIVERY_GROUP_MATCHING_FILTER, mDeliveryGroupMatchingFilter); } + if (mIsDeferUntilActive) { + b.putBoolean(KEY_DEFER_UNTIL_ACTIVE, mIsDeferUntilActive); + } return b.isEmpty() ? null : b; } } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 240dbe1eea24..e654b566e6ef 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -26,9 +26,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiContext; -import android.companion.virtual.VirtualDevice; import android.companion.virtual.VirtualDeviceManager; -import android.companion.virtual.VirtualDeviceParams; import android.compat.annotation.UnsupportedAppUsage; import android.content.AttributionSource; import android.content.AutofillOptions; @@ -2584,6 +2582,22 @@ class ContextImpl extends Context { } @Override + public Context createContextForSdkInSandbox(ApplicationInfo sdkInfo, int flags) + throws NameNotFoundException { + if (!Process.isSdkSandbox()) { + throw new SecurityException("API can only be called from SdkSandbox process"); + } + + ContextImpl ctx = (ContextImpl) createApplicationContext(sdkInfo, flags); + + // Set sandbox app's context as the application context for sdk context + ctx.mPackageInfo.makeApplicationInner(/*forceDefaultAppClass=*/false, + /*instrumentation=*/null); + + return ctx; + } + + @Override public Context createPackageContext(String packageName, int flags) throws NameNotFoundException { return createPackageContextAsUser(packageName, flags, mUser); @@ -2742,9 +2756,12 @@ class ContextImpl extends Context { @Override public @NonNull Context createDeviceContext(int deviceId) { - if (!isValidDeviceId(deviceId)) { - throw new IllegalArgumentException( - "Not a valid ID of the default device or any virtual device: " + deviceId); + if (deviceId != VirtualDeviceManager.DEVICE_ID_DEFAULT) { + VirtualDeviceManager vdm = getSystemService(VirtualDeviceManager.class); + if (!vdm.isValidVirtualDeviceId(deviceId)) { + throw new IllegalArgumentException( + "Not a valid ID of the default device or any virtual device: " + deviceId); + } } ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mParams, @@ -2757,31 +2774,6 @@ class ContextImpl extends Context { return context; } - /** - * Checks whether the passed {@code deviceId} is valid or not. - * {@link VirtualDeviceManager#DEVICE_ID_DEFAULT} is valid as it is the ID of the default - * device when no additional virtual devices exist. If {@code deviceId} is the id of - * a virtual device, it should correspond to a virtual device created by - * {@link VirtualDeviceManager#createVirtualDevice(int, VirtualDeviceParams)}. - */ - private boolean isValidDeviceId(int deviceId) { - if (deviceId == VirtualDeviceManager.DEVICE_ID_DEFAULT) { - return true; - } - if (deviceId > VirtualDeviceManager.DEVICE_ID_DEFAULT) { - VirtualDeviceManager vdm = getSystemService(VirtualDeviceManager.class); - if (vdm != null) { - List<VirtualDevice> virtualDevices = vdm.getVirtualDevices(); - for (int i = 0; i < virtualDevices.size(); i++) { - if (virtualDevices.get(i).getDeviceId() == deviceId) { - return true; - } - } - } - } - return false; - } - @NonNull @Override public WindowContext createWindowContext(@WindowType int type, @@ -3044,10 +3036,13 @@ class ContextImpl extends Context { @Override public void updateDeviceId(int updatedDeviceId) { - if (!isValidDeviceId(updatedDeviceId)) { - throw new IllegalArgumentException( - "Not a valid ID of the default device or any virtual device: " - + updatedDeviceId); + if (updatedDeviceId != VirtualDeviceManager.DEVICE_ID_DEFAULT) { + VirtualDeviceManager vdm = getSystemService(VirtualDeviceManager.class); + if (!vdm.isValidVirtualDeviceId(updatedDeviceId)) { + throw new IllegalArgumentException( + "Not a valid ID of the default device or any virtual device: " + + updatedDeviceId); + } } if (mIsExplicitDeviceId) { throw new UnsupportedOperationException( diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java index 877177cc8861..f0c39ab0dc26 100644 --- a/core/java/android/app/ForegroundServiceTypePolicy.java +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -117,14 +117,10 @@ public abstract class ForegroundServiceTypePolicy { * The FGS type enforcement: * deprecating the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}. * - * <p>Starting a FGS with this type from apps with targetSdkVersion - * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will - * result in a warning in the log.</p> - * * @hide */ @ChangeId - @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.TIRAMISU) + @Disabled @Overridable public static final long FGS_TYPE_DATA_SYNC_DEPRECATION_CHANGE_ID = 255039210L; @@ -132,13 +128,8 @@ public abstract class ForegroundServiceTypePolicy { * The FGS type enforcement: * disabling the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}. * - * <p>Starting a FGS with this type from apps with targetSdkVersion - * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will - * result in an exception.</p> - * * @hide */ - // TODO (b/254661666): Change to @EnabledSince(U) in next OS release @ChangeId @Disabled @Overridable diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java index 50ba7dbbec85..69693fc3977b 100644 --- a/core/java/android/app/LocaleConfig.java +++ b/core/java/android/app/LocaleConfig.java @@ -110,7 +110,7 @@ public class LocaleConfig implements Parcelable { * @see Context#createPackageContext(String, int). */ @NonNull - public static LocaleConfig fromResources(@NonNull Context context) { + public static LocaleConfig fromContextIgnoringOverride(@NonNull Context context) { return new LocaleConfig(context, false); } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index ad17e0dea9c5..1633073d7cc8 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2417,6 +2417,7 @@ public class DevicePolicyManager { * applied (cross profile intent filters updated). Only usesd for CTS tests. * @hide */ + @SuppressLint("ActionValue") @TestApi @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_DATA_SHARING_RESTRICTION_APPLIED = @@ -2427,6 +2428,7 @@ public class DevicePolicyManager { * has been changed. * @hide */ + @SuppressLint("ActionValue") @TestApi @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_DEVICE_POLICY_CONSTANTS_CHANGED = @@ -6979,6 +6981,8 @@ public class DevicePolicyManager { * {@link #ENCRYPTION_STATUS_UNSUPPORTED}, {@link #ENCRYPTION_STATUS_INACTIVE}, * {@link #ENCRYPTION_STATUS_ACTIVATING}, {@link #ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY}, * {@link #ENCRYPTION_STATUS_ACTIVE}, or {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}. + * + * @throws SecurityException if called on a parent instance. */ public int getStorageEncryptionStatus() { throwIfParentInstance("getStorageEncryptionStatus"); diff --git a/core/java/android/app/time/UnixEpochTime.java b/core/java/android/app/time/UnixEpochTime.java index 61cbc5ed7977..0b8f7ee45a96 100644 --- a/core/java/android/app/time/UnixEpochTime.java +++ b/core/java/android/app/time/UnixEpochTime.java @@ -124,7 +124,7 @@ public final class UnixEpochTime implements Parcelable { @Override public String toString() { return "UnixEpochTime{" - + "mElapsedRealtimeTimeMillis=" + mElapsedRealtimeMillis + + "mElapsedRealtimeMillis=" + mElapsedRealtimeMillis + ", mUnixEpochTimeMillis=" + mUnixEpochTimeMillis + '}'; } diff --git a/core/java/android/app/timedetector/TimeDetector.java b/core/java/android/app/timedetector/TimeDetector.java index f95d6d3bb056..50a7da1cede5 100644 --- a/core/java/android/app/timedetector/TimeDetector.java +++ b/core/java/android/app/timedetector/TimeDetector.java @@ -73,6 +73,18 @@ public interface TimeDetector { String SHELL_COMMAND_SUGGEST_NETWORK_TIME = "suggest_network_time"; /** + * A shell command that prints the current network time information. + * @hide + */ + String SHELL_COMMAND_GET_NETWORK_TIME = "get_network_time"; + + /** + * A shell command that clears the detector's network time information. + * @hide + */ + String SHELL_COMMAND_CLEAR_NETWORK_TIME = "clear_network_time"; + + /** * A shell command that injects a GNSS time suggestion. * @hide */ diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 63f4bcfc4791..d31124d452a8 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -662,9 +662,7 @@ public final class CompanionDeviceManager { * @return the associations list * @see #addOnAssociationsChangedListener(Executor, OnAssociationsChangedListener) * @see #removeOnAssociationsChangedListener(OnAssociationsChangedListener) - * @hide */ - @SystemApi @UserHandleAware @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) public @NonNull List<AssociationInfo> getAllAssociations() { @@ -678,10 +676,7 @@ public final class CompanionDeviceManager { /** * Listener for any changes to {@link AssociationInfo}. - * - * @hide */ - @SystemApi public interface OnAssociationsChangedListener { /** * Invoked when a change occurs to any of the associations for the user (including adding @@ -696,9 +691,7 @@ public final class CompanionDeviceManager { * Register listener for any changes to {@link AssociationInfo}. * * @see #getAllAssociations() - * @hide */ - @SystemApi @UserHandleAware @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) public void addOnAssociationsChangedListener( @@ -720,9 +713,7 @@ public final class CompanionDeviceManager { * Unregister listener for any changes to {@link AssociationInfo}. * * @see #getAllAssociations() - * @hide */ - @SystemApi @UserHandleAware @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) public void removeOnAssociationsChangedListener( diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl index f0d23ac8374f..e96a2c18037b 100644 --- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl +++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl @@ -56,6 +56,14 @@ interface IVirtualDeviceManager { */ int getDeviceIdForDisplayId(int displayId); + /** + * Checks whether the passed {@code deviceId} is a valid virtual device ID or not. + * {@link VirtualDeviceManager#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default + * device which is not a virtual device. {@code deviceId} must correspond to a virtual device + * created by {@link VirtualDeviceManager#createVirtualDevice(int, VirtualDeviceParams)}. + */ + boolean isValidVirtualDeviceId(int deviceId); + /** * Returns the device policy for the given virtual device and policy type. */ diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 6ad18d545a6d..3bc1628d3576 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -258,6 +258,26 @@ public final class VirtualDeviceManager { } /** + * Checks whether the passed {@code deviceId} is a valid virtual device ID or not. + * {@link VirtualDeviceManager#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default + * device which is not a virtual device. {@code deviceId} must correspond to a virtual device + * created by {@link VirtualDeviceManager#createVirtualDevice(int, VirtualDeviceParams)}. + * + * @hide + */ + public boolean isValidVirtualDeviceId(int deviceId) { + if (mService == null) { + Log.w(TAG, "Failed to retrieve virtual devices; no virtual device manager service."); + return false; + } + try { + return mService.isValidVirtualDeviceId(deviceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Returns device-specific audio session id for audio playback. * * @param deviceId - id of the virtual audio device diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 12a2cae4c5c8..c8d48c189247 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -26,6 +26,8 @@ import android.annotation.DrawableRes; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.PermissionMethod; +import android.annotation.PermissionName; import android.annotation.RequiresPermission; import android.annotation.StringDef; import android.annotation.StringRes; @@ -53,8 +55,6 @@ import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.pm.PermissionMethod; -import android.content.pm.PermissionName; import android.content.res.AssetManager; import android.content.res.ColorStateList; import android.content.res.Configuration; @@ -408,7 +408,12 @@ public abstract class Context { * will cause the isolated service to be co-located in the same shared isolated process. * * Note that the shared isolated process is scoped to the calling app; once created, only - * the calling app can bind additional isolated services into the shared process. + * the calling app can bind additional isolated services into the shared process. However, + * the services themselves can come from different APKs and therefore different vendors. + * + * Only services that set the {@link android.R.attr#allowSharedIsolatedProcess} attribute + * to {@code true} are allowed to be bound into a shared isolated process. + * */ public static final int BIND_SHARED_ISOLATED_PROCESS = 0x00002000; @@ -6854,6 +6859,26 @@ public abstract class Context { @CreatePackageOptions int flags) throws PackageManager.NameNotFoundException; /** + * Creates a context given an {@link android.content.pm.ApplicationInfo}. + * + * Context created is for an sdk library that is being loaded in sdk sandbox. + * + * @param sdkInfo information regarding the sdk library being loaded. + * + * @throws PackageManager.NameNotFoundException if there is no application with + * the given package name. + * @throws SecurityException if caller is not a SdkSandbox process. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @NonNull + public Context createContextForSdkInSandbox(@NonNull ApplicationInfo sdkInfo, + @CreatePackageOptions int flags) throws PackageManager.NameNotFoundException { + throw new RuntimeException("Not implemented. Must override in a subclass."); + } + + /** * Return a new Context object for the given split name. The new Context has a ClassLoader and * Resources object that can access the split's and all of its dependencies' code/resources. * Each call to this method returns a new instance of a Context object; diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 0dc4adc79f30..e65e91c45396 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -1073,6 +1073,15 @@ public class ContextWrapper extends Context { /** @hide */ @Override + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @NonNull + public Context createContextForSdkInSandbox(@NonNull ApplicationInfo sdkInfo, int flags) + throws PackageManager.NameNotFoundException { + return mBase.createContextForSdkInSandbox(sdkInfo, flags); + } + + /** @hide */ + @Override public Context createContextForSplit(String splitName) throws PackageManager.NameNotFoundException { return mBase.createContextForSplit(splitName); diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 7ee8f604dd4f..4318c3993b13 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -11601,7 +11601,7 @@ public class Intent implements Parcelable, Cloneable { private void toUriInner(StringBuilder uri, String scheme, String defAction, String defPackage, int flags) { if (scheme != null) { - uri.append("scheme=").append(scheme).append(';'); + uri.append("scheme=").append(Uri.encode(scheme)).append(';'); } if (mAction != null && !mAction.equals(defAction)) { uri.append("action=").append(Uri.encode(mAction)).append(';'); diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index a9f55bc1ea4c..8fd905e5bd87 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1102,6 +1102,41 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public static final long ALWAYS_SANDBOX_DISPLAY_APIS = 185004937L; // buganizer id /** + * This change id excludes the packages it is applied to from the camera compat force rotation + * treatment. See com.android.server.wm.DisplayRotationCompatPolicy for context. + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION = + 263959004L; // buganizer id + + /** + * This change id excludes the packages it is applied to from activity refresh after camera + * compat force rotation treatment. See com.android.server.wm.DisplayRotationCompatPolicy for + * context. + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH = 264304459L; // buganizer id + + /** + * This change id makes the packages it is applied to do activity refresh after camera compat + * force rotation treatment using "resumed -> paused -> resumed" cycle rather than "resumed -> + * ... -> stopped -> ... -> resumed" cycle. See + * com.android.server.wm.DisplayRotationCompatPolicy for context. + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = + 264301586L; // buganizer id + + /** * This change id is the gatekeeper for all treatments that force a given min aspect ratio. * Enabling this change will allow the following min aspect ratio treatments to be applied: * OVERRIDE_MIN_ASPECT_RATIO_MEDIUM diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl index 60a7b13ff6e9..9dd9c0f757d5 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -46,6 +46,8 @@ interface IPackageInstallerSession { void commit(in IntentSender statusReceiver, boolean forTransferred); void transfer(in String packageName); void abandon(); + void seal(); + List<String> fetchPackageNames(); DataLoaderParamsParcel getDataLoaderParams(); void addFile(int location, String name, long lengthBytes, in byte[] metadata, in byte[] signature); @@ -63,6 +65,7 @@ interface IPackageInstallerSession { void requestUserPreapproval(in PackageInstaller.PreapprovalDetails details, in IntentSender statusReceiver); boolean isKeepApplicationEnabledSetting(); + boolean isRequestUpdateOwnership(); ParcelFileDescriptor getAppMetadataFd(); ParcelFileDescriptor openWriteAppMetadata(); diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 54ca1e59aafe..0e37c8798919 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -214,6 +214,8 @@ interface IPackageManager { @UnsupportedAppUsage void setInstallerPackageName(in String targetPackage, in String installerPackageName); + void relinquishUpdateOwnership(in String targetPackage); + void setApplicationCategoryHint(String packageName, int categoryHint, String callerPackageName); /** @deprecated rawr, don't call AIDL methods directly! */ diff --git a/core/java/android/content/pm/InstallSourceInfo.java b/core/java/android/content/pm/InstallSourceInfo.java index 88f1a16ec3ab..67123e87a265 100644 --- a/core/java/android/content/pm/InstallSourceInfo.java +++ b/core/java/android/content/pm/InstallSourceInfo.java @@ -35,6 +35,8 @@ public final class InstallSourceInfo implements Parcelable { @Nullable private final String mInstallingPackageName; + @Nullable private final String mUpdateOwnerPackageName; + @Nullable private final int mPackageSource; /** @hide */ @@ -42,18 +44,20 @@ public final class InstallSourceInfo implements Parcelable { @Nullable SigningInfo initiatingPackageSigningInfo, @Nullable String originatingPackageName, @Nullable String installingPackageName) { this(initiatingPackageName, initiatingPackageSigningInfo, originatingPackageName, - installingPackageName, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); + installingPackageName, null /* updateOwnerPackageName */, + PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); } /** @hide */ public InstallSourceInfo(@Nullable String initiatingPackageName, @Nullable SigningInfo initiatingPackageSigningInfo, @Nullable String originatingPackageName, @Nullable String installingPackageName, - int packageSource) { + @Nullable String updateOwnerPackageName, int packageSource) { mInitiatingPackageName = initiatingPackageName; mInitiatingPackageSigningInfo = initiatingPackageSigningInfo; mOriginatingPackageName = originatingPackageName; mInstallingPackageName = installingPackageName; + mUpdateOwnerPackageName = updateOwnerPackageName; mPackageSource = packageSource; } @@ -69,6 +73,7 @@ public final class InstallSourceInfo implements Parcelable { dest.writeParcelable(mInitiatingPackageSigningInfo, flags); dest.writeString(mOriginatingPackageName); dest.writeString(mInstallingPackageName); + dest.writeString8(mUpdateOwnerPackageName); dest.writeInt(mPackageSource); } @@ -77,6 +82,7 @@ public final class InstallSourceInfo implements Parcelable { mInitiatingPackageSigningInfo = source.readParcelable(SigningInfo.class.getClassLoader(), android.content.pm.SigningInfo.class); mOriginatingPackageName = source.readString(); mInstallingPackageName = source.readString(); + mUpdateOwnerPackageName = source.readString8(); mPackageSource = source.readInt(); } @@ -137,6 +143,21 @@ public final class InstallSourceInfo implements Parcelable { } /** + * The name of the package that is the update owner, or null if not available. + * + * This indicates the update ownership enforcement is enabled for this app, + * and which package is the update owner. + * + * Returns null if the update ownership enforcement is disabled for the app. + * + * @see PackageInstaller.SessionParams#setRequestUpdateOwnership + */ + @Nullable + public String getUpdateOwnerPackageName() { + return mUpdateOwnerPackageName; + } + + /** * Information about the package source when installer installed this app. */ public @PackageInstaller.PackageSourceType int getPackageSource() { diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 703a92523cf5..df1340d318d8 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -44,8 +44,11 @@ import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.ActivityManager; +import android.app.ActivityThread; import android.app.AppGlobals; import android.compat.annotation.UnsupportedAppUsage; +import android.content.IIntentReceiver; +import android.content.IIntentSender; import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageManager.DeleteFlags; @@ -59,9 +62,11 @@ import android.graphics.Bitmap; import android.icu.util.ULocale; import android.net.Uri; import android.os.Build; +import android.os.Bundle; import android.os.FileBridge; import android.os.Handler; import android.os.HandlerExecutor; +import android.os.IBinder; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; @@ -226,8 +231,8 @@ public class PackageInstaller { * {@link #STATUS_PENDING_USER_ACTION}, {@link #STATUS_SUCCESS}, * {@link #STATUS_FAILURE}, {@link #STATUS_FAILURE_ABORTED}, * {@link #STATUS_FAILURE_BLOCKED}, {@link #STATUS_FAILURE_CONFLICT}, - * {@link #STATUS_FAILURE_INCOMPATIBLE}, {@link #STATUS_FAILURE_INVALID}, or - * {@link #STATUS_FAILURE_STORAGE}. + * {@link #STATUS_FAILURE_INCOMPATIBLE}, {@link #STATUS_FAILURE_INVALID}, + * {@link #STATUS_FAILURE_STORAGE}, or {@link #STATUS_FAILURE_TIMEOUT}. * <p> * More information about a status may be available through additional * extras; see the individual status documentation for details. @@ -441,6 +446,13 @@ public class PackageInstaller { public static final int STATUS_FAILURE_INCOMPATIBLE = 7; /** + * The operation failed because it didn't complete within the specified timeout. + * + * @see #EXTRA_STATUS_MESSAGE + */ + public static final int STATUS_FAILURE_TIMEOUT = 8; + + /** * Default value, non-streaming installation session. * * @see #EXTRA_DATA_LOADER_TYPE @@ -544,6 +556,46 @@ public class PackageInstaller { @Retention(RetentionPolicy.SOURCE) @interface PackageSourceType{} + /** + * Indicate the user intervention is required when the installer attempts to commit the session. + * This is the default case. + * + * @hide + */ + @SystemApi + public static final int REASON_CONFIRM_PACKAGE_CHANGE = 0; + + /** + * Indicate the user intervention is required because the update ownership enforcement is + * enabled, and the update owner will change. + * + * @see PackageInstaller.SessionParams#setRequestUpdateOwnership + * @see InstallSourceInfo#getUpdateOwnerPackageName + * @hide + */ + @SystemApi + public static final int REASON_OWNERSHIP_CHANGED = 1; + + /** + * Indicate the user intervention is required because the update ownership enforcement is + * enabled, and remind the update owner will retain. + * + * @see PackageInstaller.SessionParams#setRequestUpdateOwnership + * @see InstallSourceInfo#getUpdateOwnerPackageName + * @hide + */ + @SystemApi + public static final int REASON_REMIND_OWNERSHIP = 2; + + /** @hide */ + @IntDef(prefix = { "REASON_" }, value = { + REASON_CONFIRM_PACKAGE_CHANGE, + REASON_OWNERSHIP_CHANGED, + REASON_REMIND_OWNERSHIP, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface UserActionReason {} + /** Default set of checksums - includes all available checksums. * @see Session#requestChecksums */ private static final int DEFAULT_CHECKSUMS = @@ -976,6 +1028,61 @@ public class PackageInstaller { } /** + * Commit the session when all constraints are satisfied. This is a convenient method to + * combine {@link #waitForInstallConstraints(List, InstallConstraints, IntentSender, long)} + * and {@link Session#commit(IntentSender)}. + * <p> + * Once this method is called, the session is sealed and no additional mutations + * may be performed on the session. In the case of timeout, you may commit the + * session again using this method or {@link Session#commit(IntentSender)} for retries. + * + * @param statusReceiver Called when the state of the session changes. Intents + * sent to this receiver contain {@link #EXTRA_STATUS}. + * Refer to the individual status codes on how to handle them. + * @param constraints The requirements to satisfy before committing the session. + * @param timeoutMillis The maximum time to wait, in milliseconds until the + * constraints are satisfied. The caller will be notified via + * {@code statusReceiver} if timeout happens before commit. + */ + public void commitSessionAfterInstallConstraintsAreMet(int sessionId, + @NonNull IntentSender statusReceiver, @NonNull InstallConstraints constraints, + @DurationMillisLong long timeoutMillis) { + try { + var session = mInstaller.openSession(sessionId); + session.seal(); + var packageNames = session.fetchPackageNames(); + var intentSender = new IntentSender((IIntentSender) new IIntentSender.Stub() { + @Override + public void send(int code, Intent intent, String resolvedType, + IBinder allowlistToken, IIntentReceiver finishedReceiver, + String requiredPermission, Bundle options) { + var result = intent.getParcelableExtra( + PackageInstaller.EXTRA_INSTALL_CONSTRAINTS_RESULT, + InstallConstraintsResult.class); + try { + if (result.isAllConstraintsSatisfied()) { + session.commit(statusReceiver, false); + } else { + // timeout + final Intent fillIn = new Intent(); + fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); + fillIn.putExtra(PackageInstaller.EXTRA_STATUS, STATUS_FAILURE_TIMEOUT); + fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, + "Install constraints not satisfied within timeout"); + statusReceiver.sendIntent( + ActivityThread.currentApplication(), 0, fillIn, null, null); + } + } catch (Exception ignore) { + } + } + }); + waitForInstallConstraints(packageNames, constraints, intentSender, timeoutMillis); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Events for observing session lifecycle. * <p> * A typical session lifecycle looks like this: @@ -1910,6 +2017,20 @@ public class PackageInstaller { throw e.rethrowFromSystemServer(); } } + + /** + * @return {@code true} if the installer requested the update ownership enforcement + * for the packages in this session. + * + * @see PackageInstaller.SessionParams#setRequestUpdateOwnership + */ + public boolean isRequestUpdateOwnership() { + try { + return mSession.isRequestUpdateOwnership(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -2672,9 +2793,18 @@ public class PackageInstaller { * Android S ({@link android.os.Build.VERSION_CODES#S API 31})</li> * </ul> * </li> - * <li>The installer is the {@link InstallSourceInfo#getInstallingPackageName() - * installer of record} of an existing version of the app (in other words, this install - * session is an app update) or the installer is updating itself.</li> + * <li>The installer is: + * <ul> + * <li>The {@link InstallSourceInfo#getUpdateOwnerPackageName() update owner} + * of an existing version of the app (in other words, this install session is + * an app update) if the update ownership enforcement is enabled.</li> + * <li>The {@link InstallSourceInfo#getInstallingPackageName() installer of + * record} of an existing version of the app (in other words, this install + * session is an app update) if the update ownership enforcement isn't + * enabled.</li> + * <li>Updating itself.</li> + * </ul> + * </li>> * <li>The installer declares the * {@link android.Manifest.permission#UPDATE_PACKAGES_WITHOUT_USER_ACTION * UPDATE_PACKAGES_WITHOUT_USER_ACTION} permission.</li> @@ -2713,6 +2843,30 @@ public class PackageInstaller { this.keepApplicationEnabledSetting = true; } + /** + * Optionally indicate whether the package being installed needs the update ownership + * enforcement. Once the update ownership enforcement is enabled, the other installers + * will need the user action to update the package even if the installers have been + * granted the {@link android.Manifest.permission#INSTALL_PACKAGES INSTALL_PACKAGES} + * permission. Default to {@code false}. + * + * The update ownership enforcement can only be enabled on initial installation. Set + * this to {@code true} on package update indicates the installer package wants to be + * the update owner if the update ownership enforcement has enabled. + * + * Note: To enable the update ownership enforcement, the installer must have the + * {@link android.Manifest.permission#ENFORCE_UPDATE_OWNERSHIP ENFORCE_UPDATE_OWNERSHIP} + * permission. + */ + @RequiresPermission(Manifest.permission.ENFORCE_UPDATE_OWNERSHIP) + public void setRequestUpdateOwnership(boolean enable) { + if (enable) { + this.installFlags |= PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP; + } else { + this.installFlags &= ~PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP; + } + } + /** {@hide} */ public void dump(IndentingPrintWriter pw) { pw.printPair("mode", mode); @@ -2987,6 +3141,9 @@ public class PackageInstaller { /** @hide */ public boolean keepApplicationEnabledSetting; + /** @hide */ + public int pendingUserActionReason; + /** {@hide} */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public SessionInfo() { @@ -3041,6 +3198,7 @@ public class PackageInstaller { installerUid = source.readInt(); packageSource = source.readInt(); keepApplicationEnabledSetting = source.readBoolean(); + pendingUserActionReason = source.readInt(); } /** @@ -3585,6 +3743,25 @@ public class PackageInstaller { return isPreapprovalRequested; } + /** + * @return {@code true} if the installer requested the update ownership enforcement + * for the packages in this session. + * + * @see PackageInstaller.SessionParams#setRequestUpdateOwnership + */ + public boolean isRequestUpdateOwnership() { + return (installFlags & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0; + } + + /** + * Return the reason for requiring the user action. + * @hide + */ + @SystemApi + public @UserActionReason int getPendingUserActionReason() { + return pendingUserActionReason; + } + @Override public int describeContents() { return 0; @@ -3635,6 +3812,7 @@ public class PackageInstaller { dest.writeInt(installerUid); dest.writeInt(packageSource); dest.writeBoolean(keepApplicationEnabledSetting); + dest.writeInt(pendingUserActionReason); } public static final Parcelable.Creator<SessionInfo> diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 4ad657e64fb3..fe063662db0d 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1355,6 +1355,7 @@ public abstract class PackageManager { INSTALL_ENABLE_ROLLBACK, INSTALL_ALLOW_DOWNGRADE, INSTALL_STAGED, + INSTALL_REQUEST_UPDATE_OWNERSHIP, }) @Retention(RetentionPolicy.SOURCE) public @interface InstallFlags {} @@ -1545,6 +1546,21 @@ public abstract class PackageManager { */ public static final int INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK = 0x00800000; + /** + * Flag parameter for {@link #installPackage} to bypass the low targer sdk version block + * for this install. + * + * @hide + */ + public static final int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x00800000; + + /** + * Flag parameter for {@link PackageInstaller.SessionParams} to indicate that the + * update ownership enforcement is requested. + * @hide + */ + public static final int INSTALL_REQUEST_UPDATE_OWNERSHIP = 1 << 25; + /** @hide */ @IntDef(flag = true, value = { DONT_KILL_APP, @@ -2233,6 +2249,15 @@ public abstract class PackageManager { */ public static final int INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE = -129; + /** + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package declares bad certificate digest for a shared library in the package + * manifest. + * + * @hide + */ + public static final int INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST = -130; + /** @hide */ @IntDef(flag = true, prefix = { "DELETE_" }, value = { DELETE_KEEP_DATA, @@ -9681,6 +9706,8 @@ public abstract class PackageManager { case INSTALL_FAILED_WRONG_INSTALLED_VERSION: return "INSTALL_FAILED_WRONG_INSTALLED_VERSION"; case INSTALL_FAILED_PROCESS_NOT_DEFINED: return "INSTALL_FAILED_PROCESS_NOT_DEFINED"; case INSTALL_FAILED_SESSION_INVALID: return "INSTALL_FAILED_SESSION_INVALID"; + case INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST: + return "INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST"; default: return Integer.toString(status); } } @@ -10850,4 +10877,16 @@ public abstract class PackageManager { throw new UnsupportedOperationException( "isShowNewAppInstalledNotificationEnabled not implemented in subclass"); } + + /** + * Attempt to relinquish the update ownership of the given package. Only the current + * update owner of the given package can use this API or a SecurityException will be + * thrown. + * + * @param targetPackage The installed package whose update owner will be changed. + */ + public void relinquishUpdateOwnership(@NonNull String targetPackage) { + throw new UnsupportedOperationException( + "relinquishUpdateOwnership not implemented in subclass"); + } } diff --git a/core/java/android/content/pm/PermissionMethod.java b/core/java/android/content/pm/PermissionMethod.java deleted file mode 100644 index 647c696b87f3..000000000000 --- a/core/java/android/content/pm/PermissionMethod.java +++ /dev/null @@ -1,52 +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 android.content.pm; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.CLASS; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Documents that the subject method's job is to look - * up whether the provided or calling uid/pid has the requested permission. - * - * <p>Methods should either return `void`, but potentially throw {@link SecurityException}, - * or return {@link android.content.pm.PackageManager.PermissionResult} `int`. - * - * @hide - */ -@Retention(CLASS) -@Target({METHOD}) -public @interface PermissionMethod { - /** - * Hard-coded list of permissions checked by this method - */ - @PermissionName String[] value() default {}; - /** - * If true, the check passes if the caller - * has any ONE of the supplied permissions - */ - boolean anyOf() default false; - /** - * Signifies that the permission check passes if - * the calling process OR the current process has - * the permission - */ - boolean orSelf() default false; -} diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 4ade8a8b87b6..4e2acc036386 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -133,20 +133,15 @@ public class ServiceInfo extends ComponentInfo * Data(photo, file, account) upload/download, backup/restore, import/export, fetch, * transfer over network between device and cloud. * - * <p>Apps targeting API level {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and - * later should NOT use this type: - * calling {@link android.app.Service#startForeground(int, android.app.Notification, int)} with - * this type on devices running {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} is still - * allowed, but calling it with this type on devices running future platform releases may get a - * {@link android.app.InvalidForegroundServiceTypeException}.</p> - * - * @deprecated Use {@link android.app.job.JobInfo.Builder} data transfer APIs instead. + * <p class="note"> + * Use the {@link android.app.job.JobInfo.Builder#setDataTransfer} API for data transfers + * that can be deferred until conditions are ideal for the app or device. + * </p> */ @RequiresPermission( value = Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional = true ) - @Deprecated public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1 << 0; /** diff --git a/core/java/android/credentials/ClearCredentialStateException.java b/core/java/android/credentials/ClearCredentialStateException.java index c518461f1a49..78fe203fb679 100644 --- a/core/java/android/credentials/ClearCredentialStateException.java +++ b/core/java/android/credentials/ClearCredentialStateException.java @@ -31,6 +31,12 @@ import java.util.concurrent.Executor; * CancellationSignal, Executor, OutcomeReceiver)} operation. */ public class ClearCredentialStateException extends Exception { + /** + * The error type value for when the given operation failed due to an unknown reason. + */ + @NonNull + public static final String TYPE_UNKNOWN = + "android.credentials.ClearCredentialStateException.TYPE_UNKNOWN"; @NonNull private final String mType; diff --git a/core/java/android/credentials/CreateCredentialException.java b/core/java/android/credentials/CreateCredentialException.java index cb326903a828..fefa60ae23ee 100644 --- a/core/java/android/credentials/CreateCredentialException.java +++ b/core/java/android/credentials/CreateCredentialException.java @@ -33,6 +33,13 @@ import java.util.concurrent.Executor; */ public class CreateCredentialException extends Exception { /** + * The error type value for when the given operation failed due to an unknown reason. + */ + @NonNull + public static final String TYPE_UNKNOWN = + "android.credentials.CreateCredentialException.TYPE_UNKNOWN"; + + /** * The error type value for when no credential is available for the given {@link * CredentialManager#executeCreateCredential(CreateCredentialRequest, Activity, * CancellationSignal, Executor, OutcomeReceiver)} request. @@ -40,6 +47,22 @@ public class CreateCredentialException extends Exception { @NonNull public static final String TYPE_NO_CREDENTIAL = "android.credentials.CreateCredentialException.TYPE_NO_CREDENTIAL"; + /** + * The error type value for when the user intentionally cancelled the request. + * + * <p>This is a strong indicator that your app should refrain from making the same api call for + * a certain amount of time to provide a better user experience. + */ + @NonNull + public static final String TYPE_USER_CANCELED = + "android.credentials.CreateCredentialException.TYPE_USER_CANCELED"; + /** + * The error type value for when the given operation failed due to internal interruption. + * Retrying the same operation should fix the error. + */ + @NonNull + public static final String TYPE_INTERRUPTED = + "android.credentials.CreateCredentialException.TYPE_INTERRUPTED"; @NonNull private final String mType; diff --git a/core/java/android/credentials/GetCredentialException.java b/core/java/android/credentials/GetCredentialException.java index 5d6e4dfa343c..478afff1fae1 100644 --- a/core/java/android/credentials/GetCredentialException.java +++ b/core/java/android/credentials/GetCredentialException.java @@ -33,6 +33,13 @@ import java.util.concurrent.Executor; */ public class GetCredentialException extends Exception { /** + * The error type value for when the given operation failed due to an unknown reason. + */ + @NonNull + public static final String TYPE_UNKNOWN = + "android.credentials.GetCredentialException.TYPE_UNKNOWN"; + + /** * The error type value for when no credential is found available for the given {@link * CredentialManager#executeGetCredential(GetCredentialRequest, Activity, CancellationSignal, * Executor, OutcomeReceiver)} request. @@ -40,6 +47,22 @@ public class GetCredentialException extends Exception { @NonNull public static final String TYPE_NO_CREDENTIAL = "android.credentials.GetCredentialException.TYPE_NO_CREDENTIAL"; + /** + * The error type value for when the user intentionally cancelled the request. + * + * <p>This is a strong indicator that your app should refrain from making the same api call for + * a certain amount of time to provide a better user experience. + */ + @NonNull + public static final String TYPE_USER_CANCELED = + "android.credentials.GetCredentialException.TYPE_USER_CANCELED"; + /** + * The error type value for when the given operation failed due to internal interruption. + * Retrying the same operation should fix the error. + */ + @NonNull + public static final String TYPE_INTERRUPTED = + "android.credentials.GetCredentialException.TYPE_INTERRUPTED"; @NonNull private final String mType; diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java index 9f2edae6d8e4..37a572425823 100644 --- a/core/java/android/credentials/ui/Entry.java +++ b/core/java/android/credentials/ui/Entry.java @@ -88,6 +88,7 @@ public final class Entry implements Parcelable { /** Constructor to be used for an entry that requires a pending intent to be invoked * when clicked. */ + // TODO: Remove this constructor as it is no longer used public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice, @NonNull PendingIntent pendingIntent, @NonNull Intent intent) { this(key, subkey, slice); diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index c716f319103a..81d6ba93cfe9 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -24,7 +24,6 @@ import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.ActivityThread; import android.app.AppOpsManager; -import android.app.compat.CompatChanges; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.graphics.ImageFormat; @@ -42,7 +41,6 @@ import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemProperties; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RSIllegalArgumentException; @@ -279,14 +277,6 @@ public class Camera { */ public native static int getNumberOfCameras(); - private static final boolean sLandscapeToPortrait = - SystemProperties.getBoolean(CameraManager.LANDSCAPE_TO_PORTRAIT_PROP, false); - - private static boolean shouldOverrideToPortrait() { - return CompatChanges.isChangeEnabled(CameraManager.OVERRIDE_FRONT_CAMERA_APP_COMPAT) - && sLandscapeToPortrait; - } - /** * Returns the information about a particular camera. * If {@link #getNumberOfCameras()} returns N, the valid id is 0 to N-1. @@ -296,7 +286,8 @@ public class Camera { * low-level failure). */ public static void getCameraInfo(int cameraId, CameraInfo cameraInfo) { - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait( + ActivityThread.currentApplication().getApplicationContext()); _getCameraInfo(cameraId, overrideToPortrait, cameraInfo); IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); @@ -493,7 +484,8 @@ public class Camera { mEventHandler = null; } - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait( + ActivityThread.currentApplication().getApplicationContext()); return native_setup(new WeakReference<Camera>(this), cameraId, ActivityThread.currentOpPackageName(), overrideToPortrait); } diff --git a/core/java/android/hardware/OverlayProperties.java b/core/java/android/hardware/OverlayProperties.java index 1ce1361cd4e7..8bfc2f7da25e 100644 --- a/core/java/android/hardware/OverlayProperties.java +++ b/core/java/android/hardware/OverlayProperties.java @@ -59,6 +59,16 @@ public final class OverlayProperties implements Parcelable { } /** + * @return True if the device can support mixed colorspaces, false otherwise. + */ + public boolean supportMixedColorSpaces() { + if (mNativeObject == 0) { + return false; + } + return nSupportMixedColorSpaces(mNativeObject); + } + + /** * Release the local reference. */ public void release() { @@ -106,6 +116,7 @@ public final class OverlayProperties implements Parcelable { private static native long nGetDestructor(); private static native boolean nSupportFp16ForHdr(long nativeObject); + private static native boolean nSupportMixedColorSpaces(long nativeObject); private static native void nWriteOverlayPropertiesToParcel(long nativeObject, Parcel dest); private static native long nReadOverlayPropertiesFromParcel(Parcel in); } diff --git a/core/java/android/hardware/camera2/CameraExtensionSession.java b/core/java/android/hardware/camera2/CameraExtensionSession.java index 1542d61fb54f..21fead980cdd 100644 --- a/core/java/android/hardware/camera2/CameraExtensionSession.java +++ b/core/java/android/hardware/camera2/CameraExtensionSession.java @@ -19,10 +19,7 @@ package android.hardware.camera2; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; -import android.hardware.camera2.impl.PublicKey; -import android.hardware.camera2.utils.TypeReference; -import android.util.Pair; -import android.util.Range; +import android.hardware.camera2.utils.HashCodeHelpers; import java.util.concurrent.Executor; @@ -434,14 +431,66 @@ public abstract class CameraExtensionSession implements AutoCloseable { } /** - * Return the realtime still {@link #capture} latency. + * Realtime calculated still {@link #capture} latency. * - * <p>The pair will be in milliseconds with the first value indicating the capture latency from - * the {@link ExtensionCaptureCallback#onCaptureStarted} until - * {@link ExtensionCaptureCallback#onCaptureProcessStarted} - * and the second value containing the estimated post-processing latency from - * {@link ExtensionCaptureCallback#onCaptureProcessStarted} until the processed frame returns - * to the client.</p> + * @see #getRealtimeStillCaptureLatency() + */ + public final static class StillCaptureLatency { + private final long mCaptureLatency, mProcessingLatency; + + public StillCaptureLatency(long captureLatency, long processingLatency) { + mCaptureLatency = captureLatency; + mProcessingLatency = processingLatency; + } + /** + * Return the capture latency from + * {@link ExtensionCaptureCallback#onCaptureStarted} until + * {@link ExtensionCaptureCallback#onCaptureProcessStarted}. + * + * @return The realtime capture latency in milliseconds. + */ + public long getCaptureLatency() { + return mCaptureLatency; + } + + /** + * Return the estimated post-processing latency from + * {@link ExtensionCaptureCallback#onCaptureProcessStarted} until the processed frame + * returns to the client. + * + * @return returns post-processing latency in milliseconds + */ + public long getProcessingLatency() { + return mProcessingLatency; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + StillCaptureLatency latency = (StillCaptureLatency) o; + + if (mCaptureLatency != latency.mCaptureLatency) return false; + if (mProcessingLatency != latency.mProcessingLatency) return false; + + return true; + } + + @Override + public int hashCode() { + return HashCodeHelpers.hashCode(mCaptureLatency, mProcessingLatency); + } + + @Override + public String toString() { + return "StillCaptureLatency(processingLatency:" + mProcessingLatency + + ", captureLatency: " + mCaptureLatency + ")"; + } + } + + /** + * Return the realtime still {@link #capture} latency. * * <p>The estimations will take into account the current environment conditions, the camera * state and will include the time spent processing the multi-frame capture request along with @@ -451,7 +500,7 @@ public abstract class CameraExtensionSession implements AutoCloseable { * or {@code null} if the estimation is not supported. */ @Nullable - public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException { + public StillCaptureLatency getRealtimeStillCaptureLatency() throws CameraAccessException { throw new UnsupportedOperationException("Subclasses must override this method"); } diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 5b6e288d9da9..e2dedd691407 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -115,7 +115,14 @@ public final class CameraManager { @Overridable @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) @TestApi - public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; + public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; + + /** + * Package-level opt in/out for the above. + * @hide + */ + public static final String PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT = + "android.camera.PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT"; /** * System property for allowing the above @@ -607,7 +614,7 @@ public final class CameraManager { try { Size displaySize = getDisplaySize(); - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = shouldOverrideToPortrait(mContext); CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId, mContext.getApplicationInfo().targetSdkVersion, overrideToPortrait); try { @@ -727,7 +734,7 @@ public final class CameraManager { "Camera service is currently unavailable"); } - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = shouldOverrideToPortrait(mContext); cameraUser = cameraService.connectDevice(callbacks, cameraId, mContext.getOpPackageName(), mContext.getAttributionTag(), uid, oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion, @@ -1159,9 +1166,26 @@ public final class CameraManager { return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId); } - private static boolean shouldOverrideToPortrait() { - return CompatChanges.isChangeEnabled(OVERRIDE_FRONT_CAMERA_APP_COMPAT) - && CameraManagerGlobal.sLandscapeToPortrait; + /** + * @hide + */ + public static boolean shouldOverrideToPortrait(@Nullable Context context) { + if (!CameraManagerGlobal.sLandscapeToPortrait) { + return false; + } + + if (context != null) { + PackageManager packageManager = context.getPackageManager(); + + try { + return packageManager.getProperty(context.getOpPackageName(), + PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT).getBoolean(); + } catch (PackageManager.NameNotFoundException e) { + // No such property + } + } + + return CompatChanges.isChangeEnabled(OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT); } /** @@ -2318,6 +2342,15 @@ public final class CameraManager { final AvailabilityCallback callback = mCallbackMap.keyAt(i); postSingleUpdate(callback, executor, id, null /*physicalId*/, status); + + // Send the NOT_PRESENT state for unavailable physical cameras + if (isAvailable(status) && mUnavailablePhysicalDevices.containsKey(id)) { + ArrayList<String> unavailableIds = mUnavailablePhysicalDevices.get(id); + for (String unavailableId : unavailableIds) { + postSingleUpdate(callback, executor, id, unavailableId, + ICameraServiceListener.STATUS_NOT_PRESENT); + } + } } } // onStatusChangedLocked diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java index 9437ea76180a..709fa609a96a 100644 --- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java @@ -361,7 +361,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes } @Override - public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException { + public StillCaptureLatency getRealtimeStillCaptureLatency() throws CameraAccessException { synchronized (mInterfaceLock) { if (!mInitialized) { throw new IllegalStateException("Uninitialized component"); @@ -370,7 +370,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes try { LatencyPair latency = mSessionProcessor.getRealtimeCaptureLatency(); if (latency != null) { - return new Pair<>(latency.first, latency.second); + return new StillCaptureLatency(latency.first, latency.second); } return null; diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java index ed48a6dc25be..3f85d44b3b53 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -513,7 +513,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { } @Override - public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException { + public StillCaptureLatency getRealtimeStillCaptureLatency() throws CameraAccessException { synchronized (mInterfaceLock) { if (!mInitialized) { throw new IllegalStateException("Uninitialized component"); @@ -522,7 +522,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { try { LatencyPair latency = mImageExtender.getRealtimeCaptureLatency(); if (latency != null) { - return new Pair<>(latency.first, latency.second); + return new StillCaptureLatency(latency.first, latency.second); } return null; diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java index dba1a5e8dfc6..6a667fe39974 100644 --- a/core/java/android/hardware/devicestate/DeviceStateManager.java +++ b/core/java/android/hardware/devicestate/DeviceStateManager.java @@ -251,6 +251,10 @@ public final class DeviceStateManager { @Nullable private Boolean lastResult; + public FoldStateListener(Context context) { + this(context, folded -> {}); + } + public FoldStateListener(Context context, Consumer<Boolean> listener) { mFoldedDeviceStates = context.getResources().getIntArray( com.android.internal.R.array.config_foldedDeviceStates); @@ -266,5 +270,10 @@ public final class DeviceStateManager { mDelegate.accept(folded); } } + + @Nullable + public Boolean getFolded() { + return lastResult; + } } } diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl index 2bf187ac9006..5fcc31e3ea25 100644 --- a/core/java/android/hardware/face/IFaceService.aidl +++ b/core/java/android/hardware/face/IFaceService.aidl @@ -43,7 +43,7 @@ interface IFaceService { byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer); // Retrieve static sensor properties for all face sensors - @EnforcePermission("MANAGE_BIOMETRIC") + @EnforcePermission("USE_BIOMETRIC_INTERNAL") List<FaceSensorPropertiesInternal> getSensorPropertiesInternal(String opPackageName); // Retrieve static sensor properties for the specified sensor diff --git a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java index 8759a6a26348..8e6f6dc09217 100644 --- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java +++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java @@ -767,20 +767,20 @@ final class IRemoteInputConnectionInvoker { /** * Invokes {@link IRemoteInputConnection#requestTextBoundsInfo(InputConnectionCommandHeader, * RectF, ResultReceiver)} - * @param rectF {@code rectF} parameter to be passed. + * @param bounds {@code rectF} parameter to be passed. * @param executor {@code Executor} parameter to be passed. * @param consumer {@code Consumer} parameter to be passed. */ @AnyThread public void requestTextBoundsInfo( - @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor, + @NonNull RectF bounds, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<TextBoundsInfoResult> consumer) { Objects.requireNonNull(executor); Objects.requireNonNull(consumer); final ResultReceiver resultReceiver = new TextBoundsInfoResultReceiver(executor, consumer); try { - mConnection.requestTextBoundsInfo(createHeader(), rectF, resultReceiver); + mConnection.requestTextBoundsInfo(createHeader(), bounds, resultReceiver); } catch (RemoteException e) { executor.execute(() -> consumer.accept(new TextBoundsInfoResult(CODE_CANCELLED))); } diff --git a/core/java/android/inputmethodservice/RemoteInputConnection.java b/core/java/android/inputmethodservice/RemoteInputConnection.java index f93f9abc3bb0..ec26ace79cd8 100644 --- a/core/java/android/inputmethodservice/RemoteInputConnection.java +++ b/core/java/android/inputmethodservice/RemoteInputConnection.java @@ -476,9 +476,9 @@ final class RemoteInputConnection implements InputConnection { @AnyThread public void requestTextBoundsInfo( - @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor, + @NonNull RectF bounds, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<TextBoundsInfoResult> consumer) { - mInvoker.requestTextBoundsInfo(rectF, executor, consumer); + mInvoker.requestTextBoundsInfo(bounds, executor, consumer); } @AnyThread diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index def0cbd749d4..00676f3cb746 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -358,16 +358,16 @@ public class Binder implements IBinder { * Return the Linux UID assigned to the process that sent the transaction * currently being processed. * - * Logs WTF if the current thread is not currently + * Slog.wtf if the current thread is not currently * executing an incoming transaction and the calling identity has not been * explicitly set with {@link #clearCallingIdentity()} * * @hide */ - public static final int getCallingUidOrWtf() { + public static final int getCallingUidOrWtf(String message) { if (!isDirectlyHandlingTransaction() && !hasExplicitIdentity()) { - Log.wtfStack(TAG, - "Thread is not in a binder transaction, " + Slog.wtf(TAG, + message + ": Thread is not in a binder transaction, " + "and the calling identity has not been " + "explicitly set with clearCallingIdentity"); } diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 0e4c2b2928d6..b016c7815ae7 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -1472,6 +1472,21 @@ public class UserManager { public static final String DISALLOW_BIOMETRIC = "disallow_biometric"; /** + * Specifies whether the user is allowed to modify default apps in settings. + * + * <p>This restriction can be set by device or profile owner. + * + * <p>The default value is <code>false</code>. + * + * <p>Key for user restrictions. + * <p>Type: Boolean + * @see DevicePolicyManager#addUserRestriction(ComponentName, String) + * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CONFIG_DEFAULT_APPS = "disallow_config_default_apps"; + + /** * Application restriction key that is used to indicate the pending arrival * of real restrictions for the app. * diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 190b73879f5c..ec3ef9de5c7d 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -22,6 +22,8 @@ import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.PermissionMethod; +import android.annotation.PermissionName; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; @@ -48,7 +50,6 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.pm.PermissionName; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; @@ -9986,11 +9987,10 @@ public final class Settings { "fingerprint_side_fps_auth_downtime"; /** - * Whether or not a SFPS device is required to be interactive for auth to unlock the device. + * Whether or not a SFPS device is enabling the performant auth setting. * @hide */ - public static final String SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED = - "sfps_require_screen_on_to_auth_enabled"; + public static final String SFPS_PERFORMANT_AUTH_ENABLED = "sfps_performant_auth_enabled"; /** * Whether or not debugging is enabled. @@ -18503,6 +18503,8 @@ public final class Settings { * @see #checkCallingPermission * @hide */ + @PermissionMethod(orSelf = true) + @PackageManager.PermissionResult public static int checkCallingOrSelfPermission(@NonNull @PermissionName String permission) { return ActivityThread.currentApplication() .getApplicationContext().checkCallingOrSelfPermission(permission); diff --git a/core/java/android/security/rkp/IRegistration.aidl b/core/java/android/security/rkp/IRegistration.aidl index 6522a458de4e..8ec13b9f94e4 100644 --- a/core/java/android/security/rkp/IRegistration.aidl +++ b/core/java/android/security/rkp/IRegistration.aidl @@ -17,6 +17,7 @@ package android.security.rkp; import android.security.rkp.IGetKeyCallback; +import android.security.rkp.IStoreUpgradedKeyCallback; /** * This interface is associated with the registration of an @@ -70,16 +71,18 @@ oneway interface IRegistration { * mechanism, see the documentation for IKeyMintDevice.upgradeKey. * * Once a key has been upgraded, the IRegistration where the key is stored - * needs to be told about the new blob. After calling storeUpgradedKey, + * needs to be told about the new blob. After calling storeUpgradedKeyAsync, * getKey will return the new key blob instead of the old one. * * Note that this function does NOT extend the lifetime of key blobs. The * certificate for the key is unchanged, and the key will still expire at - * the same time it would have if storeUpgradedKey had never been called. + * the same time it would have if storeUpgradedKeyAsync had never been called. * * @param oldKeyBlob The old key blob to be replaced by {@code newKeyBlob}. - * * @param newKeyblob The new blob to replace {@code oldKeyBlob}. + * @param callback Receives the result of the call. A callback must only + * be used with one {@code storeUpgradedKeyAsync} call at a time. */ - void storeUpgradedKey(in byte[] oldKeyBlob, in byte[] newKeyBlob); + void storeUpgradedKeyAsync( + in byte[] oldKeyBlob, in byte[] newKeyBlob, IStoreUpgradedKeyCallback callback); } diff --git a/core/java/android/content/pm/PermissionName.java b/core/java/android/security/rkp/IStoreUpgradedKeyCallback.aidl index 719e13be05cc..7f72fa050fd6 100644 --- a/core/java/android/content/pm/PermissionName.java +++ b/core/java/android/security/rkp/IStoreUpgradedKeyCallback.aidl @@ -14,22 +14,26 @@ * limitations under the License. */ -package android.content.pm; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.LOCAL_VARIABLE; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.CLASS; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; +package android.security.rkp; /** - * Denotes that the annotated {@link String} represents a permission name. + * Callback interface for storing an upgraded remotely provisioned key blob. + * {@link IRegistration}. * * @hide */ -@Retention(CLASS) -@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) -public @interface PermissionName {} +oneway interface IStoreUpgradedKeyCallback { + /** + * Called in response to {@link IRegistration.storeUpgradedKeyAsync}, indicating + * a remotely-provisioned key is available. + */ + void onSuccess(); + + /** + * Called when an error has occurred while trying to store an upgraded + * remotely provisioned key. + * + * @param error A description of what failed, suitable for logging. + */ + void onError(String error); +} diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java index f89ad8e6e429..6a10a6ac891d 100644 --- a/core/java/android/service/credentials/CredentialProviderInfo.java +++ b/core/java/android/service/credentials/CredentialProviderInfo.java @@ -24,6 +24,7 @@ import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; @@ -58,6 +59,7 @@ public final class CredentialProviderInfo { private final Drawable mIcon; @Nullable private final CharSequence mLabel; + private final boolean mIsSystemProvider; /** * Constructs an information instance of the credential provider. @@ -65,13 +67,14 @@ public final class CredentialProviderInfo { * @param context the context object * @param serviceComponent the serviceComponent of the provider service * @param userId the android userId for which the current process is running + * @param isSystemProvider whether this provider is a system provider * @throws PackageManager.NameNotFoundException If provider service is not found * @throws SecurityException If provider does not require the relevant permission */ public CredentialProviderInfo(@NonNull Context context, - @NonNull ComponentName serviceComponent, int userId) + @NonNull ComponentName serviceComponent, int userId, boolean isSystemProvider) throws PackageManager.NameNotFoundException { - this(context, getServiceInfoOrThrow(serviceComponent, userId)); + this(context, getServiceInfoOrThrow(serviceComponent, userId), isSystemProvider); } /** @@ -79,8 +82,11 @@ public final class CredentialProviderInfo { * @param context the context object * @param serviceInfo the service info for the provider app. This must be retrieved from the * {@code PackageManager} + * @param isSystemProvider whether the provider is a system app or not */ - public CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) { + public CredentialProviderInfo(@NonNull Context context, + @NonNull ServiceInfo serviceInfo, + boolean isSystemProvider) { if (!Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE.equals(serviceInfo.permission)) { Log.i(TAG, "Credential Provider Service from : " + serviceInfo.packageName + "does not require permission" @@ -95,6 +101,7 @@ public final class CredentialProviderInfo { mLabel = mServiceInfo.loadSafeLabel( mContext.getPackageManager(), 0 /* do not ellipsize */, TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM); + mIsSystemProvider = isSystemProvider; Log.i(TAG, "mLabel is : " + mLabel + ", for: " + mServiceInfo.getComponentName() .flattenToString()); populateProviderCapabilities(context, serviceInfo); @@ -147,6 +154,42 @@ public final class CredentialProviderInfo { } /** + * Returns the valid credential provider services available for the user with the + * given {@code userId}. + */ + @NonNull + public static List<CredentialProviderInfo> getAvailableSystemServices( + @NonNull Context context, + @UserIdInt int userId) { + final List<CredentialProviderInfo> services = new ArrayList<>(); + + final List<ResolveInfo> resolveInfos = + context.getPackageManager().queryIntentServicesAsUser( + new Intent(CredentialProviderService.SYSTEM_SERVICE_INTERFACE), + PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA), + userId); + for (ResolveInfo resolveInfo : resolveInfos) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + try { + ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo( + serviceInfo.packageName, + PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY)); + if (appInfo != null + && context.checkPermission(Manifest.permission.SYSTEM_CREDENTIAL_PROVIDER, + /*pId=*/-1, appInfo.uid) == PackageManager.PERMISSION_GRANTED) { + services.add(new CredentialProviderInfo(context, serviceInfo, + /*isSystemProvider=*/true)); + } + } catch (SecurityException e) { + Log.i(TAG, "Error getting info for " + serviceInfo + ": " + e); + } catch (PackageManager.NameNotFoundException e) { + Log.i(TAG, "Error getting info for " + serviceInfo + ": " + e); + } + } + return services; + } + + /** * Returns true if the service supports the given {@code credentialType}, false otherwise. */ @NonNull @@ -160,6 +203,10 @@ public final class CredentialProviderInfo { return mServiceInfo; } + public boolean isSystemProvider() { + return mIsSystemProvider; + } + /** Returns the service icon. */ @Nullable public Drawable getServiceIcon() { @@ -195,7 +242,8 @@ public final class CredentialProviderInfo { for (ResolveInfo resolveInfo : resolveInfos) { final ServiceInfo serviceInfo = resolveInfo.serviceInfo; try { - services.add(new CredentialProviderInfo(context, serviceInfo)); + services.add(new CredentialProviderInfo(context, + serviceInfo, false)); } catch (SecurityException e) { Log.w(TAG, "Error getting info for " + serviceInfo + ": " + e); } diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index 70dd16c5ca3a..ee386c31a984 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -125,6 +125,33 @@ public abstract class CredentialProviderService extends Service { public static final String EXTRA_CREATE_CREDENTIAL_EXCEPTION = "android.service.credentials.extra.CREATE_CREDENTIAL_EXCEPTION"; + /** + * Intent extra: The {@link BeginGetCredentialRequest} attached with + * the {@code pendingIntent} that is invoked when the user selects an + * authentication entry (intending to unlock the provider app) on the UI. + * + * <p>When a provider app receives a {@link BeginGetCredentialRequest} through the + * {@link CredentialProviderService#onBeginGetCredential} call, it can construct the + * {@link BeginGetCredentialResponse} with either an authentication {@link Action} (if the app + * is locked), or a {@link CredentialsResponseContent} (if the app is unlocked). In the former + * case, i.e. the app is locked, user will be shown the authentication action. When selected, + * the underlying {@link PendingIntent} will be invoked which will lead the user to provider's + * unlock activity. This pending intent will also contain the original + * {@link BeginGetCredentialRequest} to be retrieved and processed after the unlock + * flow is complete. + * + * <p>After the app is unlocked, the {@link BeginGetCredentialResponse} must be constructed + * using a {@link CredentialsResponseContent}, which must be set on an {@link Intent} as an + * intent extra against CredentialProviderService#EXTRA_CREDENTIALS_RESPONSE_CONTENT}. + * This intent should then be set as a result through {@link android.app.Activity#setResult} + * before finishing the activity. + * + * <p> + * Type: {@link BeginGetCredentialRequest} + */ + public static final String EXTRA_BEGIN_GET_CREDENTIAL_REQUEST = + "android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST"; + private static final String TAG = "CredProviderService"; public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; @@ -140,6 +167,20 @@ public abstract class CredentialProviderService extends Service { public static final String SERVICE_INTERFACE = "android.service.credentials.CredentialProviderService"; + /** + * The {@link Intent} that must be declared as handled by a system credential provider + * service. + * + * <p>The service must also require the + * {android.Manifest.permission#BIND_CREDENTIAL_PROVIDER_SERVICE} permission + * so that only the system can bind to it. + * + * @hide + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SYSTEM_SERVICE_INTERFACE = + "android.service.credentials.system.CredentialProviderService"; + @CallSuper @Override public void onCreate() { diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java index a2fa1392b079..a3892238f1e6 100644 --- a/core/java/android/service/dreams/DreamActivity.java +++ b/core/java/android/service/dreams/DreamActivity.java @@ -58,11 +58,13 @@ public class DreamActivity extends Activity { setTitle(title); } - final Bundle extras = getIntent().getExtras(); - mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK); - - if (mCallback != null) { + final Object callback = getIntent().getExtras().getBinder(EXTRA_CALLBACK); + if (callback instanceof DreamService.DreamActivityCallbacks) { + mCallback = (DreamService.DreamActivityCallbacks) callback; mCallback.onActivityCreated(this); + } else { + mCallback = null; + finishAndRemoveTask(); } } diff --git a/core/java/android/text/SegmentFinder.java b/core/java/android/text/SegmentFinder.java index be0094b28509..047d07a2e3e0 100644 --- a/core/java/android/text/SegmentFinder.java +++ b/core/java/android/text/SegmentFinder.java @@ -74,7 +74,7 @@ public abstract class SegmentFinder { /** * The default {@link SegmentFinder} implementation based on given segment ranges. */ - public static class DefaultSegmentFinder extends SegmentFinder { + public static class PrescribedSegmentFinder extends SegmentFinder { private final int[] mSegments; /** @@ -87,7 +87,7 @@ public abstract class SegmentFinder { * @throws IllegalArgumentException if the given segments array's length is not even; the * given segments are not sorted or there are segments overlap with others. */ - public DefaultSegmentFinder(@NonNull int[] segments) { + public PrescribedSegmentFinder(@NonNull int[] segments) { checkSegmentsValid(segments); mSegments = segments; } diff --git a/core/java/android/text/TextShaper.java b/core/java/android/text/TextShaper.java index a1d6cc8e283a..6da0b63dbc1f 100644 --- a/core/java/android/text/TextShaper.java +++ b/core/java/android/text/TextShaper.java @@ -173,7 +173,7 @@ public class TextShaper { private TextShaper() {} /** - * An consumer interface for accepting text shape result. + * A consumer interface for accepting text shape result. */ public interface GlyphsConsumer { /** diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 5a9a2520d839..8683cc2a8009 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -555,6 +555,7 @@ public final class InputDevice implements Parcelable { private String mKeyboardLanguageTag = null; private String mKeyboardLayoutType = null; private boolean mSupportsUsi = false; + private List<MotionRange> mMotionRanges = new ArrayList<>(); /** @see InputDevice#getId() */ public Builder setId(int id) { @@ -670,12 +671,50 @@ public final class InputDevice implements Parcelable { return this; } + /** @see InputDevice#getMotionRanges() */ + public Builder addMotionRange(int axis, int source, + float min, float max, float flat, float fuzz, float resolution) { + mMotionRanges.add(new MotionRange(axis, source, min, max, flat, fuzz, resolution)); + return this; + } + /** Build {@link InputDevice}. */ public InputDevice build() { - return new InputDevice(mId, mGeneration, mControllerNumber, mName, mVendorId, - mProductId, mDescriptor, mIsExternal, mSources, mKeyboardType, mKeyCharacterMap, - mKeyboardLanguageTag, mKeyboardLayoutType, mHasVibrator, mHasMicrophone, - mHasButtonUnderPad, mHasSensor, mHasBattery, mSupportsUsi); + InputDevice device = new InputDevice( + mId, + mGeneration, + mControllerNumber, + mName, + mVendorId, + mProductId, + mDescriptor, + mIsExternal, + mSources, + mKeyboardType, + mKeyCharacterMap, + mKeyboardLanguageTag, + mKeyboardLayoutType, + mHasVibrator, + mHasMicrophone, + mHasButtonUnderPad, + mHasSensor, + mHasBattery, + mSupportsUsi); + + final int numRanges = mMotionRanges.size(); + for (int i = 0; i < numRanges; i++) { + final MotionRange range = mMotionRanges.get(i); + device.addMotionRange( + range.getAxis(), + range.getSource(), + range.getMin(), + range.getMax(), + range.getFlat(), + range.getFuzz(), + range.getResolution()); + } + + return device; } } @@ -1378,7 +1417,8 @@ public final class InputDevice implements Parcelable { out.writeInt(mHasBattery ? 1 : 0); out.writeInt(mSupportsUsi ? 1 : 0); - final int numRanges = mMotionRanges.size(); + int numRanges = mMotionRanges.size(); + numRanges = numRanges > MAX_RANGES ? MAX_RANGES : numRanges; out.writeInt(numRanges); for (int i = 0; i < numRanges; i++) { MotionRange range = mMotionRanges.get(i); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 3502c34091a2..2114ce7d7ffa 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1110,6 +1110,8 @@ public final class ViewRootImpl implements ViewParent, // Update the last resource config in case the resource configuration was changed while // activity relaunched. updateLastConfigurationFromResources(getConfiguration()); + // Make sure to report the completion of draw for relaunch with preserved window. + reportNextDraw("rebuilt"); } private Configuration getConfiguration() { @@ -1424,7 +1426,7 @@ public final class ViewRootImpl implements ViewParent, != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; if (registered) { final AccessibilityWindowAttributes attributes = new AccessibilityWindowAttributes( - mWindowAttributes); + mWindowAttributes, mContext.getResources().getConfiguration().getLocales()); if (!attributes.equals(mAccessibilityWindowAttributes)) { mAccessibilityWindowAttributes = attributes; mAccessibilityManager.setAccessibilityWindowAttributes(getDisplayId(), diff --git a/core/java/android/view/WindowInfo.java b/core/java/android/view/WindowInfo.java index 11d63c84d142..27dca0af81be 100644 --- a/core/java/android/view/WindowInfo.java +++ b/core/java/android/view/WindowInfo.java @@ -20,6 +20,7 @@ import android.app.ActivityTaskManager; import android.graphics.Matrix; import android.graphics.Region; import android.os.IBinder; +import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; import android.util.Pools; @@ -60,6 +61,8 @@ public class WindowInfo implements Parcelable { public MagnificationSpec mMagnificationSpec = new MagnificationSpec(); + public LocaleList locales = LocaleList.getEmptyLocaleList(); + private WindowInfo() { /* do nothing - hide constructor */ } @@ -99,6 +102,7 @@ public class WindowInfo implements Parcelable { } } window.mMagnificationSpec.setTo(other.mMagnificationSpec); + window.locales = other.locales; return window; } @@ -136,6 +140,7 @@ public class WindowInfo implements Parcelable { parcel.writeInt(0); } mMagnificationSpec.writeToParcel(parcel, flags); + parcel.writeParcelable(locales, flags); } @Override @@ -160,6 +165,7 @@ public class WindowInfo implements Parcelable { matrix.setValues(mTransformMatrix); builder.append(", mTransformMatrix=").append(matrix); builder.append(", mMagnificationSpec=").append(mMagnificationSpec); + builder.append(", locales=").append(locales); builder.append(']'); return builder.toString(); } @@ -187,6 +193,7 @@ public class WindowInfo implements Parcelable { parcel.readBinderList(childTokens); } mMagnificationSpec = MagnificationSpec.CREATOR.createFromParcel(parcel); + locales = parcel.readParcelable(null, LocaleList.class); } private void clear() { @@ -210,6 +217,7 @@ public class WindowInfo implements Parcelable { mMagnificationSpec.clear(); title = null; accessibilityIdOfAnchor = AccessibilityNodeInfo.UNDEFINED_NODE_ID; + locales = LocaleList.getEmptyLocaleList(); } public static final @android.annotation.NonNull Parcelable.Creator<WindowInfo> CREATOR = diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index d0f0d4a41ec2..35f1787c0bb5 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -853,6 +853,143 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"; /** + * Activity level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the activity should be excluded from the + * camera compatibility force rotation treatment. + * + * <p>The camera compatibility treatment aligns orientations of portrait app window and natural + * orientation of the device and set opposite to natural orientation for a landscape app + * window. Mismatch between them can lead to camera issues like sideways or stretched + * viewfinder since this is one of the strongest assumptions that apps make when they implement + * camera previews. Since app and natural display orientations aren't guaranteed to match, the + * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to + * camera and is removed once camera is closed. + * + * <p>The camera compatibility can be enabled by device manufacturers on the displays that have + * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed + * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> + * for more details). + * + * <p>With this property set to {@code true} or unset, the system may apply the force rotation + * treatment to fixed orientation activities. Device manufacturers can exclude packages from the + * treatment using their discretion to improve display compatibility. + * + * <p>With this property set to {@code false}, the system will not apply the force rotation + * treatment. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION" + * android:value="true|false"/> + * </activity> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION = + "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION"; + + /** + * Activity level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the activity should be excluded + * from the activity "refresh" after the camera compatibility force rotation treatment. + * + * <p>The camera compatibility treatment aligns orientations of portrait app window and natural + * orientation of the device and set opposite to natural orientation for a landscape app + * window. Mismatch between them can lead to camera issues like sideways or stretched + * viewfinder since this is one of the strongest assumptions that apps make when they implement + * camera previews. Since app and natural display orientations aren't guaranteed to match, the + * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to + * camera and is removed once camera is closed. + * + * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed -> + * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle + * (if overridden, see {@link #PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE} for context). + * This allows to clear cached values in apps (e.g. display or camera rotation) that influence + * camera preview and can lead to sideways or stretching issues persisting even after force + * rotation. + * + * <p>The camera compatibility can be enabled by device manufacturers on the displays that have + * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed + * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> + * for more details). + * + * <p>With this property set to {@code true} or unset, the system may "refresh" activity after + * the force rotation treatment. Device manufacturers can exclude packages from the "refresh" + * using their discretion to improve display compatibility. + * + * <p>With this property set to {@code false}, the system will not "refresh" activity after the + * force rotation treatment. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH" + * android:value="true|false"/> + * </activity> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH = + "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH"; + + /** + * Activity level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the activity should be or shouldn't be + * "refreshed" after the camera compatibility force rotation treatment using "paused -> + * resumed" cycle rather than "stopped -> resumed". + * + * <p>The camera compatibility treatment aligns orientations of portrait app window and natural + * orientation of the device and set opposite to natural orientation for a landscape app + * window. Mismatch between them can lead to camera issues like sideways or stretched + * viewfinder since this is one of the strongest assumptions that apps make when they implement + * camera previews. Since app and natural display orientations aren't guaranteed to match, the + * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to + * camera and is removed once camera is closed. + * + * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed -> + * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle + * (if overridden by device manufacturers or using this property). This allows to clear cached + * values in apps (e.g., display or camera rotation) that influence camera preview and can lead + * to sideways or stretching issues persisting even after force rotation. + * + * <p>The camera compatibility can be enabled by device manufacturers on the displays that have + * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed + * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> + * for more details). + * + * <p>Device manufacturers can override packages to "refresh" via "resumed -> paused -> resumed" + * cycle using their discretion to improve display compatibility. + * + * <p>With this property set to {@code true}, the system will "refresh" activity after the + * force rotation treatment using "resumed -> paused -> resumed" cycle. + * + * <p>With this property set to {@code false}, the system will not "refresh" activity after the + * force rotation treatment using "resumed -> paused -> resumed" cycle even if the device + * manufacturer adds the corresponding override. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE" + * android:value="true|false"/> + * </activity> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = + "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE"; + + /** * @hide */ public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array"; diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java index a757236f92fd..dd320e196e8b 100644 --- a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java +++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java @@ -83,7 +83,7 @@ public abstract class AccessibilityDisplayProxy { * @param displayId the id of the display to proxy. * @param executor the executor used to execute proxy callbacks. * @param installedAndEnabledServices the list of infos representing the installed and - * enabled a11y services. + * enabled accessibility services. */ public AccessibilityDisplayProxy(int displayId, @NonNull Executor executor, @NonNull List<AccessibilityServiceInfo> installedAndEnabledServices) { @@ -147,19 +147,27 @@ public abstract class AccessibilityDisplayProxy { } /** - * Gets the focus of the window specified by {@code windowInfo}. + * Gets the node with focus, in this display. * - * @param windowInfo the window to search - * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or + * <p>For {@link AccessibilityNodeInfo#FOCUS_INPUT}, this returns the input-focused node in the + * proxy display if this display can receive unspecified input events (input that does not + * specify a target display.) + * + * <p>For {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}, this returns the + * accessibility-focused node in the proxy display if the display has accessibility focus. + * @param focusType The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or * {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}. * @return The node info of the focused view or null. - * @hide - * TODO(254545943): Do not expose until support for accessibility focus and/or input is in place + */ @Nullable - public AccessibilityNodeInfo findFocus(@NonNull AccessibilityWindowInfo windowInfo, int focus) { - AccessibilityNodeInfo windowRoot = windowInfo.getRoot(); - return windowRoot != null ? windowRoot.findFocus(focus) : null; + public AccessibilityNodeInfo findFocus(int focusType) { + // TODO(264423198): Support querying the focused node of the proxy's display even if it is + // not the top-focused display and can't receive untargeted input events. + // TODO(254545943): Separate accessibility focus between proxy and phone state. + return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId, + AccessibilityWindowInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, + focusType); } /** @@ -177,10 +185,10 @@ public abstract class AccessibilityDisplayProxy { * Sets the list of {@link AccessibilityServiceInfo}s describing the services interested in the * {@link AccessibilityDisplayProxy}'s display. * - * <p>These represent a11y features and services that are installed and running. These should - * not include {@link AccessibilityService}s installed on the phone. + * <p>These represent accessibility features and services that are installed and running. These + * should not include {@link AccessibilityService}s installed on the phone. * - * @param installedAndEnabledServices the list of installed and running a11y services. + * @param installedAndEnabledServices the list of installed and running accessibility services. */ public void setInstalledAndEnabledServices( @NonNull List<AccessibilityServiceInfo> installedAndEnabledServices) { diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 52eda0a19c55..83a6c5a2e8a6 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -921,8 +921,6 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link AccessibilityWindowInfo#ACTIVE_WINDOW_ID} - * to query the currently active window. Use * {@link AccessibilityWindowInfo#ANY_WINDOW_ID} to query all * windows * @param accessibilityNodeId A unique view id or virtual descendant id from diff --git a/core/java/android/view/accessibility/AccessibilityWindowAttributes.java b/core/java/android/view/accessibility/AccessibilityWindowAttributes.java index 562300c62f6d..92ed73b22e78 100644 --- a/core/java/android/view/accessibility/AccessibilityWindowAttributes.java +++ b/core/java/android/view/accessibility/AccessibilityWindowAttributes.java @@ -17,11 +17,14 @@ package android.view.accessibility; import android.annotation.NonNull; +import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.view.WindowManager; +import java.util.Objects; + /** * This class represents the attributes of a window needed for {@link AccessibilityWindowInfo}. * @@ -30,13 +33,22 @@ import android.view.WindowManager; public final class AccessibilityWindowAttributes implements Parcelable { private final CharSequence mWindowTitle; + private final LocaleList mLocales; - public AccessibilityWindowAttributes(@NonNull WindowManager.LayoutParams layoutParams) { + public AccessibilityWindowAttributes(@NonNull WindowManager.LayoutParams layoutParams, + @NonNull LocaleList locales) { mWindowTitle = populateWindowTitle(layoutParams); + mLocales = locales; } private AccessibilityWindowAttributes(Parcel in) { mWindowTitle = in.readCharSequence(); + LocaleList inLocales = in.readParcelable(null, LocaleList.class); + if (inLocales != null) { + mLocales = inLocales; + } else { + mLocales = LocaleList.getEmptyLocaleList(); + } } public CharSequence getWindowTitle() { @@ -63,6 +75,10 @@ public final class AccessibilityWindowAttributes implements Parcelable { return windowTitle; } + public @NonNull LocaleList getLocales() { + return mLocales; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -70,12 +86,13 @@ public final class AccessibilityWindowAttributes implements Parcelable { AccessibilityWindowAttributes that = (AccessibilityWindowAttributes) o; - return TextUtils.equals(mWindowTitle, that.mWindowTitle); + return TextUtils.equals(mWindowTitle, that.mWindowTitle) && Objects.equals( + mLocales, that.mLocales); } @Override public int hashCode() { - return mWindowTitle.hashCode(); + return Objects.hash(mWindowTitle, mLocales); } public static final Creator<AccessibilityWindowAttributes> CREATOR = @@ -99,12 +116,14 @@ public final class AccessibilityWindowAttributes implements Parcelable { @Override public void writeToParcel(@NonNull Parcel parcel, int flags) { parcel.writeCharSequence(mWindowTitle); + parcel.writeParcelable(mLocales, flags); } @Override public String toString() { return "AccessibilityWindowAttributes{" + "mAccessibilityWindowTitle=" + mWindowTitle + + "mLocales=" + mLocales + '}'; } } diff --git a/core/java/android/view/accessibility/AccessibilityWindowInfo.java b/core/java/android/view/accessibility/AccessibilityWindowInfo.java index 9be999053fa6..d84e0fb421cf 100644 --- a/core/java/android/view/accessibility/AccessibilityWindowInfo.java +++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.java @@ -23,6 +23,7 @@ import android.annotation.UptimeMillisLong; import android.app.ActivityTaskManager; import android.graphics.Rect; import android.graphics.Region; +import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; @@ -132,6 +133,8 @@ public final class AccessibilityWindowInfo implements Parcelable { private int mConnectionId = UNDEFINED_CONNECTION_ID; + private LocaleList mLocales = LocaleList.getEmptyLocaleList(); + /** * Creates a new {@link AccessibilityWindowInfo}. */ @@ -555,6 +558,26 @@ public final class AccessibilityWindowInfo implements Parcelable { } /** + * Sets the locales of the window. Locales are populated by the view root by default. + * + * @param locales The {@link android.os.LocaleList}. + * + * @hide + */ + public void setLocales(@NonNull LocaleList locales) { + mLocales = locales; + } + + /** + * Return the {@link android.os.LocaleList} of the window. + * + * @return the locales of the window. + */ + public @NonNull LocaleList getLocales() { + return mLocales; + } + + /** * Returns a cached instance if such is available or a new one is * created. * @@ -676,6 +699,7 @@ public final class AccessibilityWindowInfo implements Parcelable { } parcel.writeInt(mConnectionId); + parcel.writeParcelable(mLocales, flags); } /** @@ -706,6 +730,7 @@ public final class AccessibilityWindowInfo implements Parcelable { } mConnectionId = other.mConnectionId; + mLocales = other.mLocales; } private void initFromParcel(Parcel parcel) { @@ -733,6 +758,7 @@ public final class AccessibilityWindowInfo implements Parcelable { } mConnectionId = parcel.readInt(); + mLocales = parcel.readParcelable(null, LocaleList.class); } @Override diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index d067d4bc366b..497f0668107f 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -66,8 +66,7 @@ import java.util.concurrent.Executor; import java.util.function.Consumer; /** - * <p>The {@link ContentCaptureManager} provides additional ways for for apps to - * integrate with the content capture subsystem. + * <p>Provides additional ways for apps to integrate with the content capture subsystem. * * <p>Content capture provides real-time, continuous capture of application activity, display and * events to an intelligence service that is provided by the Android system. The intelligence diff --git a/core/java/android/view/inputmethod/HandwritingGesture.java b/core/java/android/view/inputmethod/HandwritingGesture.java index 251626941eee..e0fc426e1467 100644 --- a/core/java/android/view/inputmethod/HandwritingGesture.java +++ b/core/java/android/view/inputmethod/HandwritingGesture.java @@ -82,38 +82,60 @@ public abstract class HandwritingGesture { @IntDef({GRANULARITY_CHARACTER, GRANULARITY_WORD}) @interface Granularity {} - /** Undefined gesture type. */ + /** + * Undefined gesture type. + * @hide + */ + @TestApi public static final int GESTURE_TYPE_NONE = 0x0000; /** * Gesture of type {@link SelectGesture} to select an area of text. + * @hide */ + @TestApi public static final int GESTURE_TYPE_SELECT = 0x0001; /** * Gesture of type {@link InsertGesture} to insert text at a designated point. + * @hide */ + @TestApi public static final int GESTURE_TYPE_INSERT = 1 << 1; /** * Gesture of type {@link DeleteGesture} to delete an area of text. + * @hide */ + @TestApi public static final int GESTURE_TYPE_DELETE = 1 << 2; - /** Gesture of type {@link RemoveSpaceGesture} to remove whitespace from text. */ + /** + * Gesture of type {@link RemoveSpaceGesture} to remove whitespace from text. + * @hide + */ + @TestApi public static final int GESTURE_TYPE_REMOVE_SPACE = 1 << 3; - /** Gesture of type {@link JoinOrSplitGesture} to join or split text. */ + /** + * Gesture of type {@link JoinOrSplitGesture} to join or split text. + * @hide + */ + @TestApi public static final int GESTURE_TYPE_JOIN_OR_SPLIT = 1 << 4; /** * Gesture of type {@link SelectRangeGesture} to select range of text. + * @hide */ + @TestApi public static final int GESTURE_TYPE_SELECT_RANGE = 1 << 5; /** * Gesture of type {@link DeleteRangeGesture} to delete range of text. + * @hide */ + @TestApi public static final int GESTURE_TYPE_DELETE_RANGE = 1 << 6; /** diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index 9b519c3225e2..687253683dce 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -1262,13 +1262,13 @@ public interface InputConnection { /** * Called by input method to request the {@link TextBoundsInfo} for a range of text which is - * covered by or in vicinity of the given {@code RectF}. It can be used as a supplementary + * covered by or in vicinity of the given {@code bounds}. It can be used as a supplementary * method to implement the handwriting gesture API - * {@link #performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)}. * * <p><strong>Editor authors</strong>: It's preferred that the editor returns a * {@link TextBoundsInfo} of all the text lines whose bounds intersect with the given - * {@code rectF}. + * {@code bounds}. * </p> * * <p><strong>IME authors</strong>: This method is expensive when the text is long. Please @@ -1276,7 +1276,7 @@ public interface InputConnection { * consuming. It's preferable to only request text bounds in smaller areas. * </p> * - * @param rectF the interested area where the text bounds are requested, in the screen + * @param bounds the interested area where the text bounds are requested, in the screen * coordinates. * @param executor the executor to run the callback. * @param consumer the callback invoked by editor to return the result. It must return a @@ -1286,7 +1286,7 @@ public interface InputConnection { * @see android.view.inputmethod.TextBoundsInfoResult */ default void requestTextBoundsInfo( - @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor, + @NonNull RectF bounds, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<TextBoundsInfoResult> consumer) { Objects.requireNonNull(executor); Objects.requireNonNull(consumer); diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index 4befd6f026ca..5e323fac2d8c 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -362,9 +362,9 @@ public class InputConnectionWrapper implements InputConnection { */ @Override public void requestTextBoundsInfo( - @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor, + @NonNull RectF bounds, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<TextBoundsInfoResult> consumer) { - mTarget.requestTextBoundsInfo(rectF, executor, consumer); + mTarget.requestTextBoundsInfo(bounds, executor, consumer); } /** diff --git a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java index 7525d723b802..6f8b422da218 100644 --- a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java +++ b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java @@ -1106,7 +1106,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { @Dispatching(cancellable = true) @Override public void requestTextBoundsInfo( - InputConnectionCommandHeader header, RectF rectF, + InputConnectionCommandHeader header, RectF bounds, @NonNull ResultReceiver resultReceiver) { dispatchWithTracing("requestTextBoundsInfo", () -> { if (header.mSessionId != mCurrentSessionId.get()) { @@ -1121,7 +1121,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { } ic.requestTextBoundsInfo( - rectF, + bounds, Runnable::run, (textBoundsInfoResult) -> { final int resultCode = textBoundsInfoResult.getResultCode(); diff --git a/core/java/android/view/inputmethod/TextBoundsInfo.java b/core/java/android/view/inputmethod/TextBoundsInfo.java index dd055437b561..d42d94e91933 100644 --- a/core/java/android/view/inputmethod/TextBoundsInfo.java +++ b/core/java/android/view/inputmethod/TextBoundsInfo.java @@ -43,8 +43,8 @@ import java.util.function.Consumer; * The text bounds information of a slice of text in the editor. * * <p> This class provides IME the layout information of the text within the range from - * {@link #getStart()} to {@link #getEnd()}. It's intended to be used by IME as a supplementary API - * to support handwriting gestures. + * {@link #getStartIndex()} to {@link #getEndIndex()}. It's intended to be used by IME as a + * supplementary API to support handwriting gestures. * </p> */ public final class TextBoundsInfo implements Parcelable { @@ -168,16 +168,13 @@ public final class TextBoundsInfo implements Parcelable { private final SegmentFinder mGraphemeSegmentFinder; /** - * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation + * Set the given {@link android.graphics.Matrix} to be the transformation * matrix that is to be applied other positional data in this class. - * - * @return a new instance (copy) of the transformation matrix. */ @NonNull - public Matrix getMatrix() { - final Matrix matrix = new Matrix(); + public void getMatrix(@NonNull Matrix matrix) { + Objects.requireNonNull(matrix); matrix.setValues(mMatrixValues); - return matrix; } /** @@ -186,7 +183,7 @@ public final class TextBoundsInfo implements Parcelable { * * @see Builder#setStartAndEnd(int, int) */ - public int getStart() { + public int getStartIndex() { return mStart; } @@ -196,28 +193,28 @@ public final class TextBoundsInfo implements Parcelable { * * @see Builder#setStartAndEnd(int, int) */ - public int getEnd() { + public int getEndIndex() { return mEnd; } /** - * Return the bounds of the character at the given {@code index}, in the coordinates of the - * editor. + * Set the bounds of the character at the given {@code index} to the given {@link RectF}, in + * the coordinates of the editor. * * @param index the index of the queried character. - * @return the bounding box of the queried character. + * @param bounds the {@link RectF} used to receive the result. * * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from * the {@code start} to the {@code end}. */ @NonNull - public RectF getCharacterBounds(int index) { + public void getCharacterBounds(int index, @NonNull RectF bounds) { if (index < mStart || index >= mEnd) { throw new IndexOutOfBoundsException("Index is out of the bounds of " + "[" + mStart + ", " + mEnd + ")."); } final int offset = 4 * (index - mStart); - return new RectF(mCharacterBounds[offset], mCharacterBounds[offset + 1], + bounds.set(mCharacterBounds[offset], mCharacterBounds[offset + 1], mCharacterBounds[offset + 2], mCharacterBounds[offset + 3]); } @@ -333,6 +330,16 @@ public final class TextBoundsInfo implements Parcelable { * won't check the text in the ranges of [5, 7) and [12, 15). * </p> * + * <p> Under the following conditions, this method will return -1 indicating that no valid + * character is found: + * <ul> + * <li> The given {@code y} coordinate is above the first line or below the last line (the + * first line or the last line is identified by the {@link SegmentFinder} returned from + * {@link #getLineSegmentFinder()}). </li> + * <li> There is no character in this {@link TextBoundsInfo}. </li> + * </ul> + * </p> + * * @param x the x coordinates of the interested location, in the editor's coordinates. * @param y the y coordinates of the interested location, in the editor's coordinates. * @return the index of the character whose position is closest to the given location. It will @@ -990,8 +997,8 @@ public final class TextBoundsInfo implements Parcelable { public static final class Builder { private final float[] mMatrixValues = new float[9]; private boolean mMatrixInitialized; - private int mStart; - private int mEnd; + private int mStart = -1; + private int mEnd = -1; private float[] mCharacterBounds; private int[] mCharacterFlags; private int[] mCharacterBidiLevels; @@ -999,6 +1006,17 @@ public final class TextBoundsInfo implements Parcelable { private SegmentFinder mWordSegmentFinder; private SegmentFinder mGraphemeSegmentFinder; + /** + * Create a builder for {@link TextBoundsInfo}. + * @param start the start index of the {@link TextBoundsInfo}, inclusive. + * @param end the end index of the {@link TextBoundsInfo}, exclusive. + * @throws IllegalArgumentException if the given {@code start} or {@code end} is negative, + * or {@code end} is smaller than the {@code start}. + */ + public Builder(int start, int end) { + setStartAndEnd(start, end); + } + /** Clear all the parameters set on this {@link Builder} to reuse it. */ @NonNull public Builder clear() { @@ -1152,7 +1170,7 @@ public final class TextBoundsInfo implements Parcelable { * * @see #getGraphemeSegmentFinder() * @see SegmentFinder - * @see SegmentFinder.DefaultSegmentFinder + * @see SegmentFinder.PrescribedSegmentFinder */ @NonNull public Builder setGraphemeSegmentFinder(@NonNull SegmentFinder graphemeSegmentFinder) { @@ -1171,7 +1189,7 @@ public final class TextBoundsInfo implements Parcelable { * * @see #getWordSegmentFinder() * @see SegmentFinder - * @see SegmentFinder.DefaultSegmentFinder + * @see SegmentFinder.PrescribedSegmentFinder */ @NonNull public Builder setWordSegmentFinder(@NonNull SegmentFinder wordSegmentFinder) { @@ -1193,7 +1211,7 @@ public final class TextBoundsInfo implements Parcelable { * * @see #getLineSegmentFinder() * @see SegmentFinder - * @see SegmentFinder.DefaultSegmentFinder + * @see SegmentFinder.PrescribedSegmentFinder */ @NonNull public Builder setLineSegmentFinder(@NonNull SegmentFinder lineSegmentFinder) { @@ -1360,7 +1378,7 @@ public final class TextBoundsInfo implements Parcelable { breaks = GrowingArrayUtils.append(breaks, count++, start + offset); } } - return new SegmentFinder.DefaultSegmentFinder(Arrays.copyOf(breaks, count)); + return new SegmentFinder.PrescribedSegmentFinder(Arrays.copyOf(breaks, count)); } /** diff --git a/core/java/android/webkit/TEST_MAPPING b/core/java/android/webkit/TEST_MAPPING index bd25200ffc38..c1bc6d720ece 100644 --- a/core/java/android/webkit/TEST_MAPPING +++ b/core/java/android/webkit/TEST_MAPPING @@ -9,6 +9,14 @@ ] }, { + "name": "CtsSdkSandboxWebkitTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { "name": "CtsHostsideWebViewTests", "options": [ { diff --git a/core/java/android/webkit/WebResourceError.java b/core/java/android/webkit/WebResourceError.java index 11f1b6f17566..4c874892d576 100644 --- a/core/java/android/webkit/WebResourceError.java +++ b/core/java/android/webkit/WebResourceError.java @@ -19,7 +19,7 @@ package android.webkit; import android.annotation.SystemApi; /** - * Encapsulates information about errors occured during loading of web resources. See + * Encapsulates information about errors that occurred during loading of web resources. See * {@link WebViewClient#onReceivedError(WebView, WebResourceRequest, WebResourceError) WebViewClient.onReceivedError(WebView, WebResourceRequest, WebResourceError)} */ public abstract class WebResourceError { diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java index ff2e17548df4..2bd5c8859b9f 100644 --- a/core/java/android/widget/MediaController.java +++ b/core/java/android/widget/MediaController.java @@ -218,6 +218,7 @@ public class MediaController extends FrameLayout { p.width = mAnchor.getWidth(); p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2; p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight(); + p.token = mAnchor.getWindowToken(); } // This is called whenever mAnchor's layout bound changes diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index b9b928e4c2f9..4005bc86af39 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -13245,7 +13245,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Creates the {@link TextBoundsInfo} for the text lines that intersects with the {@code rectF}. * @hide */ - public TextBoundsInfo getTextBoundsInfo(@NonNull RectF rectF) { + public TextBoundsInfo getTextBoundsInfo(@NonNull RectF bounds) { final Layout layout = getLayout(); if (layout == null) { // No valid text layout, return null. @@ -13268,19 +13268,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final float layoutLeft = viewportToContentHorizontalOffset(); final float layoutTop = viewportToContentVerticalOffset(); - final RectF localRectF = new RectF(rectF); - globalToLocalMatrix.mapRect(localRectF); - localRectF.offset(-layoutLeft, -layoutTop); + final RectF localBounds = new RectF(bounds); + globalToLocalMatrix.mapRect(localBounds); + localBounds.offset(-layoutLeft, -layoutTop); // Text length is 0. There is no character bounds, return empty TextBoundsInfo. // rectF doesn't intersect with the layout, return empty TextBoundsInfo. - if (!localRectF.intersects(0f, 0f, layout.getWidth(), layout.getHeight()) + if (!localBounds.intersects(0f, 0f, layout.getWidth(), layout.getHeight()) || text.length() == 0) { - final TextBoundsInfo.Builder builder = new TextBoundsInfo.Builder(); + final TextBoundsInfo.Builder builder = new TextBoundsInfo.Builder(0, 0); final SegmentFinder emptySegmentFinder = - new SegmentFinder.DefaultSegmentFinder(new int[0]); - builder.setStartAndEnd(0, 0) - .setMatrix(localToGlobalMatrix) + new SegmentFinder.PrescribedSegmentFinder(new int[0]); + builder.setMatrix(localToGlobalMatrix) .setCharacterBounds(new float[0]) .setCharacterBidiLevel(new int[0]) .setCharacterFlags(new int[0]) @@ -13290,8 +13289,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return builder.build(); } - final int startLine = layout.getLineForVertical((int) Math.floor(localRectF.top)); - final int endLine = layout.getLineForVertical((int) Math.floor(localRectF.bottom)); + final int startLine = layout.getLineForVertical((int) Math.floor(localBounds.top)); + final int endLine = layout.getLineForVertical((int) Math.floor(localBounds.bottom)); final int start = layout.getLineStart(startLine); final int end = layout.getLineEnd(endLine); @@ -13349,18 +13348,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener lineRanges[2 * offset] = layout.getLineStart(line); lineRanges[2 * offset + 1] = layout.getLineEnd(line); } - final SegmentFinder lineSegmentFinder = new SegmentFinder.DefaultSegmentFinder(lineRanges); + final SegmentFinder lineSegmentFinder = + new SegmentFinder.PrescribedSegmentFinder(lineRanges); - final TextBoundsInfo.Builder builder = new TextBoundsInfo.Builder(); - builder.setStartAndEnd(start, end) + return new TextBoundsInfo.Builder(start, end) .setMatrix(localToGlobalMatrix) .setCharacterBounds(characterBounds) .setCharacterBidiLevel(characterBidiLevels) .setCharacterFlags(characterFlags) .setGraphemeSegmentFinder(graphemeSegmentFinder) .setLineSegmentFinder(lineSegmentFinder) - .setWordSegmentFinder(wordSegmentFinder); - return builder.build(); + .setWordSegmentFinder(wordSegmentFinder) + .build(); } /** diff --git a/core/java/android/window/ITaskOrganizerController.aidl b/core/java/android/window/ITaskOrganizerController.aidl index e6bb1f64ad86..0032b9ce0512 100644 --- a/core/java/android/window/ITaskOrganizerController.aidl +++ b/core/java/android/window/ITaskOrganizerController.aidl @@ -40,7 +40,8 @@ interface ITaskOrganizerController { void unregisterTaskOrganizer(ITaskOrganizer organizer); /** Creates a persistent root task in WM for a particular windowing-mode. */ - void createRootTask(int displayId, int windowingMode, IBinder launchCookie); + void createRootTask(int displayId, int windowingMode, IBinder launchCookie, + boolean removeWithTaskOrganizer); /** Deletes a persistent root task in WM */ boolean deleteRootTask(in WindowContainerToken task); diff --git a/core/java/android/window/TaskFragmentAnimationParams.aidl b/core/java/android/window/TaskFragmentAnimationParams.aidl index 04dee58089d4..8ae84c22dda1 100644 --- a/core/java/android/window/TaskFragmentAnimationParams.aidl +++ b/core/java/android/window/TaskFragmentAnimationParams.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/java/android/window/TaskFragmentAnimationParams.java b/core/java/android/window/TaskFragmentAnimationParams.java index a600a4db42b8..12ad91498626 100644 --- a/core/java/android/window/TaskFragmentAnimationParams.java +++ b/core/java/android/window/TaskFragmentAnimationParams.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/java/android/window/TaskFragmentOperation.aidl b/core/java/android/window/TaskFragmentOperation.aidl index c21700c6634b..c1ed0c7846e3 100644 --- a/core/java/android/window/TaskFragmentOperation.aidl +++ b/core/java/android/window/TaskFragmentOperation.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index bec6c58e4c8a..0d6a58b166ae 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ package android.window; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Intent; +import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; @@ -30,41 +32,112 @@ import java.util.Objects; /** * Data object of params for TaskFragment related {@link WindowContainerTransaction} operation. * - * @see WindowContainerTransaction#setTaskFragmentOperation(IBinder, TaskFragmentOperation). + * @see WindowContainerTransaction#addTaskFragmentOperation(IBinder, TaskFragmentOperation). * @hide */ -// TODO(b/263436063): move other TaskFragment related operation here. public final class TaskFragmentOperation implements Parcelable { + /** + * Type for tracking other {@link WindowContainerTransaction} to TaskFragment that is not set + * through {@link TaskFragmentOperation}, such as {@link WindowContainerTransaction#setBounds}. + */ + public static final int OP_TYPE_UNKNOWN = -1; + + /** Creates a new TaskFragment. */ + public static final int OP_TYPE_CREATE_TASK_FRAGMENT = 0; + + /** Deletes the given TaskFragment. */ + public static final int OP_TYPE_DELETE_TASK_FRAGMENT = 1; + + /** Starts an Activity in the given TaskFragment. */ + public static final int OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT = 2; + + /** Reparents the given Activity to the given TaskFragment. */ + public static final int OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT = 3; + + /** Sets two TaskFragments adjacent to each other. */ + public static final int OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS = 4; + + /** Clears the adjacent TaskFragments relationship. */ + public static final int OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS = 5; + + /** Requests focus on the top running Activity in the given TaskFragment. */ + public static final int OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT = 6; + + /** Sets a given TaskFragment to have a companion TaskFragment. */ + public static final int OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 7; + /** Sets the {@link TaskFragmentAnimationParams} for the given TaskFragment. */ - public static final int OP_TYPE_SET_ANIMATION_PARAMS = 0; + public static final int OP_TYPE_SET_ANIMATION_PARAMS = 8; @IntDef(prefix = { "OP_TYPE_" }, value = { + OP_TYPE_UNKNOWN, + OP_TYPE_CREATE_TASK_FRAGMENT, + OP_TYPE_DELETE_TASK_FRAGMENT, + OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT, + OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT, + OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS, + OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS, + OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT, + OP_TYPE_SET_COMPANION_TASK_FRAGMENT, OP_TYPE_SET_ANIMATION_PARAMS }) @Retention(RetentionPolicy.SOURCE) - @interface OperationType {} + public @interface OperationType {} @OperationType private final int mOpType; @Nullable + private final TaskFragmentCreationParams mTaskFragmentCreationParams; + + @Nullable + private final IBinder mActivityToken; + + @Nullable + private final Intent mActivityIntent; + + @Nullable + private final Bundle mBundle; + + @Nullable + private final IBinder mSecondaryFragmentToken; + + @Nullable private final TaskFragmentAnimationParams mAnimationParams; private TaskFragmentOperation(@OperationType int opType, + @Nullable TaskFragmentCreationParams taskFragmentCreationParams, + @Nullable IBinder activityToken, @Nullable Intent activityIntent, + @Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken, @Nullable TaskFragmentAnimationParams animationParams) { mOpType = opType; + mTaskFragmentCreationParams = taskFragmentCreationParams; + mActivityToken = activityToken; + mActivityIntent = activityIntent; + mBundle = bundle; + mSecondaryFragmentToken = secondaryFragmentToken; mAnimationParams = animationParams; } private TaskFragmentOperation(Parcel in) { mOpType = in.readInt(); + mTaskFragmentCreationParams = in.readTypedObject(TaskFragmentCreationParams.CREATOR); + mActivityToken = in.readStrongBinder(); + mActivityIntent = in.readTypedObject(Intent.CREATOR); + mBundle = in.readBundle(getClass().getClassLoader()); + mSecondaryFragmentToken = in.readStrongBinder(); mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mOpType); + dest.writeTypedObject(mTaskFragmentCreationParams, flags); + dest.writeStrongBinder(mActivityToken); + dest.writeTypedObject(mActivityIntent, flags); + dest.writeBundle(mBundle); + dest.writeStrongBinder(mSecondaryFragmentToken); dest.writeTypedObject(mAnimationParams, flags); } @@ -91,6 +164,46 @@ public final class TaskFragmentOperation implements Parcelable { } /** + * Gets the options to create a new TaskFragment. + */ + @Nullable + public TaskFragmentCreationParams getTaskFragmentCreationParams() { + return mTaskFragmentCreationParams; + } + + /** + * Gets the Activity token set in this operation. + */ + @Nullable + public IBinder getActivityToken() { + return mActivityToken; + } + + /** + * Gets the Intent to start a new Activity. + */ + @Nullable + public Intent getActivityIntent() { + return mActivityIntent; + } + + /** + * Gets the Bundle set in this operation. + */ + @Nullable + public Bundle getBundle() { + return mBundle; + } + + /** + * Gets the fragment token of the secondary TaskFragment set in this operation. + */ + @Nullable + public IBinder getSecondaryFragmentToken() { + return mSecondaryFragmentToken; + } + + /** * Gets the animation related override of TaskFragment. */ @Nullable @@ -102,6 +215,21 @@ public final class TaskFragmentOperation implements Parcelable { public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("TaskFragmentOperation{ opType=").append(mOpType); + if (mTaskFragmentCreationParams != null) { + sb.append(", taskFragmentCreationParams=").append(mTaskFragmentCreationParams); + } + if (mActivityToken != null) { + sb.append(", activityToken=").append(mActivityToken); + } + if (mActivityIntent != null) { + sb.append(", activityIntent=").append(mActivityIntent); + } + if (mBundle != null) { + sb.append(", bundle=").append(mBundle); + } + if (mSecondaryFragmentToken != null) { + sb.append(", secondaryFragmentToken=").append(mSecondaryFragmentToken); + } if (mAnimationParams != null) { sb.append(", animationParams=").append(mAnimationParams); } @@ -112,9 +240,8 @@ public final class TaskFragmentOperation implements Parcelable { @Override public int hashCode() { - int result = mOpType; - result = result * 31 + mAnimationParams.hashCode(); - return result; + return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, + mBundle, mSecondaryFragmentToken, mAnimationParams); } @Override @@ -124,6 +251,11 @@ public final class TaskFragmentOperation implements Parcelable { } final TaskFragmentOperation other = (TaskFragmentOperation) obj; return mOpType == other.mOpType + && Objects.equals(mTaskFragmentCreationParams, other.mTaskFragmentCreationParams) + && Objects.equals(mActivityToken, other.mActivityToken) + && Objects.equals(mActivityIntent, other.mActivityIntent) + && Objects.equals(mBundle, other.mBundle) + && Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken) && Objects.equals(mAnimationParams, other.mAnimationParams); } @@ -139,6 +271,21 @@ public final class TaskFragmentOperation implements Parcelable { private final int mOpType; @Nullable + private TaskFragmentCreationParams mTaskFragmentCreationParams; + + @Nullable + private IBinder mActivityToken; + + @Nullable + private Intent mActivityIntent; + + @Nullable + private Bundle mBundle; + + @Nullable + private IBinder mSecondaryFragmentToken; + + @Nullable private TaskFragmentAnimationParams mAnimationParams; /** @@ -149,6 +296,52 @@ public final class TaskFragmentOperation implements Parcelable { } /** + * Sets the {@link TaskFragmentCreationParams} for creating a new TaskFragment. + */ + @NonNull + public Builder setTaskFragmentCreationParams( + @Nullable TaskFragmentCreationParams taskFragmentCreationParams) { + mTaskFragmentCreationParams = taskFragmentCreationParams; + return this; + } + + /** + * Sets an Activity token to this operation. + */ + @NonNull + public Builder setActivityToken(@Nullable IBinder activityToken) { + mActivityToken = activityToken; + return this; + } + + /** + * Sets the Intent to start a new Activity. + */ + @NonNull + public Builder setActivityIntent(@Nullable Intent activityIntent) { + mActivityIntent = activityIntent; + return this; + } + + /** + * Sets a Bundle to this operation. + */ + @NonNull + public Builder setBundle(@Nullable Bundle bundle) { + mBundle = bundle; + return this; + } + + /** + * Sets the secondary fragment token to this operation. + */ + @NonNull + public Builder setSecondaryFragmentToken(@Nullable IBinder secondaryFragmentToken) { + mSecondaryFragmentToken = secondaryFragmentToken; + return this; + } + + /** * Sets the {@link TaskFragmentAnimationParams} for the given TaskFragment. */ @NonNull @@ -162,7 +355,8 @@ public final class TaskFragmentOperation implements Parcelable { */ @NonNull public TaskFragmentOperation build() { - return new TaskFragmentOperation(mOpType, mAnimationParams); + return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken, + mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams); } } } diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java index 283df7608806..f785a3d1514e 100644 --- a/core/java/android/window/TaskFragmentOrganizer.java +++ b/core/java/android/window/TaskFragmentOrganizer.java @@ -55,7 +55,7 @@ public class TaskFragmentOrganizer extends WindowOrganizer { public static final String KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO = "task_fragment_info"; /** - * Key to the {@link WindowContainerTransaction.HierarchyOp} in + * Key to the {@link TaskFragmentOperation.OperationType} in * {@link TaskFragmentTransaction.Change#getErrorBundle()}. */ public static final String KEY_ERROR_CALLBACK_OP_TYPE = "operation_type"; @@ -112,7 +112,7 @@ public class TaskFragmentOrganizer extends WindowOrganizer { * @hide */ public static @NonNull Bundle putErrorInfoInBundle(@NonNull Throwable exception, - @Nullable TaskFragmentInfo info, int opType) { + @Nullable TaskFragmentInfo info, @TaskFragmentOperation.OperationType int opType) { final Bundle errorBundle = new Bundle(); errorBundle.putSerializable(KEY_ERROR_CALLBACK_THROWABLE, exception); if (info != null) { diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java index bffd4e437dfa..02878f8ae72b 100644 --- a/core/java/android/window/TaskOrganizer.java +++ b/core/java/android/window/TaskOrganizer.java @@ -152,17 +152,33 @@ public class TaskOrganizer extends WindowOrganizer { * @param windowingMode Windowing mode to put the root task in. * @param launchCookie Launch cookie to associate with the task so that is can be identified * when the {@link ITaskOrganizer#onTaskAppeared} callback is called. + * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed. + * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) - @Nullable - public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { try { - mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie); + mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie, + removeWithTaskOrganizer); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param launchCookie Launch cookie to associate with the task so that is can be identified + * when the {@link ITaskOrganizer#onTaskAppeared} callback is called. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) + @Nullable + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + createRootTask(displayId, windowingMode, launchCookie, false /* removeWithTaskOrganizer */); + } + /** Deletes a persistent root task in WM */ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public boolean deleteRootTask(@NonNull WindowContainerToken task) { diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 647ccf51b5ef..cc48d468258e 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -16,6 +16,15 @@ package android.window; +import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -505,32 +514,29 @@ public final class WindowContainerTransaction implements Parcelable { /** * Creates a new TaskFragment with the given options. - * @param taskFragmentOptions the options used to create the TaskFragment. + * @param taskFragmentCreationParams the options used to create the TaskFragment. */ @NonNull public WindowContainerTransaction createTaskFragment( - @NonNull TaskFragmentCreationParams taskFragmentOptions) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT) - .setTaskFragmentCreationOptions(taskFragmentOptions) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + @NonNull TaskFragmentCreationParams taskFragmentCreationParams) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_CREATE_TASK_FRAGMENT) + .setTaskFragmentCreationParams(taskFragmentCreationParams) + .build(); + return addTaskFragmentOperation(taskFragmentCreationParams.getFragmentToken(), operation); } /** * Deletes an existing TaskFragment. Any remaining activities below it will be destroyed. - * @param taskFragment the TaskFragment to be removed. + * @param fragmentToken client assigned unique token to create TaskFragment with specified in + * {@link TaskFragmentCreationParams#getFragmentToken()}. */ @NonNull - public WindowContainerTransaction deleteTaskFragment( - @NonNull WindowContainerToken taskFragment) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT) - .setContainer(taskFragment.asBinder()) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + public WindowContainerTransaction deleteTaskFragment(@NonNull IBinder fragmentToken) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_DELETE_TASK_FRAGMENT) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** @@ -546,16 +552,13 @@ public final class WindowContainerTransaction implements Parcelable { public WindowContainerTransaction startActivityInTaskFragment( @NonNull IBinder fragmentToken, @NonNull IBinder callerToken, @NonNull Intent activityIntent, @Nullable Bundle activityOptions) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT) - .setContainer(fragmentToken) - .setReparentContainer(callerToken) - .setActivityIntent(activityIntent) - .setLaunchOptions(activityOptions) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT) + .setActivityToken(callerToken) + .setActivityIntent(activityIntent) + .setBundle(activityOptions) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** @@ -567,33 +570,11 @@ public final class WindowContainerTransaction implements Parcelable { @NonNull public WindowContainerTransaction reparentActivityToTaskFragment( @NonNull IBinder fragmentToken, @NonNull IBinder activityToken) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT) - .setReparentContainer(fragmentToken) - .setContainer(activityToken) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - } - - /** - * Reparents all children of one TaskFragment to another. - * @param oldParent children of this TaskFragment will be reparented. - * @param newParent the new parent TaskFragment to move the children to. If {@code null}, the - * children will be moved to the leaf Task. - */ - @NonNull - public WindowContainerTransaction reparentChildren( - @NonNull WindowContainerToken oldParent, - @Nullable WindowContainerToken newParent) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN) - .setContainer(oldParent.asBinder()) - .setReparentContainer(newParent != null ? newParent.asBinder() : null) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT) + .setActivityToken(activityToken) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** @@ -602,25 +583,36 @@ public final class WindowContainerTransaction implements Parcelable { * {@link #setAdjacentRoots(WindowContainerToken, WindowContainerToken)}, but can be used with * fragmentTokens when that TaskFragments haven't been created (but will be created in the same * {@link WindowContainerTransaction}). - * To reset it, pass {@code null} for {@code fragmentToken2}. * @param fragmentToken1 client assigned unique token to create TaskFragment with specified * in {@link TaskFragmentCreationParams#getFragmentToken()}. * @param fragmentToken2 client assigned unique token to create TaskFragment with specified - * in {@link TaskFragmentCreationParams#getFragmentToken()}. If it is - * {@code null}, the transaction will reset the adjacent TaskFragment. + * in {@link TaskFragmentCreationParams#getFragmentToken()}. */ @NonNull public WindowContainerTransaction setAdjacentTaskFragments( - @NonNull IBinder fragmentToken1, @Nullable IBinder fragmentToken2, + @NonNull IBinder fragmentToken1, @NonNull IBinder fragmentToken2, @Nullable TaskFragmentAdjacentParams params) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS) - .setContainer(fragmentToken1) - .setReparentContainer(fragmentToken2) - .setLaunchOptions(params != null ? params.toBundle() : null) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS) + .setSecondaryFragmentToken(fragmentToken2) + .setBundle(params != null ? params.toBundle() : null) + .build(); + return addTaskFragmentOperation(fragmentToken1, operation); + } + + /** + * Clears the adjacent TaskFragments relationship that is previously set through + * {@link #setAdjacentTaskFragments}. Clear operation on one TaskFragment will also clear its + * current adjacent TaskFragment's. + * @param fragmentToken client assigned unique token to create TaskFragment with specified + * in {@link TaskFragmentCreationParams#getFragmentToken()}. + */ + @NonNull + public WindowContainerTransaction clearAdjacentTaskFragments(@NonNull IBinder fragmentToken) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** @@ -700,14 +692,10 @@ public final class WindowContainerTransaction implements Parcelable { */ @NonNull public WindowContainerTransaction requestFocusOnTaskFragment(@NonNull IBinder fragmentToken) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT) - .setContainer(fragmentToken) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** @@ -728,30 +716,32 @@ public final class WindowContainerTransaction implements Parcelable { } /** - * Sets the TaskFragment {@code container} to have a companion TaskFragment {@code companion}. + * Sets the TaskFragment {@code fragmentToken} to have a companion TaskFragment + * {@code companionFragmentToken}. * This indicates that the organizer will remove the TaskFragment when the companion * TaskFragment is removed. * - * @param container the TaskFragment container - * @param companion the companion TaskFragment. If it is {@code null}, the transaction will - * reset the companion TaskFragment. + * @param fragmentToken client assigned unique token to create TaskFragment with specified + * in {@link TaskFragmentCreationParams#getFragmentToken()}. + * @param companionFragmentToken client assigned unique token to create TaskFragment with + * specified in + * {@link TaskFragmentCreationParams#getFragmentToken()}. + * If it is {@code null}, the transaction will reset the companion + * TaskFragment. * @hide */ @NonNull - public WindowContainerTransaction setCompanionTaskFragment(@NonNull IBinder container, - @Nullable IBinder companion) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT) - .setContainer(container) - .setReparentContainer(companion) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + public WindowContainerTransaction setCompanionTaskFragment(@NonNull IBinder fragmentToken, + @Nullable IBinder companionFragmentToken) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_COMPANION_TASK_FRAGMENT) + .setSecondaryFragmentToken(companionFragmentToken) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** - * Sets the {@link TaskFragmentOperation} to apply to the given TaskFragment. + * Adds a {@link TaskFragmentOperation} to apply to the given TaskFragment. * * @param fragmentToken client assigned unique token to create TaskFragment with specified in * {@link TaskFragmentCreationParams#getFragmentToken()}. @@ -760,13 +750,13 @@ public final class WindowContainerTransaction implements Parcelable { * @hide */ @NonNull - public WindowContainerTransaction setTaskFragmentOperation(@NonNull IBinder fragmentToken, + public WindowContainerTransaction addTaskFragmentOperation(@NonNull IBinder fragmentToken, @NonNull TaskFragmentOperation taskFragmentOperation) { Objects.requireNonNull(fragmentToken); Objects.requireNonNull(taskFragmentOperation); final HierarchyOp hierarchyOp = new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION) + HierarchyOp.HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION) .setContainer(fragmentToken) .setTaskFragmentOperation(taskFragmentOperation) .build(); @@ -1267,25 +1257,17 @@ public final class WindowContainerTransaction implements Parcelable { public static final int HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS = 4; public static final int HIERARCHY_OP_TYPE_LAUNCH_TASK = 5; public static final int HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT = 6; - public static final int HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT = 7; - public static final int HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT = 8; - public static final int HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT = 9; - public static final int HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT = 10; - public static final int HIERARCHY_OP_TYPE_REPARENT_CHILDREN = 11; - public static final int HIERARCHY_OP_TYPE_PENDING_INTENT = 12; - public static final int HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS = 13; - public static final int HIERARCHY_OP_TYPE_START_SHORTCUT = 14; - public static final int HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER = 15; - public static final int HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER = 16; - public static final int HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER = 17; - public static final int HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT = 18; - public static final int HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP = 19; - public static final int HIERARCHY_OP_TYPE_REMOVE_TASK = 20; - public static final int HIERARCHY_OP_TYPE_FINISH_ACTIVITY = 21; - public static final int HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 22; - public static final int HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS = 23; - public static final int HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH = 24; - public static final int HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION = 25; + public static final int HIERARCHY_OP_TYPE_PENDING_INTENT = 7; + public static final int HIERARCHY_OP_TYPE_START_SHORTCUT = 8; + public static final int HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER = 9; + public static final int HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER = 10; + public static final int HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER = 11; + public static final int HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP = 12; + public static final int HIERARCHY_OP_TYPE_REMOVE_TASK = 13; + public static final int HIERARCHY_OP_TYPE_FINISH_ACTIVITY = 14; + public static final int HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS = 15; + public static final int HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH = 16; + public static final int HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION = 17; // The following key(s) are for use with mLaunchOptions: // When launching a task (eg. from recents), this is the taskId to be launched. @@ -1326,11 +1308,7 @@ public final class WindowContainerTransaction implements Parcelable { @Nullable private Intent mActivityIntent; - /** Used as options for {@link #createTaskFragment}. */ - @Nullable - private TaskFragmentCreationParams mTaskFragmentCreationOptions; - - /** Used as options for {@link #setTaskFragmentOperation}. */ + /** Used as options for {@link #addTaskFragmentOperation}. */ @Nullable private TaskFragmentOperation mTaskFragmentOperation; @@ -1452,7 +1430,6 @@ public final class WindowContainerTransaction implements Parcelable { mActivityTypes = copy.mActivityTypes; mLaunchOptions = copy.mLaunchOptions; mActivityIntent = copy.mActivityIntent; - mTaskFragmentCreationOptions = copy.mTaskFragmentCreationOptions; mTaskFragmentOperation = copy.mTaskFragmentOperation; mPendingIntent = copy.mPendingIntent; mShortcutInfo = copy.mShortcutInfo; @@ -1476,7 +1453,6 @@ public final class WindowContainerTransaction implements Parcelable { mActivityTypes = in.createIntArray(); mLaunchOptions = in.readBundle(); mActivityIntent = in.readTypedObject(Intent.CREATOR); - mTaskFragmentCreationOptions = in.readTypedObject(TaskFragmentCreationParams.CREATOR); mTaskFragmentOperation = in.readTypedObject(TaskFragmentOperation.CREATOR); mPendingIntent = in.readTypedObject(PendingIntent.CREATOR); mShortcutInfo = in.readTypedObject(ShortcutInfo.CREATOR); @@ -1516,16 +1492,6 @@ public final class WindowContainerTransaction implements Parcelable { return mReparent; } - @NonNull - public IBinder getCompanionContainer() { - return mReparent; - } - - @NonNull - public IBinder getCallingActivity() { - return mReparent; - } - public boolean getToTop() { return mToTop; } @@ -1561,11 +1527,6 @@ public final class WindowContainerTransaction implements Parcelable { } @Nullable - public TaskFragmentCreationParams getTaskFragmentCreationOptions() { - return mTaskFragmentCreationOptions; - } - - @Nullable public TaskFragmentOperation getTaskFragmentOperation() { return mTaskFragmentOperation; } @@ -1605,22 +1566,6 @@ public final class WindowContainerTransaction implements Parcelable { case HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT: return "{SetAdjacentFlagRoot: container=" + mContainer + " clearRoot=" + mToTop + "}"; - case HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT: - return "{CreateTaskFragment: options=" + mTaskFragmentCreationOptions + "}"; - case HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT: - return "{DeleteTaskFragment: taskFragment=" + mContainer + "}"; - case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: - return "{StartActivityInTaskFragment: fragmentToken=" + mContainer + " intent=" - + mActivityIntent + " options=" + mLaunchOptions + "}"; - case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: - return "{ReparentActivityToTaskFragment: fragmentToken=" + mReparent - + " activity=" + mContainer + "}"; - case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: - return "{ReparentChildren: oldParent=" + mContainer + " newParent=" + mReparent - + "}"; - case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: - return "{SetAdjacentTaskFragments: container=" + mContainer - + " adjacentContainer=" + mReparent + "}"; case HIERARCHY_OP_TYPE_START_SHORTCUT: return "{StartShortcut: options=" + mLaunchOptions + " info=" + mShortcutInfo + "}"; @@ -1631,8 +1576,6 @@ public final class WindowContainerTransaction implements Parcelable { case HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER: return "{removeLocalInsetsProvider: container=" + mContainer + " insetsType=" + Arrays.toString(mInsetsTypes) + "}"; - case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: - return "{requestFocusOnTaskFragment: container=" + mContainer + "}"; case HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP: return "{setAlwaysOnTop: container=" + mContainer + " alwaysOnTop=" + mAlwaysOnTop + "}"; @@ -1640,16 +1583,13 @@ public final class WindowContainerTransaction implements Parcelable { return "{RemoveTask: task=" + mContainer + "}"; case HIERARCHY_OP_TYPE_FINISH_ACTIVITY: return "{finishActivity: activity=" + mContainer + "}"; - case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: - return "{setCompanionTaskFragment: container = " + mContainer + " companion = " - + mReparent + "}"; case HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS: return "{ClearAdjacentRoot: container=" + mContainer + "}"; case HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH: return "{setReparentLeafTaskIfRelaunch: container= " + mContainer + " reparentLeafTaskIfRelaunch= " + mReparentLeafTaskIfRelaunch + "}"; - case HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION: - return "{setTaskFragmentOperation: fragmentToken= " + mContainer + case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: + return "{addTaskFragmentOperation: fragmentToken= " + mContainer + " operation= " + mTaskFragmentOperation + "}"; default: return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent @@ -1677,7 +1617,6 @@ public final class WindowContainerTransaction implements Parcelable { dest.writeIntArray(mActivityTypes); dest.writeBundle(mLaunchOptions); dest.writeTypedObject(mActivityIntent, flags); - dest.writeTypedObject(mTaskFragmentCreationOptions, flags); dest.writeTypedObject(mTaskFragmentOperation, flags); dest.writeTypedObject(mPendingIntent, flags); dest.writeTypedObject(mShortcutInfo, flags); @@ -1733,9 +1672,6 @@ public final class WindowContainerTransaction implements Parcelable { private Intent mActivityIntent; @Nullable - private TaskFragmentCreationParams mTaskFragmentCreationOptions; - - @Nullable private TaskFragmentOperation mTaskFragmentOperation; @Nullable @@ -1812,12 +1748,6 @@ public final class WindowContainerTransaction implements Parcelable { return this; } - Builder setTaskFragmentCreationOptions( - @Nullable TaskFragmentCreationParams taskFragmentCreationOptions) { - mTaskFragmentCreationOptions = taskFragmentCreationOptions; - return this; - } - Builder setTaskFragmentOperation( @Nullable TaskFragmentOperation taskFragmentOperation) { mTaskFragmentOperation = taskFragmentOperation; @@ -1852,7 +1782,6 @@ public final class WindowContainerTransaction implements Parcelable { hierarchyOp.mActivityIntent = mActivityIntent; hierarchyOp.mPendingIntent = mPendingIntent; hierarchyOp.mAlwaysOnTop = mAlwaysOnTop; - hierarchyOp.mTaskFragmentCreationOptions = mTaskFragmentCreationOptions; hierarchyOp.mTaskFragmentOperation = mTaskFragmentOperation; hierarchyOp.mShortcutInfo = mShortcutInfo; hierarchyOp.mReparentLeafTaskIfRelaunch = mReparentLeafTaskIfRelaunch; diff --git a/core/java/com/android/internal/inputmethod/EditableInputConnection.java b/core/java/com/android/internal/inputmethod/EditableInputConnection.java index 52e747150789..8690e8d0d9d0 100644 --- a/core/java/com/android/internal/inputmethod/EditableInputConnection.java +++ b/core/java/com/android/internal/inputmethod/EditableInputConnection.java @@ -271,9 +271,9 @@ public final class EditableInputConnection extends BaseInputConnection @Override public void requestTextBoundsInfo( - @NonNull RectF rectF, @Nullable @CallbackExecutor Executor executor, + @NonNull RectF bounds, @Nullable @CallbackExecutor Executor executor, @NonNull Consumer<TextBoundsInfoResult> consumer) { - final TextBoundsInfo textBoundsInfo = mTextView.getTextBoundsInfo(rectF); + final TextBoundsInfo textBoundsInfo = mTextView.getTextBoundsInfo(bounds); final int resultCode; if (textBoundsInfo != null) { resultCode = TextBoundsInfoResult.CODE_SUCCESS; diff --git a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl index 65016c27575e..b375936860a8 100644 --- a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl +++ b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl @@ -111,7 +111,7 @@ import com.android.internal.inputmethod.InputConnectionCommandHeader; int cursorUpdateMode, int cursorUpdateFilter, int imeDisplayId, in AndroidFuture future /* T=Boolean */); - void requestTextBoundsInfo(in InputConnectionCommandHeader header, in RectF rect, + void requestTextBoundsInfo(in InputConnectionCommandHeader header, in RectF bounds, in ResultReceiver resultReceiver /* T=TextBoundsInfoResult */); void commitContent(in InputConnectionCommandHeader header, in InputContentInfo inputContentInfo, diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index 3d8982bba505..04b7239cb5b3 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -187,8 +187,6 @@ public class BatteryStatsHistory { private int mNextHistoryTagIdx = 0; private int mNumHistoryTagChars = 0; private int mHistoryBufferLastPos = -1; - private int mActiveHistoryStates = 0xffffffff; - private int mActiveHistoryStates2 = 0xffffffff; private long mLastHistoryElapsedRealtimeMs = 0; private long mTrackRunningHistoryElapsedRealtimeMs = 0; private long mTrackRunningHistoryUptimeMs = 0; @@ -362,8 +360,6 @@ public class BatteryStatsHistory { mNextHistoryTagIdx = 0; mNumHistoryTagChars = 0; mHistoryBufferLastPos = -1; - mActiveHistoryStates = 0xffffffff; - mActiveHistoryStates2 = 0xffffffff; if (mStepDetailsCalculator != null) { mStepDetailsCalculator.clear(); } @@ -1098,8 +1094,9 @@ public class BatteryStatsHistory { */ public void recordScreenBrightnessEvent(long elapsedRealtimeMs, long uptimeMs, int brightnessBin) { - mHistoryCur.states = (mHistoryCur.states & ~HistoryItem.STATE_BRIGHTNESS_MASK) - | (brightnessBin << HistoryItem.STATE_BRIGHTNESS_SHIFT); + mHistoryCur.states = setBitField(mHistoryCur.states, brightnessBin, + HistoryItem.STATE_BRIGHTNESS_SHIFT, + HistoryItem.STATE_BRIGHTNESS_MASK); writeHistoryItem(elapsedRealtimeMs, uptimeMs); } @@ -1108,8 +1105,9 @@ public class BatteryStatsHistory { */ public void recordGpsSignalQualityEvent(long elapsedRealtimeMs, long uptimeMs, int signalLevel) { - mHistoryCur.states2 = (mHistoryCur.states2 & ~HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK) - | (signalLevel << HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT); + mHistoryCur.states2 = setBitField(mHistoryCur.states2, signalLevel, + HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT, + HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK); writeHistoryItem(elapsedRealtimeMs, uptimeMs); } @@ -1117,8 +1115,9 @@ public class BatteryStatsHistory { * Records a device idle mode change event. */ public void recordDeviceIdleEvent(long elapsedRealtimeMs, long uptimeMs, int mode) { - mHistoryCur.states2 = (mHistoryCur.states2 & ~HistoryItem.STATE2_DEVICE_IDLE_MASK) - | (mode << HistoryItem.STATE2_DEVICE_IDLE_SHIFT); + mHistoryCur.states2 = setBitField(mHistoryCur.states2, mode, + HistoryItem.STATE2_DEVICE_IDLE_SHIFT, + HistoryItem.STATE2_DEVICE_IDLE_MASK); writeHistoryItem(elapsedRealtimeMs, uptimeMs); } @@ -1130,13 +1129,15 @@ public class BatteryStatsHistory { mHistoryCur.states = (mHistoryCur.states | addStateFlag) & ~removeStateFlag; if (state != -1) { mHistoryCur.states = - (mHistoryCur.states & ~HistoryItem.STATE_PHONE_STATE_MASK) - | (state << HistoryItem.STATE_PHONE_STATE_SHIFT); + setBitField(mHistoryCur.states, state, + HistoryItem.STATE_PHONE_STATE_SHIFT, + HistoryItem.STATE_PHONE_STATE_MASK); } if (signalStrength != -1) { mHistoryCur.states = - (mHistoryCur.states & ~HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK) - | (signalStrength << HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT); + setBitField(mHistoryCur.states, signalStrength, + HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT, + HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK); } writeHistoryItem(elapsedRealtimeMs, uptimeMs); } @@ -1146,8 +1147,9 @@ public class BatteryStatsHistory { */ public void recordDataConnectionTypeChangeEvent(long elapsedRealtimeMs, long uptimeMs, int dataConnectionType) { - mHistoryCur.states = (mHistoryCur.states & ~HistoryItem.STATE_DATA_CONNECTION_MASK) - | (dataConnectionType << HistoryItem.STATE_DATA_CONNECTION_SHIFT); + mHistoryCur.states = setBitField(mHistoryCur.states, dataConnectionType, + HistoryItem.STATE_DATA_CONNECTION_SHIFT, + HistoryItem.STATE_DATA_CONNECTION_MASK); writeHistoryItem(elapsedRealtimeMs, uptimeMs); } @@ -1157,8 +1159,9 @@ public class BatteryStatsHistory { public void recordWifiSupplicantStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int supplState) { mHistoryCur.states2 = - (mHistoryCur.states2 & ~HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK) - | (supplState << HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT); + setBitField(mHistoryCur.states2, supplState, + HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT, + HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK); writeHistoryItem(elapsedRealtimeMs, uptimeMs); } @@ -1168,8 +1171,9 @@ public class BatteryStatsHistory { public void recordWifiSignalStrengthChangeEvent(long elapsedRealtimeMs, long uptimeMs, int strengthBin) { mHistoryCur.states2 = - (mHistoryCur.states2 & ~HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK) - | (strengthBin << HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT); + setBitField(mHistoryCur.states2, strengthBin, + HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT, + HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK); writeHistoryItem(elapsedRealtimeMs, uptimeMs); } @@ -1227,6 +1231,16 @@ public class BatteryStatsHistory { } } + private int setBitField(int bits, int value, int shift, int mask) { + int shiftedValue = value << shift; + if ((shiftedValue & ~mask) != 0) { + Slog.wtfStack(TAG, "Value " + Integer.toHexString(value) + + " does not fit in the bit field: " + Integer.toHexString(mask)); + shiftedValue &= mask; + } + return (bits & ~mask) | shiftedValue; + } + /** * Writes the current history item to history. */ @@ -1260,8 +1274,8 @@ public class BatteryStatsHistory { } final long timeDiffMs = (mHistoryBaseTimeMs + elapsedRealtimeMs) - mHistoryLastWritten.time; - final int diffStates = mHistoryLastWritten.states ^ (cur.states & mActiveHistoryStates); - final int diffStates2 = mHistoryLastWritten.states2 ^ (cur.states2 & mActiveHistoryStates2); + final int diffStates = mHistoryLastWritten.states ^ cur.states; + final int diffStates2 = mHistoryLastWritten.states2 ^ cur.states2; final int lastDiffStates = mHistoryLastWritten.states ^ mHistoryLastLastWritten.states; final int lastDiffStates2 = mHistoryLastWritten.states2 ^ mHistoryLastLastWritten.states2; if (DEBUG) { @@ -1274,9 +1288,9 @@ public class BatteryStatsHistory { recordTraceEvents(cur.eventCode, cur.eventTag); recordTraceCounters(mHistoryLastWritten.states, - cur.states & mActiveHistoryStates, BatteryStats.HISTORY_STATE_DESCRIPTIONS); + cur.states, BatteryStats.HISTORY_STATE_DESCRIPTIONS); recordTraceCounters(mHistoryLastWritten.states2, - cur.states2 & mActiveHistoryStates2, BatteryStats.HISTORY_STATE2_DESCRIPTIONS); + cur.states2, BatteryStats.HISTORY_STATE2_DESCRIPTIONS); if (mHistoryBufferLastPos >= 0 && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE && timeDiffMs < 1000 && (diffStates & lastDiffStates) == 0 @@ -1387,8 +1401,6 @@ public class BatteryStatsHistory { final boolean hasTags = mHistoryLastWritten.tagsFirstOccurrence || cur.tagsFirstOccurrence; mHistoryLastWritten.setTo(mHistoryBaseTimeMs + elapsedRealtimeMs, cmd, cur); mHistoryLastWritten.tagsFirstOccurrence = hasTags; - mHistoryLastWritten.states &= mActiveHistoryStates; - mHistoryLastWritten.states2 &= mActiveHistoryStates2; writeHistoryDelta(mHistoryBuffer, mHistoryLastWritten, mHistoryLastLastWritten); mLastHistoryElapsedRealtimeMs = elapsedRealtimeMs; cur.wakelockTag = null; @@ -1627,7 +1639,7 @@ public class BatteryStatsHistory { } if (cur.eventCode != HistoryItem.EVENT_NONE) { final int index = writeHistoryTag(cur.eventTag); - final int codeAndIndex = (cur.eventCode & 0xffff) | (index << 16); + final int codeAndIndex = setBitField(cur.eventCode & 0xffff, index, 16, 0xFFFF0000); dest.writeInt(codeAndIndex); if ((index & BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG) != 0) { cur.eventTag.writeToParcel(dest, 0); @@ -1689,9 +1701,11 @@ public class BatteryStatsHistory { } private int buildBatteryLevelInt(HistoryItem h) { - return ((((int) h.batteryLevel) << 25) & 0xfe000000) - | ((((int) h.batteryTemperature) << 15) & 0x01ff8000) - | ((((int) h.batteryVoltage) << 1) & 0x00007ffe); + int bits = 0; + bits = setBitField(bits, h.batteryLevel, 25, 0xfe000000 /* 7F << 25 */); + bits = setBitField(bits, h.batteryTemperature, 15, 0x01ff8000 /* 3FF << 15 */); + bits = setBitField(bits, h.batteryVoltage, 1, 0x00007ffe /* 3FFF << 1 */); + return bits; } private int buildStateInt(HistoryItem h) { diff --git a/core/jni/android_hardware_OverlayProperties.cpp b/core/jni/android_hardware_OverlayProperties.cpp index a96af8628e62..9941ca427025 100644 --- a/core/jni/android_hardware_OverlayProperties.cpp +++ b/core/jni/android_hardware_OverlayProperties.cpp @@ -69,6 +69,16 @@ static jboolean android_hardware_OverlayProperties_supportFp16ForHdr(JNIEnv* env return false; } +static jboolean android_hardware_OverlayProperties_supportMixedColorSpaces(JNIEnv* env, + jobject thiz, + jlong nativeObject) { + gui::OverlayProperties* properties = reinterpret_cast<gui::OverlayProperties*>(nativeObject); + if (properties != nullptr && properties->supportMixedColorSpaces) { + return true; + } + return false; +} + // ---------------------------------------------------------------------------- // Serialization // ---------------------------------------------------------------------------- @@ -128,6 +138,8 @@ static const JNINativeMethod gMethods[] = { { "nGetDestructor", "()J", (void*) android_hardware_OverlayProperties_getDestructor }, { "nSupportFp16ForHdr", "(J)Z", (void*) android_hardware_OverlayProperties_supportFp16ForHdr }, + { "nSupportMixedColorSpaces", "(J)Z", + (void*) android_hardware_OverlayProperties_supportMixedColorSpaces }, { "nWriteOverlayPropertiesToParcel", "(JLandroid/os/Parcel;)V", (void*) android_hardware_OverlayProperties_write }, { "nReadOverlayPropertiesFromParcel", "(Landroid/os/Parcel;)J", diff --git a/core/proto/android/service/package.proto b/core/proto/android/service/package.proto index 1dedbb9362a3..0fe2a6bebb49 100644 --- a/core/proto/android/service/package.proto +++ b/core/proto/android/service/package.proto @@ -124,6 +124,9 @@ message PackageProto { // The package on behalf of which the initiiating package requested the install. optional string originating_package_name = 2; + + // The package that is the update owner. + optional string update_owner_package_name = 3; } message StatesProto { diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5136fcbab7dd..81b3af0e0e60 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3993,6 +3993,18 @@ <permission android:name="android.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS" android:protectionLevel="signature|privileged" /> + <!-- Allows a system application to be registered with credential manager without + having to be enabled by the user. + @hide --> + <permission android:name="android.permission.SYSTEM_CREDENTIAL_PROVIDER" + android:protectionLevel="signature|privileged" /> + + <!-- Allows an application to be able to store and retrieve credentials from a remote + device. + @hide --> + <permission android:name="android.permission.HYBRID_CREDENTIAL_PROVIDER" + android:protectionLevel="signature|privileged" /> + <!-- ========================================= --> <!-- Permissions for special development tools --> <!-- ========================================= --> @@ -6815,6 +6827,16 @@ android:protectionLevel="normal" /> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION"/> + <!-- Allows an application to indicate via {@link + android.content.pm.PackageInstaller.SessionParams#setRequestUpdateOwnership} + that it has the intention of becoming the update owner. + <p>Protection level: normal + --> + <permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" + android:protectionLevel="normal" /> + <uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" /> + + <!-- Allows an application to take screenshots of layers that normally would be blacked out when a screenshot is taken. Specifically, layers that have the flag {@link android.view.SurfaceControl#SECURE} will be screenshot if the caller requests to @@ -6885,7 +6907,7 @@ @hide --> <permission android:name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT" - android:protectionLevel="internal|role"/> + android:protectionLevel="signature|privileged|role"/> <!-- @SystemApi Required by a AmbientContextEventDetectionService to ensure that only the service with this permission can bind to it. diff --git a/core/res/res/values-television/themes_device_defaults.xml b/core/res/res/values-television/themes_device_defaults.xml index 9d0e52213f8b..7cda99a157d2 100644 --- a/core/res/res/values-television/themes_device_defaults.xml +++ b/core/res/res/values-television/themes_device_defaults.xml @@ -14,6 +14,7 @@ limitations under the License. --> <resources> + <style name="Theme.DeviceDefault.Dialog" parent="Theme.Leanback.Dialog" /> <style name="Theme.DeviceDefault.Dialog.Alert" parent="Theme.Leanback.Dialog.Alert" /> <style name="Theme.DeviceDefault.Dialog.AppError" parent="Theme.Leanback.Dialog.AppError" /> <style name="Theme.DeviceDefault.Light.Dialog.Alert" parent="Theme.Leanback.Dialog.Alert" /> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 8ac13efba700..ef94484f8de6 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1783,6 +1783,14 @@ --> <attr name="attributionTags" format="string" /> + <!-- Default value <code>true</code> allows an installer to enable update + ownership enforcement for this package via {@link + android.content.pm.PackageInstaller.SessionParams#setRequestUpdateOwnership} + during initial installation. This overrides the installer's use of {@link + android.content.pm.PackageInstaller.SessionParams#setRequestUpdateOwnership}. + --> + <attr name="allowUpdateOwnership" format="boolean" /> + <!-- The <code>manifest</code> tag is the root of an <code>AndroidManifest.xml</code> file, describing the contents of an Android package (.apk) file. One @@ -1820,6 +1828,7 @@ <attr name="isSplitRequired" /> <attr name="requiredSplitTypes" /> <attr name="splitTypes" /> + <attr name="allowUpdateOwnership" /> </declare-styleable> <!-- The <code>application</code> tag describes application-level components diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index dfd4d9a18271..a9c56f0f802b 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -124,6 +124,7 @@ <public name="allowSharedIsolatedProcess" /> <public name="keyboardLocale" /> <public name="keyboardLayoutType" /> + <public name="allowUpdateOwnership" /> </staging-public-group> <staging-public-group type="id" first-id="0x01cd0000"> diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java index 82db716fcdc2..cce1b2bc3ece 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java @@ -21,10 +21,15 @@ import android.hardware.broadcastradio.ProgramIdentifier; import android.hardware.broadcastradio.ProgramInfo; import android.hardware.broadcastradio.ProgramListChunk; import android.hardware.broadcastradio.VendorKeyValue; +import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioMetadata; +import android.os.RemoteException; import android.util.ArrayMap; +import android.util.ArraySet; + +import java.util.List; final class AidlTestUtils { @@ -94,6 +99,14 @@ final class AidlTestUtils { return makeHalProgramInfo(hwSel, hwSel.primaryId, hwSel.primaryId, hwSignalQuality); } + static ProgramInfo programInfoToHalProgramInfo(RadioManager.ProgramInfo info) { + return makeHalProgramInfo( + ConversionUtils.programSelectorToHalProgramSelector(info.getSelector()), + ConversionUtils.identifierToHalProgramIdentifier(info.getLogicallyTunedTo()), + ConversionUtils.identifierToHalProgramIdentifier(info.getPhysicallyTunedTo()), + info.getSignalStrength()); + } + static ProgramInfo makeHalProgramInfo( android.hardware.broadcastradio.ProgramSelector hwSel, ProgramIdentifier logicallyTunedTo, ProgramIdentifier physicallyTunedTo, @@ -108,7 +121,23 @@ final class AidlTestUtils { return hwInfo; } - static ProgramListChunk makeProgramListChunk(boolean purge, boolean complete, + static ProgramListChunk makeHalChunk(boolean purge, boolean complete, + List<RadioManager.ProgramInfo> modified, List<ProgramSelector.Identifier> removed) { + ProgramInfo[] halModified = + new android.hardware.broadcastradio.ProgramInfo[modified.size()]; + for (int i = 0; i < modified.size(); i++) { + halModified[i] = programInfoToHalProgramInfo(modified.get(i)); + } + + ProgramIdentifier[] halRemoved = + new android.hardware.broadcastradio.ProgramIdentifier[removed.size()]; + for (int i = 0; i < removed.size(); i++) { + halRemoved[i] = ConversionUtils.identifierToHalProgramIdentifier(removed.get(i)); + } + return makeHalChunk(purge, complete, halModified, halRemoved); + } + + static ProgramListChunk makeHalChunk(boolean purge, boolean complete, ProgramInfo[] modified, ProgramIdentifier[] removed) { ProgramListChunk halChunk = new ProgramListChunk(); halChunk.purge = purge; @@ -118,6 +147,21 @@ final class AidlTestUtils { return halChunk; } + static ProgramList.Chunk makeChunk(boolean purge, boolean complete, + List<RadioManager.ProgramInfo> modified, + List<ProgramSelector.Identifier> removed) throws RemoteException { + ArraySet<RadioManager.ProgramInfo> modifiedSet = new ArraySet<>(); + if (modified != null) { + modifiedSet.addAll(modified); + } + ArraySet<ProgramSelector.Identifier> removedSet = new ArraySet<>(); + if (removed != null) { + removedSet.addAll(removed); + } + ProgramList.Chunk chunk = new ProgramList.Chunk(purge, complete, modifiedSet, removedSet); + return chunk; + } + static VendorKeyValue makeVendorKeyValue(String vendorKey, String vendorValue) { VendorKeyValue vendorKeyValue = new VendorKeyValue(); vendorKeyValue.key = vendorKey; diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java index 710c150c006c..5d0e07613a98 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java @@ -328,7 +328,7 @@ public final class ConversionUtilsTest { TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY); RadioManager.ProgramInfo dabInfo = ConversionUtils.programInfoFromHalProgramInfo(halDabInfo); - ProgramListChunk halChunk = AidlTestUtils.makeProgramListChunk(purge, complete, + ProgramListChunk halChunk = AidlTestUtils.makeHalChunk(purge, complete, new ProgramInfo[]{halDabInfo}, new ProgramIdentifier[]{TEST_HAL_VENDOR_ID, TEST_HAL_FM_FREQUENCY_ID}); @@ -353,7 +353,7 @@ public final class ConversionUtilsTest { TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID}); ProgramInfo halDabInfo = AidlTestUtils.makeHalProgramInfo(halDabSelector, TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_ENSEMBLE_ID, TEST_SIGNAL_QUALITY); - ProgramListChunk halChunk = AidlTestUtils.makeProgramListChunk(purge, complete, + ProgramListChunk halChunk = AidlTestUtils.makeHalChunk(purge, complete, new ProgramInfo[]{halDabInfo}, new ProgramIdentifier[]{TEST_HAL_FM_FREQUENCY_ID}); ProgramList.Chunk chunk = ConversionUtils.chunkFromHalProgramListChunk(halChunk); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ProgramInfoCacheTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ProgramInfoCacheTest.java new file mode 100644 index 000000000000..d54397e07a63 --- /dev/null +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ProgramInfoCacheTest.java @@ -0,0 +1,409 @@ +/* + * 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.broadcastradio.aidl; + +import android.hardware.broadcastradio.ProgramIdentifier; +import android.hardware.broadcastradio.ProgramInfo; +import android.hardware.broadcastradio.ProgramListChunk; +import android.hardware.radio.ProgramList; +import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioManager; +import android.os.RemoteException; +import android.util.ArraySet; + +import com.google.common.truth.Expect; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; +import java.util.Set; + +/** + * Unit tests for AIDL ProgramInfoCache + */ +@RunWith(MockitoJUnitRunner.class) +public class ProgramInfoCacheTest { + + private static final int TEST_SIGNAL_QUALITY = 90; + + private static final ProgramSelector.Identifier TEST_FM_FREQUENCY_ID = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, + /* value= */ 88_500); + private static final RadioManager.ProgramInfo TEST_FM_INFO = AidlTestUtils.makeProgramInfo( + AidlTestUtils.makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, + TEST_FM_FREQUENCY_ID), TEST_FM_FREQUENCY_ID, TEST_FM_FREQUENCY_ID, + TEST_SIGNAL_QUALITY); + private static final RadioManager.ProgramInfo TEST_FM_INFO_MODIFIED = + AidlTestUtils.makeProgramInfo(AidlTestUtils.makeProgramSelector( + ProgramSelector.PROGRAM_TYPE_FM, TEST_FM_FREQUENCY_ID), TEST_FM_FREQUENCY_ID, + TEST_FM_FREQUENCY_ID, /* signalQuality= */ 99); + + private static final ProgramSelector.Identifier TEST_AM_FREQUENCY_ID = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, + /* value= */ 1_700); + private static final RadioManager.ProgramInfo TEST_AM_INFO = AidlTestUtils.makeProgramInfo( + AidlTestUtils.makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, + TEST_AM_FREQUENCY_ID), TEST_AM_FREQUENCY_ID, TEST_AM_FREQUENCY_ID, + TEST_SIGNAL_QUALITY); + + private static final ProgramSelector.Identifier TEST_RDS_PI_ID = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, + /* value= */ 15_019); + private static final RadioManager.ProgramInfo TEST_RDS_INFO = AidlTestUtils.makeProgramInfo( + AidlTestUtils.makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, TEST_RDS_PI_ID), + TEST_RDS_PI_ID, new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 89_500), + TEST_SIGNAL_QUALITY); + + private static final ProgramSelector.Identifier TEST_DAB_DMB_SID_EXT_ID = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, + /* value= */ 0xA000000111L); + private static final ProgramSelector.Identifier TEST_DAB_ENSEMBLE_ID = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, + /* value= */ 0x1001); + private static final ProgramSelector.Identifier TEST_DAB_FREQUENCY_ID = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, + /* value= */ 220_352); + private static final RadioManager.ProgramInfo TEST_DAB_INFO = AidlTestUtils.makeProgramInfo( + new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_DMB_SID_EXT_ID, + new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID}, + /* vendorIds= */ null), TEST_DAB_DMB_SID_EXT_ID, TEST_DAB_FREQUENCY_ID, + TEST_SIGNAL_QUALITY); + + private static final ProgramSelector.Identifier TEST_VENDOR_ID = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, + /* value= */ 9_001); + private static final RadioManager.ProgramInfo TEST_VENDOR_INFO = AidlTestUtils.makeProgramInfo( + AidlTestUtils.makeProgramSelector(ProgramSelector.PROGRAM_TYPE_VENDOR_START, + TEST_VENDOR_ID), TEST_VENDOR_ID, TEST_VENDOR_ID, TEST_SIGNAL_QUALITY); + + private static final ProgramInfoCache FULL_PROGRAM_INFO_CACHE = new ProgramInfoCache( + /* filter= */ null, /* complete= */ true, + TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO, TEST_VENDOR_INFO); + + @Rule + public final Expect expect = Expect.create(); + + @Test + public void isComplete_forCompleteProgramInfoCache_returnsTrue() { + expect.withMessage("Complete program info cache") + .that(FULL_PROGRAM_INFO_CACHE.isComplete()).isTrue(); + } + + @Test + public void isComplete_forIncompleteProgramInfoCache_returnsFalse() { + ProgramInfoCache programInfoCache = new ProgramInfoCache(/* filter= */ null, + /* complete= */ false); + expect.withMessage("Incomplete program info cache") + .that(programInfoCache.isComplete()).isFalse(); + } + + @Test + public void getFilter_forProgramInfoCache() { + ProgramList.Filter fmFilter = new ProgramList.Filter( + Set.of(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + ProgramInfoCache fmProgramInfoCache = new ProgramInfoCache(fmFilter); + + expect.withMessage("Program info cache filter") + .that(fmProgramInfoCache.getFilter()).isEqualTo(fmFilter); + } + + @Test + public void updateFromHalProgramListChunk_withPurgingCompleteChunk() { + ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null, + /* complete= */ false, TEST_FM_INFO); + ProgramListChunk chunk = AidlTestUtils.makeHalChunk(/* purge= */ true, /* complete= */ true, + new ProgramInfo[]{AidlTestUtils.programInfoToHalProgramInfo(TEST_RDS_INFO), + AidlTestUtils.programInfoToHalProgramInfo(TEST_VENDOR_INFO)}, + new ProgramIdentifier[]{}); + + cache.updateFromHalProgramListChunk(chunk); + + expect.withMessage("Program cache updated with purge-enabled and complete chunk") + .that(cache.toProgramInfoList()) + .containsExactly(TEST_RDS_INFO, TEST_VENDOR_INFO); + expect.withMessage("Complete program cache").that(cache.isComplete()).isTrue(); + } + + @Test + public void updateFromHalProgramListChunk_withNonPurgingIncompleteChunk() { + ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null, + /* complete= */ false, TEST_FM_INFO, TEST_RDS_INFO, TEST_AM_INFO); + ProgramListChunk chunk = AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ false, + new ProgramInfo[]{AidlTestUtils.programInfoToHalProgramInfo(TEST_FM_INFO_MODIFIED), + AidlTestUtils.programInfoToHalProgramInfo(TEST_VENDOR_INFO)}, + new ProgramIdentifier[]{ConversionUtils.identifierToHalProgramIdentifier( + TEST_RDS_PI_ID)}); + + cache.updateFromHalProgramListChunk(chunk); + + expect.withMessage("Program cache updated with non-purging and incomplete chunk") + .that(cache.toProgramInfoList()) + .containsExactly(TEST_FM_INFO_MODIFIED, TEST_VENDOR_INFO, TEST_AM_INFO); + expect.withMessage("Incomplete program cache").that(cache.isComplete()).isFalse(); + } + + @Test + public void filterAndUpdateFromInternal_withNullFilter() { + ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null, + /* complete= */ true); + + cache.filterAndUpdateFromInternal(FULL_PROGRAM_INFO_CACHE, /* purge= */ false); + + expect.withMessage("Program cache filtered by null filter") + .that(cache.toProgramInfoList()) + .containsExactly(TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO, + TEST_VENDOR_INFO); + } + + @Test + public void filterAndUpdateFromInternal_withEmptyFilter() { + ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new ArraySet<>(), + new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false)); + + cache.filterAndUpdateFromInternal(FULL_PROGRAM_INFO_CACHE, /* purge= */ false); + + expect.withMessage("Program cache filtered by empty filter") + .that(cache.toProgramInfoList()) + .containsExactly(TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO, + TEST_VENDOR_INFO); + } + + @Test + public void filterAndUpdateFromInternal_withFilterByIdentifierType() { + ProgramInfoCache cache = new ProgramInfoCache( + new ProgramList.Filter(Set.of(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, + ProgramSelector.IDENTIFIER_TYPE_RDS_PI), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false)); + + cache.filterAndUpdateFromInternal(FULL_PROGRAM_INFO_CACHE, /* purge= */ false); + + expect.withMessage("Program cache filtered by identifier type") + .that(cache.toProgramInfoList()) + .containsExactly(TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO); + } + + @Test + public void filterAndUpdateFromInternal_withFilterByIdentifier() { + ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter( + new ArraySet<>(), Set.of(TEST_FM_FREQUENCY_ID, TEST_DAB_DMB_SID_EXT_ID), + /* includeCategories= */ true, /* excludeModifications= */ false)); + int maxNumModifiedPerChunk = 2; + int maxNumRemovedPerChunk = 2; + + List<ProgramList.Chunk> programListChunks = cache.filterAndUpdateFromInternal( + FULL_PROGRAM_INFO_CACHE, /* purge= */ true, maxNumModifiedPerChunk, + maxNumRemovedPerChunk); + + expect.withMessage("Program cache filtered by identifier") + .that(cache.toProgramInfoList()).containsExactly(TEST_FM_INFO, TEST_DAB_INFO); + verifyChunkListPurge(programListChunks, /* purge= */ true); + verifyChunkListComplete(programListChunks, FULL_PROGRAM_INFO_CACHE.isComplete()); + verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_FM_INFO, + TEST_DAB_INFO); + verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk); + } + + @Test + public void filterAndUpdateFromInternal_withFilterExcludingCategories() { + ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new ArraySet<>(), + new ArraySet<>(), /* includeCategories= */ false, + /* excludeModifications= */ false)); + int maxNumModifiedPerChunk = 3; + int maxNumRemovedPerChunk = 2; + + List<ProgramList.Chunk> programListChunks = cache.filterAndUpdateFromInternal( + FULL_PROGRAM_INFO_CACHE, /* purge= */ false, maxNumModifiedPerChunk, + maxNumRemovedPerChunk); + + expect.withMessage("Program cache filtered by excluding categories") + .that(cache.toProgramInfoList()) + .containsExactly(TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO); + verifyChunkListPurge(programListChunks, /* purge= */ true); + verifyChunkListComplete(programListChunks, FULL_PROGRAM_INFO_CACHE.isComplete()); + verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_FM_INFO, + TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO); + verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk); + } + + @Test + public void filterAndUpdateFromInternal_withFilterExcludingModifications() { + ProgramList.Filter filterExcludingModifications = new ProgramList.Filter(new ArraySet<>(), + new ArraySet<>(), /* includeCategories= */ true, + /* excludeModifications= */ true); + ProgramInfoCache cache = new ProgramInfoCache(filterExcludingModifications, + /* complete= */ true, TEST_FM_INFO, TEST_RDS_INFO, TEST_AM_INFO, TEST_DAB_INFO); + ProgramInfoCache halCache = new ProgramInfoCache(/* filter= */ null, /* complete= */ false, + TEST_FM_INFO_MODIFIED, TEST_VENDOR_INFO); + int maxNumModifiedPerChunk = 2; + int maxNumRemovedPerChunk = 2; + + List<ProgramList.Chunk> programListChunks = cache.filterAndUpdateFromInternal(halCache, + /* purge= */ false, maxNumModifiedPerChunk, maxNumRemovedPerChunk); + + expect.withMessage("Program cache filtered by excluding modifications") + .that(cache.toProgramInfoList()) + .containsExactly(TEST_FM_INFO, TEST_VENDOR_INFO); + verifyChunkListPurge(programListChunks, /* purge= */ false); + verifyChunkListComplete(programListChunks, halCache.isComplete()); + verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_VENDOR_INFO); + verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk, TEST_RDS_PI_ID, + TEST_AM_FREQUENCY_ID, TEST_DAB_DMB_SID_EXT_ID); + } + + @Test + public void filterAndUpdateFromInternal_withPurge() { + ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new ArraySet<>(), + new ArraySet<>(), /* includeCategories= */ true, + /* excludeModifications= */ false), + /* complete= */ true, TEST_FM_INFO, TEST_RDS_INFO); + ProgramInfoCache halCache = new ProgramInfoCache(/* filter= */ null, /* complete= */ false, + TEST_FM_INFO_MODIFIED, TEST_DAB_INFO, TEST_VENDOR_INFO); + int maxNumModifiedPerChunk = 2; + int maxNumRemovedPerChunk = 2; + + List<ProgramList.Chunk> programListChunks = cache.filterAndUpdateFromInternal(halCache, + /* purge= */ true, maxNumModifiedPerChunk, maxNumRemovedPerChunk); + + expect.withMessage("Purged program cache").that(cache.toProgramInfoList()) + .containsExactly(TEST_FM_INFO_MODIFIED, TEST_DAB_INFO, TEST_VENDOR_INFO); + verifyChunkListPurge(programListChunks, /* purge= */ true); + verifyChunkListComplete(programListChunks, halCache.isComplete()); + verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_FM_INFO_MODIFIED, + TEST_DAB_INFO, TEST_VENDOR_INFO); + verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk); + } + + @Test + public void filterAndApplyChunkInternal_withPurgingIncompleteChunk() throws RemoteException { + ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null, + /* complete= */ false, TEST_FM_INFO, TEST_DAB_INFO); + ProgramList.Chunk chunk = AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ false, + List.of(TEST_FM_INFO_MODIFIED, TEST_RDS_INFO, TEST_VENDOR_INFO), + List.of(TEST_DAB_DMB_SID_EXT_ID)); + int maxNumModifiedPerChunk = 2; + int maxNumRemovedPerChunk = 2; + + List<ProgramList.Chunk> programListChunks = cache.filterAndApplyChunkInternal(chunk, + maxNumModifiedPerChunk, maxNumRemovedPerChunk); + + expect.withMessage("Program cache applied with non-purging and complete chunk") + .that(cache.toProgramInfoList()) + .containsExactly(TEST_FM_INFO_MODIFIED, TEST_RDS_INFO, TEST_VENDOR_INFO); + verifyChunkListPurge(programListChunks, /* purge= */ true); + verifyChunkListComplete(programListChunks, /* complete= */ false); + verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_FM_INFO_MODIFIED, + TEST_RDS_INFO, TEST_VENDOR_INFO); + verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk); + } + + @Test + public void filterAndApplyChunk_withNonPurgingCompleteChunk() throws RemoteException { + ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null, + /* complete= */ false, TEST_FM_INFO, TEST_RDS_INFO, TEST_AM_INFO, TEST_DAB_INFO); + ProgramList.Chunk chunk = AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true, + List.of(TEST_FM_INFO_MODIFIED, TEST_VENDOR_INFO), + List.of(TEST_RDS_PI_ID, TEST_AM_FREQUENCY_ID, TEST_DAB_DMB_SID_EXT_ID)); + int maxNumModifiedPerChunk = 2; + int maxNumRemovedPerChunk = 2; + + List<ProgramList.Chunk> programListChunks = cache.filterAndApplyChunkInternal(chunk, + maxNumModifiedPerChunk, maxNumRemovedPerChunk); + + expect.withMessage("Program cache applied with purge-enabled complete chunk") + .that(cache.toProgramInfoList()) + .containsExactly(TEST_FM_INFO_MODIFIED, TEST_VENDOR_INFO); + verifyChunkListPurge(programListChunks, /* purge= */ false); + verifyChunkListComplete(programListChunks, /* complete= */ true); + verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_FM_INFO_MODIFIED, + TEST_VENDOR_INFO); + verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk, TEST_RDS_PI_ID, + TEST_AM_FREQUENCY_ID, TEST_DAB_DMB_SID_EXT_ID); + } + + private void verifyChunkListPurge(List<ProgramList.Chunk> chunks, boolean purge) { + if (chunks.isEmpty()) { + return; + } + for (int i = 0; i < chunks.size(); i++) { + ProgramList.Chunk chunk = chunks.get(i); + boolean expectedPurge = (i == 0 && purge); + + expect.withMessage("Purge for chunk %s", i) + .that(chunk.isPurge()).isEqualTo(expectedPurge); + } + } + + private void verifyChunkListComplete(List<ProgramList.Chunk> chunks, boolean complete) { + if (chunks.isEmpty()) { + return; + } + for (int i = 0; i < chunks.size(); i++) { + ProgramList.Chunk chunk = chunks.get(i); + boolean expectedComplete = (i == chunks.size() - 1 && complete); + + expect.withMessage("Purge for chunk %s", i) + .that(chunk.isComplete()).isEqualTo(expectedComplete); + } + } + + private void verifyChunkListModified(List<ProgramList.Chunk> chunks, + int maxModifiedPerChunk, RadioManager.ProgramInfo... expectedProgramInfos) { + if (chunks.isEmpty()) { + expect.withMessage("Empty program info list") + .that(expectedProgramInfos.length).isEqualTo(0); + return; + } + + ArraySet<RadioManager.ProgramInfo> actualSet = new ArraySet<>(); + for (int i = 0; i < chunks.size(); i++) { + Set<RadioManager.ProgramInfo> chunkModified = chunks.get(i).getModified(); + actualSet.addAll(chunkModified); + + expect.withMessage("Chunk %s modified program info array size", i) + .that(chunkModified.size()).isAtMost(maxModifiedPerChunk); + } + expect.withMessage("Program info items") + .that(actualSet).containsExactlyElementsIn(expectedProgramInfos); + } + + private void verifyChunkListRemoved(List<ProgramList.Chunk> chunks, + int maxRemovedPerChunk, ProgramSelector.Identifier... expectedIdentifiers) { + if (chunks.isEmpty()) { + expect.withMessage("Empty program info list") + .that(expectedIdentifiers.length).isEqualTo(0); + return; + } + + ArraySet<ProgramSelector.Identifier> actualSet = new ArraySet<>(); + for (int i = 0; i < chunks.size(); i++) { + Set<ProgramSelector.Identifier> chunkRemoved = chunks.get(i).getRemoved(); + actualSet.addAll(chunkRemoved); + + expect.withMessage("Chunk %s removed identifier array size ", i) + .that(chunkRemoved.size()).isAtMost(maxRemovedPerChunk); + } + expect.withMessage("Removed identifier items") + .that(actualSet).containsExactlyElementsIn(expectedIdentifiers); + } +} diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java index d7723acf6f05..464ecb2b50a1 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java @@ -37,7 +37,9 @@ import android.graphics.Bitmap; import android.hardware.broadcastradio.IBroadcastRadio; import android.hardware.broadcastradio.ITunerCallback; import android.hardware.broadcastradio.IdentifierType; +import android.hardware.broadcastradio.ProgramFilter; import android.hardware.broadcastradio.ProgramInfo; +import android.hardware.broadcastradio.ProgramListChunk; import android.hardware.broadcastradio.Result; import android.hardware.broadcastradio.VendorKeyValue; import android.hardware.radio.ProgramList; @@ -61,8 +63,10 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.verification.VerificationWithTimeout; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; /** * Tests for AIDL HAL TunerSession. @@ -72,7 +76,7 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT; private static final VerificationWithTimeout CALLBACK_TIMEOUT = timeout(/* millis= */ 200); - private static final int SIGNAL_QUALITY = 1; + private static final int SIGNAL_QUALITY = 90; private static final long AM_FM_FREQUENCY_SPACING = 500; private static final long[] AM_FM_FREQUENCY_LIST = {97_500, 98_100, 99_100}; private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR = @@ -84,6 +88,27 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { new RadioManager.FmBandConfig(FM_BAND_DESCRIPTOR); private static final int UNSUPPORTED_CONFIG_FLAG = 0; + private static final ProgramSelector.Identifier TEST_FM_FREQUENCY_ID = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, + /* value= */ 88_500); + private static final ProgramSelector.Identifier TEST_RDS_PI_ID = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, + /* value= */ 15_019); + + private static final RadioManager.ProgramInfo TEST_FM_INFO = AidlTestUtils.makeProgramInfo( + AidlTestUtils.makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, + TEST_FM_FREQUENCY_ID), TEST_FM_FREQUENCY_ID, TEST_FM_FREQUENCY_ID, + SIGNAL_QUALITY); + private static final RadioManager.ProgramInfo TEST_FM_INFO_MODIFIED = + AidlTestUtils.makeProgramInfo(AidlTestUtils.makeProgramSelector( + ProgramSelector.PROGRAM_TYPE_FM, TEST_FM_FREQUENCY_ID), TEST_FM_FREQUENCY_ID, + TEST_FM_FREQUENCY_ID, /* signalQuality= */ 100); + private static final RadioManager.ProgramInfo TEST_RDS_INFO = AidlTestUtils.makeProgramInfo( + AidlTestUtils.makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, TEST_RDS_PI_ID), + TEST_RDS_PI_ID, new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 89_500), + SIGNAL_QUALITY); + // Mocks @Mock private IBroadcastRadio mBroadcastRadioMock; private android.hardware.radio.ITunerCallback[] mAidlTunerCallbackMocks; @@ -393,7 +418,7 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test - public void tune_withHalHasUnknownError_fails() throws Exception { + public void tune_withUnknownErrorFromHal_fails() throws Exception { openAidlClients(/* numClients= */ 1); ProgramSelector sel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); doThrow(new ServiceSpecificException(Result.UNKNOWN_ERROR)) @@ -403,7 +428,7 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { mTunerSessions[0].tune(sel); }); - assertWithMessage("Exception for tuning when HAL has unknown error") + assertWithMessage("Unknown error HAL exception when tuning") .that(thrown).hasMessageThat().contains("UNKNOWN_ERROR"); } @@ -536,7 +561,7 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test - public void seek_withHalHasInternalError_fails() throws Exception { + public void seek_withInternalErrorFromHal_fails() throws Exception { openAidlClients(/* numClients= */ 1); doThrow(new ServiceSpecificException(Result.INTERNAL_ERROR)) .when(mBroadcastRadioMock).seek(anyBoolean(), anyBoolean()); @@ -545,7 +570,7 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false); }); - assertWithMessage("Exception for seeking when HAL has internal error") + assertWithMessage("Internal error HAL exception when seeking") .that(thrown).hasMessageThat().contains("INTERNAL_ERROR"); } @@ -644,11 +669,276 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void startProgramListUpdates_withEmptyFilter() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + ProgramFilter halFilter = ConversionUtils.filterToHalProgramFilter(filter); + List<RadioManager.ProgramInfo> modified = List.of(TEST_FM_INFO, TEST_RDS_INFO); + List<ProgramSelector.Identifier> removed = new ArrayList<>(); + ProgramListChunk halProgramList = AidlTestUtils.makeHalChunk(/* purge= */ true, + /* complete= */ true, modified, removed); + ProgramList.Chunk expectedProgramList = + AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true, modified, removed); + + mTunerSessions[0].startProgramListUpdates(filter); + mHalTunerCallback.onProgramListUpdated(halProgramList); + + verify(mBroadcastRadioMock).startProgramListUpdates(halFilter); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT) + .onProgramListUpdated(expectedProgramList); + } + + @Test + public void startProgramListUpdates_withCallbackCalledForMultipleTimes() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + mTunerSessions[0].startProgramListUpdates(filter); + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ true, + /* complete= */ true, List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>())); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true, + List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>())); + + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID))); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true, + List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID))); + } + + @Test + public void startProgramListUpdates_withTheSameFilterForMultipleTimes() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + mTunerSessions[0].startProgramListUpdates(filter); + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ true, + /* complete= */ true, List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>())); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true, + List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>())); + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID))); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true, + List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID))); + + mTunerSessions[0].startProgramListUpdates(filter); + + verify(mBroadcastRadioMock).startProgramListUpdates(any()); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true, + List.of(TEST_FM_INFO_MODIFIED), new ArrayList<>())); + } + + @Test + public void startProgramListUpdates_withNullFilter() throws Exception { + openAidlClients(/* numClients= */ 1); + + mTunerSessions[0].startProgramListUpdates(/* filter= */ null); + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ true, + /* complete= */ true, List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>())); + + verify(mBroadcastRadioMock).startProgramListUpdates(any()); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true, + List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>())); + + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID))); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true, + List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID))); + } + + @Test + public void startProgramListUpdates_withIdFilter() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter idFilter = new ProgramList.Filter(new ArraySet<>(), + Set.of(TEST_RDS_PI_ID), /* includeCategories= */ true, + /* excludeModifications= */ true); + ProgramFilter halFilter = ConversionUtils.filterToHalProgramFilter(idFilter); + + mTunerSessions[0].startProgramListUpdates(idFilter); + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_RDS_INFO), new ArrayList<>())); + + verify(mBroadcastRadioMock).startProgramListUpdates(halFilter); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true, + List.of(TEST_RDS_INFO), new ArrayList<>())); + + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO), new ArrayList<>())); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(any()); + } + + @Test + public void startProgramListUpdates_withFilterExcludingModifications() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter filterExcludingModifications = new ProgramList.Filter( + Set.of(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ true); + ProgramFilter halFilter = + ConversionUtils.filterToHalProgramFilter(filterExcludingModifications); + + mTunerSessions[0].startProgramListUpdates(filterExcludingModifications); + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO), new ArrayList<>())); + + verify(mBroadcastRadioMock).startProgramListUpdates(halFilter); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true, + List.of(TEST_FM_INFO), new ArrayList<>())); + + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED), new ArrayList<>())); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(any()); + } + + @Test + public void startProgramListUpdates_withFilterIncludingModifications() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter filterIncludingModifications = new ProgramList.Filter( + Set.of(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + ProgramFilter halFilter = + ConversionUtils.filterToHalProgramFilter(filterIncludingModifications); + + mTunerSessions[0].startProgramListUpdates(filterIncludingModifications); + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO), new ArrayList<>())); + + verify(mBroadcastRadioMock).startProgramListUpdates(halFilter); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true, + List.of(TEST_FM_INFO), new ArrayList<>())); + + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED), new ArrayList<>())); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated( + AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true, + List.of(TEST_FM_INFO_MODIFIED), new ArrayList<>())); + } + + @Test + public void onProgramListUpdated_afterSessionClosed_doesNotUpdates() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + mTunerSessions[0].startProgramListUpdates(filter); + + mTunerSessions[0].close(); + + verify(mBroadcastRadioMock).stopProgramListUpdates(); + + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO), new ArrayList<>())); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0)).onProgramListUpdated(any()); + } + + @Test + public void startProgramListUpdates_forMultipleSessions() throws Exception { + int numSessions = 3; + openAidlClients(numSessions); + ProgramList.Filter fmIdFilter = new ProgramList.Filter(new ArraySet<>(), + Set.of(TEST_FM_FREQUENCY_ID), /* includeCategories= */ false, + /* excludeModifications= */ true); + ProgramList.Filter filterExcludingCategories = new ProgramList.Filter(new ArraySet<>(), + new ArraySet<>(), /* includeCategories= */ true, + /* excludeModifications= */ true); + ProgramList.Filter rdsTypeFilter = new ProgramList.Filter( + Set.of(ProgramSelector.IDENTIFIER_TYPE_RDS_PI), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + + mTunerSessions[0].startProgramListUpdates(fmIdFilter); + + ProgramFilter halFilter = ConversionUtils.filterToHalProgramFilter(fmIdFilter); + verify(mBroadcastRadioMock).startProgramListUpdates(halFilter); + + mTunerSessions[1].startProgramListUpdates(filterExcludingCategories); + + halFilter.identifiers = new android.hardware.broadcastradio.ProgramIdentifier[]{}; + halFilter.includeCategories = true; + verify(mBroadcastRadioMock).startProgramListUpdates(halFilter); + + mTunerSessions[2].startProgramListUpdates(rdsTypeFilter); + + halFilter.excludeModifications = false; + verify(mBroadcastRadioMock).startProgramListUpdates(halFilter); + } + + @Test + public void onProgramListUpdated_forMultipleSessions() throws Exception { + int numSessions = 3; + openAidlClients(numSessions); + List<ProgramList.Filter> filters = List.of(new ProgramList.Filter( + Set.of(ProgramSelector.IDENTIFIER_TYPE_RDS_PI), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false), + new ProgramList.Filter(new ArraySet<>(), Set.of(TEST_FM_FREQUENCY_ID), + /* includeCategories= */ false, /* excludeModifications= */ true), + new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ true)); + + for (int index = 0; index < numSessions; index++) { + mTunerSessions[index].startProgramListUpdates(filters.get(index)); + } + + mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>())); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT) + .onProgramListUpdated(AidlTestUtils.makeChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_RDS_INFO), new ArrayList<>())); + verify(mAidlTunerCallbackMocks[1], CALLBACK_TIMEOUT) + .onProgramListUpdated(AidlTestUtils.makeChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_FM_INFO), new ArrayList<>())); + verify(mAidlTunerCallbackMocks[2], CALLBACK_TIMEOUT) + .onProgramListUpdated(AidlTestUtils.makeChunk(/* purge= */ false, + /* complete= */ true, List.of(TEST_RDS_INFO, TEST_FM_INFO), + new ArrayList<>())); + } + + @Test + public void startProgramListUpdates_forNonCurrentUser_doesNotStartUpdates() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].startProgramListUpdates(filter); + + verify(mBroadcastRadioMock, never()).startProgramListUpdates(any()); + } + + @Test + public void startProgramListUpdates_withUnknownErrorFromHal_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + doThrow(new ServiceSpecificException(Result.UNKNOWN_ERROR)) + .when(mBroadcastRadioMock).startProgramListUpdates(any()); + + ParcelableException thrown = assertThrows(ParcelableException.class, () -> { + mTunerSessions[0].startProgramListUpdates(/* filter= */ null); + }); + + assertWithMessage("Unknown error HAL exception when updating program list") + .that(thrown).hasMessageThat().contains("UNKNOWN_ERROR"); + } + + @Test public void stopProgramListUpdates() throws Exception { openAidlClients(/* numClients= */ 1); - ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), /* includeCategories= */ true, /* excludeModifications= */ false); - mTunerSessions[0].startProgramListUpdates(aidlFilter); + mTunerSessions[0].startProgramListUpdates(filter); mTunerSessions[0].stopProgramListUpdates(); @@ -658,9 +948,9 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { @Test public void stopProgramListUpdates_forNonCurrentUser_doesNotStopUpdates() throws Exception { openAidlClients(/* numClients= */ 1); - ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), /* includeCategories= */ true, /* excludeModifications= */ false); - mTunerSessions[0].startProgramListUpdates(aidlFilter); + mTunerSessions[0].startProgramListUpdates(filter); doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); mTunerSessions[0].stopProgramListUpdates(); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java index ea9a8461ad92..3815008bd4fb 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java @@ -373,7 +373,7 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test - public void tune_withHalHasUnknownError_fails() throws Exception { + public void tune_withUnknownErrorFromHal_fails() throws Exception { openAidlClients(/* numClients= */ 1); ProgramSelector sel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); doAnswer(invocation -> Result.UNKNOWN_ERROR).when(mHalTunerSessionMock).tune(any()); @@ -382,7 +382,7 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { mTunerSessions[0].tune(sel); }); - assertWithMessage("Exception for tuning when HAL has unknown error") + assertWithMessage("Unknown error HAL exception when tuning") .that(thrown).hasMessageThat().contains(Result.toString(Result.UNKNOWN_ERROR)); } @@ -513,7 +513,7 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test - public void seek_withHalHasInternalError_fails() throws Exception { + public void seek_withInternalErrorFromHal_fails() throws Exception { openAidlClients(/* numClients= */ 1); doAnswer(invocation -> Result.INTERNAL_ERROR).when(mHalTunerSessionMock) .scan(anyBoolean(), anyBoolean()); @@ -522,7 +522,7 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false); }); - assertWithMessage("Exception for seeking when HAL has internal error") + assertWithMessage("Internal error HAL exception when seeking") .that(thrown).hasMessageThat().contains(Result.toString(Result.INTERNAL_ERROR)); } @@ -633,6 +633,32 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void startProgramListUpdates_forNonCurrentUser_doesNotStartUpdates() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].startProgramListUpdates(filter); + + verify(mHalTunerSessionMock, never()).startProgramListUpdates(any()); + } + + @Test + public void startProgramListUpdates_withUnknownErrorFromHal_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + doAnswer(invocation -> Result.UNKNOWN_ERROR).when(mHalTunerSessionMock) + .startProgramListUpdates(any()); + + ParcelableException thrown = assertThrows(ParcelableException.class, () -> { + mTunerSessions[0].startProgramListUpdates(/* filter= */ null); + }); + + assertWithMessage("Unknown error HAL exception when updating program list") + .that(thrown).hasMessageThat().contains(Result.toString(Result.UNKNOWN_ERROR)); + } + + @Test public void stopProgramListUpdates() throws Exception { openAidlClients(/* numClients= */ 1); ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt index 625c318d9efd..249e2468d87e 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt @@ -16,6 +16,8 @@ package android.content.res + +import android.platform.test.annotations.Presubmit import androidx.core.util.forEach import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest @@ -27,6 +29,7 @@ import kotlin.math.floor import org.junit.Test import org.junit.runner.RunWith +@Presubmit @RunWith(AndroidJUnit4::class) class FontScaleConverterFactoryTest { @@ -79,10 +82,10 @@ class FontScaleConverterFactoryTest { @LargeTest @Test fun allFeasibleScalesAndConversionsDoNotCrash() { - generateSequenceOfFractions(-10000f..10000f, step = 0.01f) + generateSequenceOfFractions(-10f..10f, step = 0.01f) .mapNotNull{ FontScaleConverterFactory.forScale(it) } .flatMap{ table -> - generateSequenceOfFractions(-10000f..10000f, step = 0.01f) + generateSequenceOfFractions(-2000f..2000f, step = 0.01f) .map{ Pair(table, it) } } .forEach { (table, sp) -> diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt index e9f850e9aeff..bfa8c9ada911 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt @@ -16,11 +16,13 @@ package android.content.res +import android.platform.test.annotations.Presubmit import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertWithMessage import org.junit.Test import org.junit.runner.RunWith +@Presubmit @RunWith(AndroidJUnit4::class) class FontScaleConverterTest { diff --git a/core/tests/coretests/src/android/util/TypedValueTest.kt b/core/tests/coretests/src/android/util/TypedValueTest.kt index b020c38d6e2b..af01447fc21e 100644 --- a/core/tests/coretests/src/android/util/TypedValueTest.kt +++ b/core/tests/coretests/src/android/util/TypedValueTest.kt @@ -17,6 +17,7 @@ package android.util import android.content.res.FontScaleConverterFactory +import android.platform.test.annotations.Presubmit import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.filters.SmallTest @@ -30,6 +31,7 @@ import kotlin.math.abs import kotlin.math.min import kotlin.math.roundToInt +@Presubmit @RunWith(AndroidJUnit4::class) class TypedValueTest { @LargeTest @@ -223,7 +225,6 @@ class TypedValueTest { metrics.scaledDensity = 0f listOf( - TypedValue.COMPLEX_UNIT_PX, TypedValue.COMPLEX_UNIT_DIP, TypedValue.COMPLEX_UNIT_SP, TypedValue.COMPLEX_UNIT_PT, @@ -257,8 +258,7 @@ class TypedValueTest { TypedValue.COMPLEX_UNIT_MM ) .forEach { dimenType -> - // Test for every integer value in the range... - for (i: Int in -(1 shl 23) until (1 shl 23)) { + for (i: Int in -10000 until 10000) { assertRoundTripIsEqual(i.toFloat(), dimenType, metrics) assertRoundTripIsEqual(i - .1f, dimenType, metrics) assertRoundTripIsEqual(i + .5f, dimenType, metrics) diff --git a/core/tests/coretests/src/android/view/WindowInfoTest.java b/core/tests/coretests/src/android/view/WindowInfoTest.java index f9e3f43b562c..d927f0632273 100644 --- a/core/tests/coretests/src/android/view/WindowInfoTest.java +++ b/core/tests/coretests/src/android/view/WindowInfoTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.mock; import android.app.ActivityTaskManager; import android.graphics.Matrix; import android.os.IBinder; +import android.os.LocaleList; import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.text.TextUtils; @@ -41,6 +42,7 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; +import java.util.Locale; /** * Class for testing {@link WindowInfo}. @@ -48,6 +50,7 @@ import java.util.Arrays; @Presubmit @RunWith(AndroidJUnit4.class) public class WindowInfoTest { + private static final LocaleList TEST_LOCALES = new LocaleList(Locale.ROOT); @SmallTest @Test @@ -129,6 +132,7 @@ public class WindowInfoTest { assertTrue(windowinfo.regionInScreen.isEmpty()); assertEquals(windowinfo.mTransformMatrix.length, 9); assertTrue(windowinfo.mMagnificationSpec.isNop()); + assertEquals(windowinfo.locales, LocaleList.getEmptyLocaleList()); } private boolean areWindowsEqual(WindowInfo w1, WindowInfo w2) { @@ -141,6 +145,7 @@ public class WindowInfoTest { equality &= w1.mMagnificationSpec.equals(w2.mMagnificationSpec); equality &= Arrays.equals(w1.mTransformMatrix, w2.mTransformMatrix); equality &= TextUtils.equals(w1.title, w2.title); + equality &= w1.locales.equals(w2.locales); return equality; } @@ -164,5 +169,6 @@ public class WindowInfoTest { windowInfo.mMagnificationSpec.offsetX = 100f; windowInfo.mMagnificationSpec.offsetY = 200f; Matrix.IDENTITY_MATRIX.getValues(windowInfo.mTransformMatrix); + windowInfo.locales = TEST_LOCALES; } } diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityWindowAttributesTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityWindowAttributesTest.java index a6abee5f7550..8d1c2e30a9ae 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityWindowAttributesTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityWindowAttributesTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotSame; +import android.os.LocaleList; import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.view.WindowManager; @@ -30,6 +31,8 @@ import androidx.test.filters.SmallTest; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Locale; + /** * Class for testing {@link AccessibilityWindowAttributes}. */ @@ -37,11 +40,13 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class AccessibilityWindowAttributesTest { private static final String TEST_WINDOW_TITLE = "test window title"; + private static final LocaleList TEST_LOCALES = new LocaleList(Locale.ROOT); @SmallTest @Test public void testParceling() { - final AccessibilityWindowAttributes windowAttributes = createInstance(TEST_WINDOW_TITLE); + final AccessibilityWindowAttributes windowAttributes = createInstance( + TEST_WINDOW_TITLE, TEST_LOCALES); Parcel parcel = Parcel.obtain(); windowAttributes.writeToParcel(parcel, 0); parcel.setDataPosition(0); @@ -56,14 +61,21 @@ public class AccessibilityWindowAttributesTest { @SmallTest @Test public void testNonequality() { - final AccessibilityWindowAttributes windowAttributes = createInstance(null); - final AccessibilityWindowAttributes windowAttributes2 = createInstance(TEST_WINDOW_TITLE); + final AccessibilityWindowAttributes windowAttributes = createInstance( + null, TEST_LOCALES); + final AccessibilityWindowAttributes windowAttributes1 = createInstance( + TEST_WINDOW_TITLE, TEST_LOCALES); + final AccessibilityWindowAttributes windowAttributes2 = createInstance( + TEST_WINDOW_TITLE, null); + assertNotEquals(windowAttributes, windowAttributes1); assertNotEquals(windowAttributes, windowAttributes2); + assertNotEquals(windowAttributes1, windowAttributes2); } - private static AccessibilityWindowAttributes createInstance(String windowTitle) { + private static AccessibilityWindowAttributes createInstance( + String windowTitle, LocaleList locales) { final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.accessibilityTitle = windowTitle; - return new AccessibilityWindowAttributes(layoutParams); + return new AccessibilityWindowAttributes(layoutParams, locales); } } diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index fc199440e782..0aad0a8e290d 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -83,5 +83,6 @@ <permission name="android.permission.READ_SAFETY_CENTER_STATUS" /> <permission name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" /> <permission name="android.permission.READ_SEARCH_INDEXABLES" /> + <permission name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT"/> </privapp-permissions> </permissions> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 1070841b543e..5040a8668ae6 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -500,6 +500,8 @@ applications that come with the platform <permission name="android.permission.MODIFY_CELL_BROADCASTS" /> <!-- Permission required for CTS test - CtsBroadcastRadioTestCases --> <permission name="android.permission.ACCESS_BROADCAST_RADIO"/> + <!-- Permission required for CTS test - CtsAmbientContextServiceTestCases --> + <permission name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT"/> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java index c7c97e0b82b9..89d63040b45a 100644 --- a/graphics/java/android/graphics/HardwareRenderer.java +++ b/graphics/java/android/graphics/HardwareRenderer.java @@ -1334,6 +1334,8 @@ public class HardwareRenderer { final OverlayProperties overlayProperties = defaultDisplay.getOverlaySupport(); boolean supportFp16ForHdr = overlayProperties != null ? overlayProperties.supportFp16ForHdr() : false; + boolean supportMixedColorSpaces = overlayProperties != null + ? overlayProperties.supportMixedColorSpaces() : false; for (int i = 0; i < allDisplays.length; i++) { final Display display = allDisplays[i]; @@ -1361,7 +1363,7 @@ public class HardwareRenderer { nInitDisplayInfo(largestWidth, largestHeight, defaultDisplay.getRefreshRate(), wideColorDataspace, defaultDisplay.getAppVsyncOffsetNanos(), defaultDisplay.getPresentationDeadlineNanos(), - supportFp16ForHdr); + supportFp16ForHdr, supportMixedColorSpaces); mDisplayInitialized = true; } @@ -1542,7 +1544,7 @@ public class HardwareRenderer { private static native void nInitDisplayInfo(int width, int height, float refreshRate, int wideColorDataspace, long appVsyncOffsetNanos, long presentationDeadlineNanos, - boolean supportsFp16ForHdr); + boolean supportsFp16ForHdr, boolean nInitDisplayInfo); private static native void nSetDrawingEnabled(boolean drawingEnabled); diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index f0e496f3a178..d35dcab11f49 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -3151,10 +3151,10 @@ public class Paint { * @see #getRunAdvance(char[], int, int, int, int, boolean, int) for more details. * * @param text the text to measure. Cannot be null. - * @param start the index of the start of the range to measure - * @param end the index + 1 of the end of the range to measure - * @param contextStart the index of the start of the shaping context - * @param contextEnd the index + 1 of the end of the shaping context + * @param start the start index of the range to measure, inclusive + * @param end the end index of the range to measure, exclusive + * @param contextStart the start index of the shaping context, inclusive + * @param contextEnd the end index of the shaping context, exclusive * @param isRtl whether the run is in RTL direction * @param offset index of caret position * @param advances the array that receives the computed character advances diff --git a/graphics/java/android/graphics/drawable/LottieDrawable.java b/graphics/java/android/graphics/drawable/LottieDrawable.java new file mode 100644 index 000000000000..c1f1b50cf339 --- /dev/null +++ b/graphics/java/android/graphics/drawable/LottieDrawable.java @@ -0,0 +1,151 @@ +/* + * 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.graphics.drawable; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; + +import dalvik.annotation.optimization.FastNative; + +import libcore.util.NativeAllocationRegistry; + +import java.io.IOException; + +/** + * {@link Drawable} for drawing Lottie files. + * + * <p>The framework handles decoding subsequent frames in another thread and + * updating when necessary. The drawable will only animate while it is being + * displayed.</p> + * + * @hide + */ +@SuppressLint("NotCloseable") +public class LottieDrawable extends Drawable implements Animatable { + private long mNativePtr; + + /** + * Create an animation from the provided JSON string + * @hide + */ + private LottieDrawable(@NonNull String animationJson) throws IOException { + mNativePtr = nCreate(animationJson); + if (mNativePtr == 0) { + throw new IOException("could not make LottieDrawable from json"); + } + + final long nativeSize = nNativeByteSize(mNativePtr); + NativeAllocationRegistry registry = new NativeAllocationRegistry( + LottieDrawable.class.getClassLoader(), nGetNativeFinalizer(), nativeSize); + registry.registerNativeAllocation(this, mNativePtr); + } + + /** + * Factory for LottieDrawable, throws IOException if native Skottie Builder fails to parse + */ + public static LottieDrawable makeLottieDrawable(@NonNull String animationJson) + throws IOException { + return new LottieDrawable(animationJson); + } + + + + /** + * Draw the current frame to the Canvas. + * @hide + */ + @Override + public void draw(@NonNull Canvas canvas) { + if (mNativePtr == 0) { + throw new IllegalStateException("called draw on empty LottieDrawable"); + } + + nDraw(mNativePtr, canvas.getNativeCanvasWrapper()); + } + + /** + * Start the animation. Needs to be called before draw calls. + * @hide + */ + @Override + public void start() { + if (mNativePtr == 0) { + throw new IllegalStateException("called start on empty LottieDrawable"); + } + + if (nStart(mNativePtr)) { + invalidateSelf(); + } + } + + /** + * Stops the animation playback. Does not release anything. + * @hide + */ + @Override + public void stop() { + if (mNativePtr == 0) { + throw new IllegalStateException("called stop on empty LottieDrawable"); + } + nStop(mNativePtr); + } + + /** + * Return whether the animation is currently running. + */ + @Override + public boolean isRunning() { + if (mNativePtr == 0) { + throw new IllegalStateException("called isRunning on empty LottieDrawable"); + } + return nIsRunning(mNativePtr); + } + + @Override + public int getOpacity() { + // We assume translucency to avoid checking each pixel. + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + //TODO + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + //TODO + } + + private static native long nCreate(String json); + private static native void nDraw(long nativeInstance, long nativeCanvas); + @FastNative + private static native long nGetNativeFinalizer(); + @FastNative + private static native long nNativeByteSize(long nativeInstance); + @FastNative + private static native boolean nIsRunning(long nativeInstance); + @FastNative + private static native boolean nStart(long nativeInstance); + @FastNative + private static native boolean nStop(long nativeInstance); + +} 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 00e13c94ea90..ee8ec1dd1008 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -239,6 +239,11 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) { + if (secondary == null) { + wct.clearAdjacentTaskFragments(primary); + return; + } + WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null; final boolean finishSecondaryWithPrimary = splitRule != null && SplitContainer.shouldFinishSecondaryWithPrimary(splitRule); @@ -310,16 +315,12 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { OP_TYPE_SET_ANIMATION_PARAMS) .setAnimationParams(animationParams) .build(); - wct.setTaskFragmentOperation(fragmentToken, operation); + wct.addTaskFragmentOperation(fragmentToken, operation); } void deleteTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken) { - if (!mFragmentInfos.containsKey(fragmentToken)) { - throw new IllegalArgumentException( - "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken); - } - wct.deleteTaskFragment(mFragmentInfos.get(fragmentToken).getToken()); + wct.deleteTaskFragment(fragmentToken); } void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) { 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 868ced0e555e..b13c6724ed0e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -20,6 +20,8 @@ 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.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; 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; @@ -31,8 +33,6 @@ import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; @@ -67,6 +67,7 @@ import android.util.SparseArray; import android.view.WindowMetrics; import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOperation; import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; @@ -592,11 +593,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") void onTaskFragmentError(@NonNull WindowContainerTransaction wct, @Nullable IBinder errorCallbackToken, @Nullable TaskFragmentInfo taskFragmentInfo, - int opType, @NonNull Throwable exception) { + @TaskFragmentOperation.OperationType int opType, @NonNull Throwable exception) { Log.e(TAG, "onTaskFragmentError=" + exception.getMessage()); switch (opType) { - case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: - case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: { + case OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: + case OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: { final TaskFragmentContainer container; if (taskFragmentInfo != null) { container = getContainer(taskFragmentInfo.getFragmentToken()); 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 d9abe8e040ba..85a00dfc010c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -30,6 +30,7 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; +import android.util.DisplayMetrics; import android.util.LayoutDirection; import android.util.Pair; import android.util.Size; @@ -555,9 +556,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator = mController.getSplitAttributesCalculator(); final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes(); - final boolean isDefaultMinSizeSatisfied = rule.checkParentMetrics(taskWindowMetrics); + final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics); if (calculator == null) { - if (!isDefaultMinSizeSatisfied) { + if (!areDefaultConstraintsSatisfied) { return EXPAND_CONTAINERS_ATTRIBUTES; } return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes, @@ -567,8 +568,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(), taskConfiguration.windowConfiguration); final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams( - taskWindowMetrics, taskConfiguration, defaultSplitAttributes, - isDefaultMinSizeSatisfied, windowLayoutInfo, rule.getTag()); + taskWindowMetrics, taskConfiguration, windowLayoutInfo, defaultSplitAttributes, + areDefaultConstraintsSatisfied, rule.getTag()); final SplitAttributes splitAttributes = calculator.apply(params); return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair); } @@ -972,6 +973,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { 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); + final float density = taskConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; + return new WindowMetrics(taskBounds, WindowInsets.CONSUMED, density); } } 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 0bf0bc85b511..a26311efc23e 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 @@ -20,13 +20,13 @@ 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.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; 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; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; 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.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY; import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY; @@ -1139,7 +1139,7 @@ public class SplitControllerTest { final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); final IBinder errorToken = new Binder(); final TaskFragmentInfo info = mock(TaskFragmentInfo.class); - final int opType = HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; + final int opType = OP_TYPE_CREATE_TASK_FRAGMENT; final Exception exception = new SecurityException("test"); final Bundle errorBundle = TaskFragmentOrganizer.putErrorInfoInBundle(exception, info, opType); 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 ff1256b47429..07d01589be5a 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 @@ -66,8 +66,10 @@ import android.graphics.Color; import android.graphics.Rect; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.util.DisplayMetrics; import android.util.Pair; import android.util.Size; +import android.view.WindowMetrics; import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOperation; @@ -101,7 +103,6 @@ import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class SplitPresenterTest { - @Mock private Activity mActivity; @Mock private Resources mActivityResources; @@ -193,7 +194,7 @@ public class SplitPresenterTest { OP_TYPE_SET_ANIMATION_PARAMS) .setAnimationParams(animationParams) .build(); - verify(mTransaction).setTaskFragmentOperation(container.getTaskFragmentToken(), + verify(mTransaction).addTaskFragmentOperation(container.getTaskFragmentToken(), expectedOperation); assertTrue(container.areLastRequestedAnimationParamsEqual(animationParams)); @@ -202,7 +203,7 @@ public class SplitPresenterTest { mPresenter.updateAnimationParams(mTransaction, container.getTaskFragmentToken(), animationParams); - verify(mTransaction, never()).setTaskFragmentOperation(any(), any()); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); } @Test @@ -571,6 +572,21 @@ public class SplitPresenterTest { splitPairRule, null /* minDimensionsPair */)); } + @Test + public void testGetTaskWindowMetrics() { + final Configuration taskConfig = new Configuration(); + taskConfig.windowConfiguration.setBounds(TASK_BOUNDS); + taskConfig.densityDpi = 123; + final TaskContainer.TaskProperties taskProperties = new TaskContainer.TaskProperties( + DEFAULT_DISPLAY, taskConfig); + doReturn(taskProperties).when(mPresenter).getTaskProperties(mActivity); + + final WindowMetrics windowMetrics = mPresenter.getTaskWindowMetrics(mActivity); + assertEquals(TASK_BOUNDS, windowMetrics.getBounds()); + assertEquals(123 * DisplayMetrics.DENSITY_DEFAULT_SCALE, + windowMetrics.getDensity(), 0f); + } + private Activity createMockActivity() { final Activity activity = mock(Activity.class); final Configuration activityConfig = new Configuration(); diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differindex a939cd8a1745..5de5365e4d03 100644 --- a/libs/WindowManager/Jetpack/window-extensions-release.aar +++ b/libs/WindowManager/Jetpack/window-extensions-release.aar diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 065fd95b3ebc..b5ef72aec6aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -257,12 +257,30 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param listener The listener to get the created task callback. + */ public void createRootTask(int displayId, int windowingMode, TaskListener listener) { - ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s", + createRootTask(displayId, windowingMode, listener, false /* removeWithTaskOrganizer */); + } + + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param listener The listener to get the created task callback. + * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed. + */ + public void createRootTask(int displayId, int windowingMode, TaskListener listener, + boolean removeWithTaskOrganizer) { + ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s" , displayId, windowingMode, listener.toString()); final IBinder cookie = new Binder(); setPendingLaunchCookieListener(cookie, listener); - super.createRootTask(displayId, windowingMode, cookie); + super.createRootTask(displayId, windowingMode, cookie, removeWithTaskOrganizer); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 9674b69baa00..360bfe78bf07 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -981,21 +981,59 @@ public class BubbleController implements ConfigurationChangeListener { } /** - * Adds and expands bubble for a specific intent. These bubbles are <b>not</b> backed by a n - * otification and remain until the user dismisses the bubble or bubble stack. Only one intent - * bubble is supported at a time. + * This method has different behavior depending on: + * - if an app bubble exists + * - if an app bubble is expanded + * + * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * intent must be explicit (i.e. include a package name or fully qualified component class name) + * and the activity for it should be resizable. + * + * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is + * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * this method will expand it. + * + * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses + * the bubble or bubble stack. + * + * Some notes: + * - Only one app bubble is supported at a time + * - Calling this method with a different intent than the existing app bubble will do nothing * * @param intent the intent to display in the bubble expanded view. */ - public void showAppBubble(Intent intent) { - if (intent == null || intent.getPackage() == null) return; + public void showOrHideAppBubble(Intent intent) { + if (intent == null || intent.getPackage() == null) { + Log.w(TAG, "App bubble failed to show, invalid intent: " + intent + + ((intent != null) ? " with package: " + intent.getPackage() : " ")); + return; + } PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId); if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return; - Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor); - b.setShouldAutoExpand(true); - inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE); + if (existingAppBubble != null) { + BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); + if (isStackExpanded()) { + if (selectedBubble != null && KEY_APP_BUBBLE.equals(selectedBubble.getKey())) { + // App bubble is expanded, lets collapse + collapseStack(); + } else { + // App bubble is not selected, select it + mBubbleData.setSelectedBubble(existingAppBubble); + } + } else { + // App bubble is not selected, select it & expand + mBubbleData.setSelectedBubble(existingAppBubble); + mBubbleData.setExpanded(true); + } + } else { + // App bubble does not exist, lets add and expand it + Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor); + b.setShouldAutoExpand(true); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } } /** @@ -1705,9 +1743,9 @@ public class BubbleController implements ConfigurationChangeListener { } @Override - public void showAppBubble(Intent intent) { + public void showOrHideAppBubble(Intent intent) { mMainExecutor.execute(() -> { - BubbleController.this.showAppBubble(intent); + BubbleController.this.showOrHideAppBubble(intent); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index af31391fec96..6230d22ebe12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -17,6 +17,7 @@ package com.android.wm.shell.bubbles; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -684,7 +685,8 @@ public class BubbleData { if (bubble.getPendingIntentCanceled() || !(reason == Bubbles.DISMISS_AGED || reason == Bubbles.DISMISS_USER_GESTURE - || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { + || reason == Bubbles.DISMISS_RELOAD_FROM_DISK) + || KEY_APP_BUBBLE.equals(bubble.getKey())) { return; } if (DEBUG_BUBBLE_DATA) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 465d1abe0a3d..df4325763a17 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -109,13 +109,28 @@ public interface Bubbles { void expandStackAndSelectBubble(Bubble bubble); /** - * Adds and expands bubble that is not notification based, but instead based on an intent from - * the app. The intent must be explicit (i.e. include a package name or fully qualified - * component class name) and the activity for it should be resizable. + * This method has different behavior depending on: + * - if an app bubble exists + * - if an app bubble is expanded * - * @param intent the intent to populate the bubble. + * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * intent must be explicit (i.e. include a package name or fully qualified component class name) + * and the activity for it should be resizable. + * + * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is + * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * this method will expand it. + * + * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses + * the bubble or bubble stack. + * + * Some notes: + * - Only one app bubble is supported at a time + * - Calling this method with a different intent than the existing app bubble will do nothing + * + * @param intent the intent to display in the bubble expanded view. */ - void showAppBubble(Intent intent); + void showOrHideAppBubble(Intent intent); /** * @return a bubble that matches the provided shortcutId, if one exists. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index e6c7e101d078..83158ffafa7e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -662,8 +662,8 @@ public class PipTransition extends PipTransitionController { } // Please file a bug to handle the unexpected transition type. - throw new IllegalStateException("Entering PIP with unexpected transition type=" - + transitTypeToString(transitType)); + android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type=" + + transitTypeToString(transitType), new Throwable()); } return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index a0a8f9fb2cde..94e593b106a5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -333,6 +333,9 @@ public class PhonePipMenuController implements PipMenuController { mTmpDestinationRectF.set(destinationBounds); mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); final SurfaceControl surfaceControl = getSurfaceControl(); + if (surfaceControl == null) { + return; + } final SurfaceControl.Transaction menuTx = mSurfaceControlTransactionFactory.getTransaction(); menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform); @@ -359,6 +362,9 @@ public class PhonePipMenuController implements PipMenuController { } final SurfaceControl surfaceControl = getSurfaceControl(); + if (surfaceControl == null) { + return; + } final SurfaceControl.Transaction menuTx = mSurfaceControlTransactionFactory.getTransaction(); menuTx.setCrop(surfaceControl, destinationBounds); 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 8ddc3c04d991..1488469759cd 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 @@ -605,9 +605,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); + if (taskId2 == INVALID_TASK_ID) { + // Launching a solo task. + ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); + activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); + options1 = activityOptions.toBundle(); + addActivityOptions(options1, null /* launchTarget */); + wct.startTask(taskId1, options1); + mSyncQueue.queue(wct); + return; + } + addActivityOptions(options1, mSideStage); wct.startTask(taskId1, options1); - startWithLegacyTransition(wct, taskId2, options2, splitPosition, splitRatio, adapter, instanceId); } @@ -632,9 +642,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); + if (taskId == INVALID_TASK_ID) { + // Launching a solo task. + ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); + activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); + options1 = activityOptions.toBundle(); + addActivityOptions(options1, null /* launchTarget */); + wct.sendPendingIntent(pendingIntent, fillInIntent, options1); + mSyncQueue.queue(wct); + return; + } + addActivityOptions(options1, mSideStage); wct.sendPendingIntent(pendingIntent, fillInIntent, options1); - startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter, instanceId); } @@ -696,6 +716,34 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mShouldUpdateRecents = false; mIsSplitEntering = true; + setSideStagePosition(sidePosition, wct); + if (!mMainStage.isActive()) { + mMainStage.activate(wct, false /* reparent */); + } + + if (mainOptions == null) mainOptions = new Bundle(); + addActivityOptions(mainOptions, mMainStage); + mainOptions = wrapAsSplitRemoteAnimation(adapter, mainOptions); + + updateWindowBounds(mSplitLayout, wct); + if (mainTaskId == INVALID_TASK_ID) { + wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions); + } else { + wct.startTask(mainTaskId, mainOptions); + } + + wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); + + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + setDividerVisibility(true, t); + }); + + setEnterInstanceId(instanceId); + } + + private Bundle wrapAsSplitRemoteAnimation(RemoteAnimationAdapter adapter, Bundle options) { final WindowContainerTransaction evictWct = new WindowContainerTransaction(); if (isSplitScreenVisible()) { mMainStage.evictAllChildren(evictWct); @@ -739,37 +787,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, }; RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter( wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay()); - - if (mainOptions == null) { - mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle(); - } else { - ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions); - mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); - mainOptions = mainActivityOptions.toBundle(); - } - - setSideStagePosition(sidePosition, wct); - if (!mMainStage.isActive()) { - mMainStage.activate(wct, false /* reparent */); - } - - if (mainOptions == null) mainOptions = new Bundle(); - addActivityOptions(mainOptions, mMainStage); - updateWindowBounds(mSplitLayout, wct); - if (mainTaskId == INVALID_TASK_ID) { - wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions); - } else { - wct.startTask(mainTaskId, mainOptions); - } - wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); - - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - setDividerVisibility(true, t); - }); - - setEnterInstanceId(instanceId); + ActivityOptions activityOptions = ActivityOptions.fromBundle(options); + activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); + return activityOptions.toBundle(); } private void setEnterInstanceId(InstanceId instanceId) { @@ -1228,8 +1248,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return SPLIT_POSITION_UNDEFINED; } - private void addActivityOptions(Bundle opts, StageTaskListener stage) { - opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); + private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) { + if (launchTarget != null) { + opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, launchTarget.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); diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt index 7fc12f06f530..7a86c2557bb4 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.flicker.bubble import android.content.Context import android.graphics.Point -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.util.DisplayMetrics import android.view.WindowManager @@ -74,20 +73,4 @@ open class DismissBubbleScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) open fun testAppIsAlwaysVisible() { flicker.assertLayers { this.isVisible(testApp) } } - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt index 08ed91b3cab1..379d5e90406b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt @@ -94,9 +94,19 @@ class LaunchBubbleFromLockScreen(flicker: FlickerTest) : BaseBubbleScreen(flicke flicker.assertLayersEnd { this.isVisible(testApp) } } - @Postsubmit @Test fun navBarLayerIsVisibleAtEnd() = flicker.navBarLayerIsVisibleAtEnd() + @Postsubmit + @Test + fun navBarLayerIsVisibleAtEnd() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarLayerIsVisibleAtEnd() + } - @Postsubmit @Test fun navBarLayerPositionAtEnd() = flicker.navBarLayerPositionAtEnd() + @Postsubmit + @Test + fun navBarLayerPositionAtEnd() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarLayerPositionAtEnd() + } /** {@inheritDoc} */ @FlakyTest @@ -127,42 +137,4 @@ class LaunchBubbleFromLockScreen(flicker: FlickerTest) : BaseBubbleScreen(flicke Assume.assumeTrue(flicker.scenario.isGesturalNavigation) super.navBarWindowIsAlwaysVisible() } - - /** {@inheritDoc} */ - @FlakyTest(bugId = 242088970) - @Test - override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() - - /** {@inheritDoc} */ - @FlakyTest(bugId = 242088970) - @Test - override fun statusBarLayerIsVisibleAtStartAndEnd() = - super.statusBarLayerIsVisibleAtStartAndEnd() - - /** {@inheritDoc} */ - @FlakyTest(bugId = 242088970) - @Test - override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() - - /** {@inheritDoc} */ - @FlakyTest(bugId = 242088970) - @Test - override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() - - /** {@inheritDoc} */ - @FlakyTest(bugId = 242088970) - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - - @FlakyTest(bugId = 251217773) - @Test - override fun entireScreenCovered() { - super.entireScreenCovered() - } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt index b69ff6451d1c..5c0f8540db02 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.bubble +import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until @@ -57,6 +58,7 @@ open class LaunchBubbleScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) } } + @Presubmit @Test open fun testAppIsAlwaysVisible() { flicker.assertLayers { this.isVisible(testApp) } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt index bc9fc7301541..8a694f770ab0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt @@ -52,7 +52,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTest(flicker) { +open class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTest(flicker) { /** Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit get() = { @@ -61,6 +61,8 @@ class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTest(flicker) pipApp.enableEnterPipOnUserLeaveHint() } teardown { + // close gracefully so that onActivityUnpinned() can be called before force exit + pipApp.closePipWindow(wmHelper) pipApp.exit(wmHelper) } transitions { tapl.goHome() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt new file mode 100644 index 000000000000..e47805001cd0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** This test will fail because of b/264261596 */ +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterPipOnUserLeaveHintTestCfArm(flicker: FlickerTest) : EnterPipOnUserLeaveHintTest(flicker) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTestCfArm.kt new file mode 100644 index 000000000000..d2e864587431 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTestCfArm.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.FlickerTestFactory +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.traces.common.service.PlatformConsts +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterPipTestCfArm(flicker: FlickerTest) : EnterPipTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation + * and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0) + ) + } + } +} 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 da162401cf79..ea6c14d7152e 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 @@ -67,7 +67,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class EnterPipToOtherOrientationTest(flicker: FlickerTest) : PipTransition(flicker) { +open class EnterPipToOtherOrientationTest(flicker: FlickerTest) : PipTransition(flicker) { private val testApp = FixedOrientationAppHelper(instrumentation) private val startingBounds = WindowUtils.getDisplayBounds(PlatformConsts.Rotation.ROTATION_90) private val endingBounds = WindowUtils.getDisplayBounds(PlatformConsts.Rotation.ROTATION_0) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTestCfArm.kt new file mode 100644 index 000000000000..39aab6ee49b7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTestCfArm.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.FlickerTestFactory +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.traces.common.service.PlatformConsts +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** This test fails because of b/264261596 */ +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class EnterPipToOtherOrientationTestCfArm(flicker: FlickerTest) : + EnterPipToOtherOrientationTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt index 1420f8ce653a..b5a5004aa553 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt @@ -56,7 +56,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ExitPipViaExpandButtonClickTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) { +open class ExitPipViaExpandButtonClickTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) { /** Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTestCfArm.kt new file mode 100644 index 000000000000..f77e335d8f52 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTestCfArm.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.FlickerTestFactory +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.traces.common.service.PlatformConsts +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExitPipViaExpandButtonClickTestCfArm(flicker: FlickerTest) : + ExitPipViaExpandButtonClickTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0) + ) + } + } +} 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 a9fe93d15428..1bf1354f56aa 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.flicker.pip import android.app.Instrumentation import android.content.Intent +import android.platform.test.annotations.Postsubmit import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.helpers.PipAppHelper @@ -25,8 +26,11 @@ 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.server.wm.traces.common.ComponentNameMatcher import com.android.server.wm.traces.common.service.PlatformConsts import com.android.wm.shell.flicker.BaseTest +import com.google.common.truth.Truth +import org.junit.Test abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) { protected val pipApp = PipAppHelper(instrumentation) @@ -56,7 +60,6 @@ abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) { * Gets a configuration that handles basic setup and teardown of pip tests and that launches the * Pip app for test * - * @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 Additional segment of flicker specification */ @@ -78,4 +81,21 @@ abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) { extraSpec(this) } } + + @Postsubmit + @Test + fun hasAtMostOnePipDismissOverlayWindow() { + val matcher = ComponentNameMatcher("", "pip-dismiss-overlay") + flicker.assertWm { + val overlaysPerState = trace.entries.map { entry -> + entry.windowStates.count { window -> + matcher.windowMatchesAnyOf(window) + } <= 1 + } + + Truth.assertWithMessage("Number of dismiss overlays per state") + .that(overlaysPerState) + .doesNotContain(false) + } + } } 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 7403aab7d4c0..0432a8497fbe 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 @@ -31,22 +31,26 @@ import org.junit.Test @RequiresDevice class TvPipMenuTests : TvPipTestBase() { - private val systemUiResources = + private val systemUiResources by lazy { packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) - private val pipBoundsWhileInMenu: Rect = + } + private val pipBoundsWhileInMenu: Rect by lazy { systemUiResources.run { val bounds = getString(getIdentifier("pip_menu_bounds", "string", SYSTEM_UI_PACKAGE_NAME)) Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds") } - private val playButtonDescription = + } + private val playButtonDescription by lazy { systemUiResources.run { getString(getIdentifier("pip_play", "string", SYSTEM_UI_PACKAGE_NAME)) } - private val pauseButtonDescription = + } + private val pauseButtonDescription by lazy { systemUiResources.run { getString(getIdentifier("pip_pause", "string", SYSTEM_UI_PACKAGE_NAME)) } + } @Before fun tvPipMenuTestsTestUp() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index e6711aca19c1..8b025cd7c246 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; + import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -32,6 +34,7 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; +import android.content.Intent; import android.content.LocusId; import android.graphics.drawable.Icon; import android.os.Bundle; @@ -94,6 +97,7 @@ public class BubbleDataTest extends ShellTestCase { private Bubble mBubbleInterruptive; private Bubble mBubbleDismissed; private Bubble mBubbleLocusId; + private Bubble mAppBubble; private BubbleData mBubbleData; private TestableBubblePositioner mPositioner; @@ -178,6 +182,11 @@ public class BubbleDataTest extends ShellTestCase { mBubbleMetadataFlagListener, mPendingIntentCanceledListener, mMainExecutor); + + Intent appBubbleIntent = new Intent(mContext, BubblesTestActivity.class); + appBubbleIntent.setPackage(mContext.getPackageName()); + mAppBubble = new Bubble(appBubbleIntent, new UserHandle(1), mMainExecutor); + mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner, @@ -1089,6 +1098,18 @@ public class BubbleDataTest extends ShellTestCase { assertOverflowChangedTo(ImmutableList.of()); } + @Test + public void test_removeAppBubble_skipsOverflow() { + mBubbleData.notificationEntryUpdated(mAppBubble, true /* suppressFlyout*/, + false /* showInShade */); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isEqualTo(mAppBubble); + + mBubbleData.dismissBubbleWithKey(KEY_APP_BUBBLE, Bubbles.DISMISS_USER_GESTURE); + + assertThat(mBubbleData.getOverflowBubbleWithKey(KEY_APP_BUBBLE)).isNull(); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull(); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 59e4b7acdba7..aeead5efc48a 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -78,6 +78,7 @@ cc_defaults { "external/skia/src/utils", "external/skia/src/gpu", "external/skia/src/shaders", + "external/skia/modules/skottie", ], }, host: { @@ -214,6 +215,15 @@ filegroup { path: "apex/java", } +java_api_contribution { + name: "framework-graphics-public-stubs", + api_surface: "public", + api_file: "api/current.txt", + visibility: [ + "//build/orchestrator/apis", + ], +} + // ------------------------ // APEX // ------------------------ @@ -375,6 +385,7 @@ cc_defaults { "external/skia/src/effects", "external/skia/src/image", "external/skia/src/images", + "external/skia/modules/skottie", ], shared_libs: [ @@ -402,6 +413,7 @@ cc_defaults { "jni/BitmapRegionDecoder.cpp", "jni/GIFMovie.cpp", "jni/GraphicsStatsService.cpp", + "jni/LottieDrawable.cpp", "jni/Movie.cpp", "jni/MovieImpl.cpp", "jni/pdf/PdfDocument.cpp", @@ -509,6 +521,7 @@ cc_defaults { "hwui/BlurDrawLooper.cpp", "hwui/Canvas.cpp", "hwui/ImageDecoder.cpp", + "hwui/LottieDrawable.cpp", "hwui/MinikinSkia.cpp", "hwui/MinikinUtils.cpp", "hwui/PaintImpl.cpp", diff --git a/libs/hwui/DeviceInfo.cpp b/libs/hwui/DeviceInfo.cpp index 0240c86d5f45..32bc122fdc58 100644 --- a/libs/hwui/DeviceInfo.cpp +++ b/libs/hwui/DeviceInfo.cpp @@ -108,6 +108,10 @@ void DeviceInfo::setSupportFp16ForHdr(bool supportFp16ForHdr) { get()->mSupportFp16ForHdr = supportFp16ForHdr; } +void DeviceInfo::setSupportMixedColorSpaces(bool supportMixedColorSpaces) { + get()->mSupportMixedColorSpaces = supportMixedColorSpaces; +} + void DeviceInfo::onRefreshRateChanged(int64_t vsyncPeriod) { mVsyncPeriod = vsyncPeriod; } diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index 577780bbb5e0..d4af0872e31e 100644 --- a/libs/hwui/DeviceInfo.h +++ b/libs/hwui/DeviceInfo.h @@ -62,6 +62,9 @@ public: static void setSupportFp16ForHdr(bool supportFp16ForHdr); static bool isSupportFp16ForHdr() { return get()->mSupportFp16ForHdr; }; + static void setSupportMixedColorSpaces(bool supportMixedColorSpaces); + static bool isSupportMixedColorSpaces() { return get()->mSupportMixedColorSpaces; }; + // this value is only valid after the GPU has been initialized and there is a valid graphics // context or if you are using the HWUI_NULL_GPU int maxTextureSize() const; @@ -92,6 +95,7 @@ private: int mMaxTextureSize; sk_sp<SkColorSpace> mWideColorSpace = SkColorSpace::MakeSRGB(); bool mSupportFp16ForHdr = false; + bool mSupportMixedColorSpaces = false; SkColorType mWideColorType = SkColorType::kN32_SkColorType; int mDisplaysSize = 0; int mPhysicalDisplayIndex = -1; diff --git a/libs/hwui/MemoryPolicy.h b/libs/hwui/MemoryPolicy.h index 41ced8cebf83..2f0f7f506447 100644 --- a/libs/hwui/MemoryPolicy.h +++ b/libs/hwui/MemoryPolicy.h @@ -53,8 +53,8 @@ struct MemoryPolicy { // Whether or not to only purge scratch resources when triggering UI Hidden or background // collection bool purgeScratchOnly = true; - // EXPERIMENTAL: Whether or not to trigger releasing GPU context when all contexts are stopped - bool releaseContextOnStoppedOnly = false; + // Whether or not to trigger releasing GPU context when all contexts are stopped + bool releaseContextOnStoppedOnly = true; }; const MemoryPolicy& loadMemoryPolicy(); diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp index 8dcd6dbe6421..045de35c1d97 100644 --- a/libs/hwui/Readback.cpp +++ b/libs/hwui/Readback.cpp @@ -28,6 +28,7 @@ #include <SkRefCnt.h> #include <SkSamplingOptions.h> #include <SkSurface.h> +#include "include/gpu/GpuTypes.h" // from Skia #include <gui/TraceUtils.h> #include <private/android/AHardwareBufferHelpers.h> #include <shaders/shaders.h> @@ -170,14 +171,15 @@ void Readback::copySurfaceInto(ANativeWindow* window, const std::shared_ptr<Copy SkBitmap skBitmap = request->getDestinationBitmap(srcRect.width(), srcRect.height()); SkBitmap* bitmap = &skBitmap; sk_sp<SkSurface> tmpSurface = - SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes, + SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), skgpu::Budgeted::kYes, bitmap->info(), 0, kTopLeft_GrSurfaceOrigin, nullptr); // if we can't generate a GPU surface that matches the destination bitmap (e.g. 565) then we // attempt to do the intermediate rendering step in 8888 if (!tmpSurface.get()) { SkImageInfo tmpInfo = bitmap->info().makeColorType(SkColorType::kN32_SkColorType); - tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes, + tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), + skgpu::Budgeted::kYes, tmpInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr); if (!tmpSurface.get()) { ALOGW("Unable to generate GPU buffer in a format compatible with the provided bitmap"); @@ -345,14 +347,17 @@ bool Readback::copyLayerInto(Layer* layer, const SkRect* srcRect, const SkRect* * software buffer. */ sk_sp<SkSurface> tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), - SkBudgeted::kYes, bitmap->info(), 0, + skgpu::Budgeted::kYes, + bitmap->info(), + 0, kTopLeft_GrSurfaceOrigin, nullptr); // if we can't generate a GPU surface that matches the destination bitmap (e.g. 565) then we // attempt to do the intermediate rendering step in 8888 if (!tmpSurface.get()) { SkImageInfo tmpInfo = bitmap->info().makeColorType(SkColorType::kN32_SkColorType); - tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes, + tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), + skgpu::Budgeted::kYes, tmpInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr); if (!tmpSurface.get()) { ALOGW("Unable to generate GPU buffer in a format compatible with the provided bitmap"); diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index 5b6fff158f10..e1030b0faf8e 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -44,6 +44,7 @@ #include "SkTextBlob.h" #include "SkVertices.h" #include "VectorDrawable.h" +#include "include/gpu/GpuTypes.h" // from Skia #include "pipeline/skia/AnimatedDrawables.h" #include "pipeline/skia/FunctorDrawable.h" @@ -570,7 +571,7 @@ public: GrRecordingContext* directContext = c->recordingContext(); mLayerImageInfo = c->imageInfo().makeWH(deviceBounds.width(), deviceBounds.height()); - mLayerSurface = SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes, + mLayerSurface = SkSurface::MakeRenderTarget(directContext, skgpu::Budgeted::kYes, mLayerImageInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr); } diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 2539694a73ee..b7d4dc90f429 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -16,11 +16,13 @@ #pragma once -#include "CanvasTransform.h" -#include "hwui/Bitmap.h" -#include "utils/Macros.h" -#include "utils/TypeLogic.h" +#include <SkRuntimeEffect.h> +#include <log/log.h> + +#include <cstdlib> +#include <vector> +#include "CanvasTransform.h" #include "SkCanvas.h" #include "SkCanvasVirtualEnforcer.h" #include "SkDrawable.h" @@ -28,11 +30,11 @@ #include "SkPaint.h" #include "SkPath.h" #include "SkRect.h" - +#include "hwui/Bitmap.h" #include "pipeline/skia/AnimatedDrawables.h" - -#include <SkRuntimeEffect.h> -#include <vector> +#include "utils/AutoMalloc.h" +#include "utils/Macros.h" +#include "utils/TypeLogic.h" enum class SkBlendMode; class SkRRect; @@ -145,7 +147,7 @@ private: template <typename Fn, typename... Args> void map(const Fn[], Args...) const; - SkAutoTMalloc<uint8_t> fBytes; + AutoTMalloc<uint8_t> fBytes; size_t fUsed = 0; size_t fReserved = 0; diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index d83d78f650aa..af8bd263f97d 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -16,23 +16,12 @@ #include "SkiaCanvas.h" -#include "CanvasProperty.h" -#include "NinePatchUtils.h" -#include "SkBlendMode.h" -#include "VectorDrawable.h" -#include "hwui/Bitmap.h" -#include "hwui/MinikinUtils.h" -#include "hwui/PaintFilter.h" -#include "pipeline/skia/AnimatedDrawables.h" -#include "pipeline/skia/HolePunch.h" - #include <SkAndroidFrameworkUtils.h> #include <SkAnimatedImage.h> #include <SkBitmap.h> #include <SkCanvasPriv.h> #include <SkCanvasStateUtils.h> #include <SkColorFilter.h> -#include <SkDeque.h> #include <SkDrawable.h> #include <SkFont.h> #include <SkGraphics.h> @@ -41,8 +30,8 @@ #include <SkMatrix.h> #include <SkPaint.h> #include <SkPicture.h> -#include <SkRSXform.h> #include <SkRRect.h> +#include <SkRSXform.h> #include <SkRect.h> #include <SkRefCnt.h> #include <SkShader.h> @@ -54,6 +43,16 @@ #include <optional> #include <utility> +#include "CanvasProperty.h" +#include "NinePatchUtils.h" +#include "SkBlendMode.h" +#include "VectorDrawable.h" +#include "hwui/Bitmap.h" +#include "hwui/MinikinUtils.h" +#include "hwui/PaintFilter.h" +#include "pipeline/skia/AnimatedDrawables.h" +#include "pipeline/skia/HolePunch.h" + namespace android { using uirenderer::PaintUtils; @@ -176,7 +175,7 @@ int SkiaCanvas::save(SaveFlags::Flags flags) { // operation. It does this by explicitly saving off the clip & matrix state // when requested and playing it back after the SkCanvas::restore. void SkiaCanvas::restore() { - const auto* rec = this->currentSaveRec(); + const SaveRec* rec = this->currentSaveRec(); if (!rec) { // Fast path - no record for this frame. mCanvas->restore(); @@ -245,7 +244,9 @@ void SkiaCanvas::restoreUnclippedLayer(int restoreCount, const Paint& paint) { } const SkiaCanvas::SaveRec* SkiaCanvas::currentSaveRec() const { - const SaveRec* rec = mSaveStack ? static_cast<const SaveRec*>(mSaveStack->back()) : nullptr; + const SaveRec* rec = (mSaveStack && !mSaveStack->empty()) + ? static_cast<const SaveRec*>(&mSaveStack->back()) + : nullptr; int currentSaveCount = mCanvas->getSaveCount(); SkASSERT(!rec || currentSaveCount >= rec->saveCount); @@ -277,13 +278,12 @@ void SkiaCanvas::recordPartialSave(SaveFlags::Flags flags) { } if (!mSaveStack) { - mSaveStack.reset(new SkDeque(sizeof(struct SaveRec), 8)); + mSaveStack.reset(new std::deque<SaveRec>()); } - SaveRec* rec = static_cast<SaveRec*>(mSaveStack->push_back()); - rec->saveCount = mCanvas->getSaveCount(); - rec->saveFlags = flags; - rec->clipIndex = mClipStack.size(); + mSaveStack->emplace_back(mCanvas->getSaveCount(), // saveCount + flags, // saveFlags + mClipStack.size()); // clipIndex } template <typename T> @@ -314,7 +314,7 @@ void SkiaCanvas::applyPersistentClips(size_t clipStartIndex) { // If the current/post-restore save rec is also persisting clips, we // leave them on the stack to be reapplied part of the next restore(). // Otherwise we're done and just pop them. - const auto* rec = this->currentSaveRec(); + const SaveRec* rec = this->currentSaveRec(); if (!rec || (rec->saveFlags & SaveFlags::Clip)) { mClipStack.erase(begin, end); } @@ -736,6 +736,10 @@ double SkiaCanvas::drawAnimatedImage(AnimatedImageDrawable* imgDrawable) { return imgDrawable->drawStaging(mCanvas); } +void SkiaCanvas::drawLottie(LottieDrawable* lottieDrawable) { + lottieDrawable->drawStaging(mCanvas); +} + void SkiaCanvas::drawVectorDrawable(VectorDrawableRoot* vectorDrawable) { vectorDrawable->drawStaging(this); } diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h index 31e3b4c3c7e2..533106db37e5 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -19,20 +19,20 @@ #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration #include "DeferredLayerUpdater.h" #endif +#include <SkCanvas.h> + +#include <cassert> +#include <deque> +#include <optional> + #include "RenderNode.h" #include "VectorDrawable.h" +#include "hwui/BlurDrawLooper.h" #include "hwui/Canvas.h" #include "hwui/Paint.h" -#include "hwui/BlurDrawLooper.h" - -#include <SkCanvas.h> -#include <SkDeque.h> #include "pipeline/skia/AnimatedDrawables.h" #include "src/core/SkArenaAlloc.h" -#include <cassert> -#include <optional> - enum class SkBlendMode; class SkRRect; @@ -145,6 +145,7 @@ public: float dstTop, float dstRight, float dstBottom, const Paint* paint) override; virtual double drawAnimatedImage(AnimatedImageDrawable* imgDrawable) override; + virtual void drawLottie(LottieDrawable* lottieDrawable) override; virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override; @@ -211,6 +212,9 @@ private: int saveCount; SaveFlags::Flags saveFlags; size_t clipIndex; + + SaveRec(int saveCount, SaveFlags::Flags saveFlags, size_t clipIndex) + : saveCount(saveCount), saveFlags(saveFlags), clipIndex(clipIndex) {} }; const SaveRec* currentSaveRec() const; @@ -224,11 +228,11 @@ private: class Clip; - std::unique_ptr<SkCanvas> mCanvasOwned; // might own a canvas we allocated - SkCanvas* mCanvas; // we do NOT own this canvas, it must survive us - // unless it is the same as mCanvasOwned.get() - std::unique_ptr<SkDeque> mSaveStack; // lazily allocated, tracks partial saves. - std::vector<Clip> mClipStack; // tracks persistent clips. + std::unique_ptr<SkCanvas> mCanvasOwned; // Might own a canvas we allocated. + SkCanvas* mCanvas; // We do NOT own this canvas, it must survive us + // unless it is the same as mCanvasOwned.get(). + std::unique_ptr<std::deque<SaveRec>> mSaveStack; // Lazily allocated, tracks partial saves. + std::vector<Clip> mClipStack; // Tracks persistent clips. sk_sp<PaintFilter> mPaintFilter; }; diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index f57d80c496ad..b1aa19475518 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -37,6 +37,7 @@ extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env); extern int register_android_graphics_Graphics(JNIEnv* env); extern int register_android_graphics_ImageDecoder(JNIEnv*); extern int register_android_graphics_drawable_AnimatedImageDrawable(JNIEnv*); +extern int register_android_graphics_drawable_LottieDrawable(JNIEnv*); extern int register_android_graphics_Interpolator(JNIEnv* env); extern int register_android_graphics_MaskFilter(JNIEnv* env); extern int register_android_graphics_Movie(JNIEnv* env); @@ -117,6 +118,7 @@ extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env); REG_JNI(register_android_graphics_HardwareRendererObserver), REG_JNI(register_android_graphics_ImageDecoder), REG_JNI(register_android_graphics_drawable_AnimatedImageDrawable), + REG_JNI(register_android_graphics_drawable_LottieDrawable), REG_JNI(register_android_graphics_Interpolator), REG_JNI(register_android_graphics_MaskFilter), REG_JNI(register_android_graphics_Matrix), diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h index 2a2019199bda..07e2fe24c939 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -60,6 +60,7 @@ typedef uirenderer::VectorDrawable::Tree VectorDrawableRoot; typedef std::function<void(uint16_t* text, float* positions)> ReadGlyphFunc; class AnimatedImageDrawable; +class LottieDrawable; class Bitmap; class Paint; struct Typeface; @@ -242,6 +243,7 @@ public: const Paint* paint) = 0; virtual double drawAnimatedImage(AnimatedImageDrawable* imgDrawable) = 0; + virtual void drawLottie(LottieDrawable* lottieDrawable) = 0; virtual void drawPicture(const SkPicture& picture) = 0; /** diff --git a/libs/hwui/hwui/LottieDrawable.cpp b/libs/hwui/hwui/LottieDrawable.cpp new file mode 100644 index 000000000000..92dc51e01a85 --- /dev/null +++ b/libs/hwui/hwui/LottieDrawable.cpp @@ -0,0 +1,83 @@ +/* + * 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. + */ + +#include "LottieDrawable.h" + +#include <SkTime.h> +#include <log/log.h> +#include <pipeline/skia/SkiaUtils.h> + +namespace android { + +sk_sp<LottieDrawable> LottieDrawable::Make(sk_sp<skottie::Animation> animation, size_t bytesUsed) { + if (animation) { + return sk_sp<LottieDrawable>(new LottieDrawable(std::move(animation), bytesUsed)); + } + return nullptr; +} +LottieDrawable::LottieDrawable(sk_sp<skottie::Animation> animation, size_t bytesUsed) + : mAnimation(std::move(animation)), mBytesUsed(bytesUsed) {} + +bool LottieDrawable::start() { + if (mRunning) { + return false; + } + + mRunning = true; + return true; +} + +bool LottieDrawable::stop() { + bool wasRunning = mRunning; + mRunning = false; + return wasRunning; +} + +bool LottieDrawable::isRunning() { + return mRunning; +} + +// TODO: Check to see if drawable is actually dirty +bool LottieDrawable::isDirty() { + return true; +} + +void LottieDrawable::onDraw(SkCanvas* canvas) { + if (mRunning) { + const nsecs_t currentTime = systemTime(SYSTEM_TIME_MONOTONIC); + + nsecs_t t = 0; + if (mStartTime == 0) { + mStartTime = currentTime; + } else { + t = currentTime - mStartTime; + } + double seekTime = std::fmod((double)t * 1e-9, mAnimation->duration()); + mAnimation->seekFrameTime(seekTime); + mAnimation->render(canvas); + } +} + +void LottieDrawable::drawStaging(SkCanvas* canvas) { + onDraw(canvas); +} + +SkRect LottieDrawable::onGetBounds() { + // We do not actually know the bounds, so give a conservative answer. + return SkRectMakeLargest(); +} + +} // namespace android diff --git a/libs/hwui/hwui/LottieDrawable.h b/libs/hwui/hwui/LottieDrawable.h new file mode 100644 index 000000000000..9cc34bf12f4f --- /dev/null +++ b/libs/hwui/hwui/LottieDrawable.h @@ -0,0 +1,67 @@ +/* + * 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. + */ + +#pragma once + +#include <SkDrawable.h> +#include <Skottie.h> +#include <utils/Timers.h> + +class SkCanvas; + +namespace android { + +/** + * Native component of android.graphics.drawable.LottieDrawable.java. + * This class can be drawn into Canvas.h and maintains the state needed to drive + * the animation from the RenderThread. + */ +class LottieDrawable : public SkDrawable { +public: + static sk_sp<LottieDrawable> Make(sk_sp<skottie::Animation> animation, size_t bytes); + + // Draw to software canvas + void drawStaging(SkCanvas* canvas); + + // Returns true if the animation was started; false otherwise (e.g. it was + // already running) + bool start(); + // Returns true if the animation was stopped; false otherwise (e.g. it was + // already stopped) + bool stop(); + bool isRunning(); + + // TODO: Is dirty should take in a time til next frame to determine if it is dirty + bool isDirty(); + + SkRect onGetBounds() override; + + size_t byteSize() const { return sizeof(*this) + mBytesUsed; } + +protected: + void onDraw(SkCanvas* canvas) override; + +private: + LottieDrawable(sk_sp<skottie::Animation> animation, size_t bytes_used); + + sk_sp<skottie::Animation> mAnimation; + bool mRunning = false; + // The start time for the drawable itself. + nsecs_t mStartTime = 0; + const size_t mBytesUsed = 0; +}; + +} // namespace android diff --git a/libs/hwui/jni/LottieDrawable.cpp b/libs/hwui/jni/LottieDrawable.cpp new file mode 100644 index 000000000000..fb6eede213a8 --- /dev/null +++ b/libs/hwui/jni/LottieDrawable.cpp @@ -0,0 +1,91 @@ +/* + * 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. + */ + +#include <SkRect.h> +#include <Skottie.h> +#include <hwui/Canvas.h> +#include <hwui/LottieDrawable.h> + +#include "GraphicsJNI.h" +#include "Utils.h" + +using namespace android; + +static jclass gLottieDrawableClass; + +static jlong LottieDrawable_nCreate(JNIEnv* env, jobject, jstring jjson) { + const ScopedUtfChars cstr(env, jjson); + // TODO(b/259267150) provide more accurate byteSize + size_t bytes = strlen(cstr.c_str()); + auto animation = skottie::Animation::Builder().make(cstr.c_str(), bytes); + sk_sp<LottieDrawable> drawable(LottieDrawable::Make(std::move(animation), bytes)); + if (!drawable) { + return 0; + } + return reinterpret_cast<jlong>(drawable.release()); +} + +static void LottieDrawable_destruct(LottieDrawable* drawable) { + SkSafeUnref(drawable); +} + +static jlong LottieDrawable_nGetNativeFinalizer(JNIEnv* /*env*/, jobject /*clazz*/) { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&LottieDrawable_destruct)); +} + +static void LottieDrawable_nDraw(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, jlong canvasPtr) { + auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr); + auto* canvas = reinterpret_cast<Canvas*>(canvasPtr); + canvas->drawLottie(drawable); +} + +static jboolean LottieDrawable_nIsRunning(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) { + auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr); + return drawable->isRunning(); +} + +static jboolean LottieDrawable_nStart(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) { + auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr); + return drawable->start(); +} + +static jboolean LottieDrawable_nStop(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) { + auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr); + return drawable->stop(); +} + +static jlong LottieDrawable_nNativeByteSize(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) { + auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr); + return drawable->byteSize(); +} + +static const JNINativeMethod gLottieDrawableMethods[] = { + {"nCreate", "(Ljava/lang/String;)J", (void*)LottieDrawable_nCreate}, + {"nNativeByteSize", "(J)J", (void*)LottieDrawable_nNativeByteSize}, + {"nGetNativeFinalizer", "()J", (void*)LottieDrawable_nGetNativeFinalizer}, + {"nDraw", "(JJ)V", (void*)LottieDrawable_nDraw}, + {"nIsRunning", "(J)Z", (void*)LottieDrawable_nIsRunning}, + {"nStart", "(J)Z", (void*)LottieDrawable_nStart}, + {"nStop", "(J)Z", (void*)LottieDrawable_nStop}, +}; + +int register_android_graphics_drawable_LottieDrawable(JNIEnv* env) { + gLottieDrawableClass = reinterpret_cast<jclass>( + env->NewGlobalRef(FindClassOrDie(env, "android/graphics/drawable/LottieDrawable"))); + + return android::RegisterMethodsOrDie(env, "android/graphics/drawable/LottieDrawable", + gLottieDrawableMethods, NELEM(gLottieDrawableMethods)); +} diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index 47e2edb2ed0f..3f4d004ae8fa 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -829,12 +829,10 @@ static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass, DeviceInfo::setDensity(density); } -static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv* env, jclass, jint physicalWidth, - jint physicalHeight, jfloat refreshRate, - jint wideColorDataspace, - jlong appVsyncOffsetNanos, - jlong presentationDeadlineNanos, - jboolean supportFp16ForHdr) { +static void android_view_ThreadedRenderer_initDisplayInfo( + JNIEnv* env, jclass, jint physicalWidth, jint physicalHeight, jfloat refreshRate, + jint wideColorDataspace, jlong appVsyncOffsetNanos, jlong presentationDeadlineNanos, + jboolean supportFp16ForHdr, jboolean supportMixedColorSpaces) { DeviceInfo::setWidth(physicalWidth); DeviceInfo::setHeight(physicalHeight); DeviceInfo::setRefreshRate(refreshRate); @@ -842,6 +840,7 @@ static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv* env, jclass, j DeviceInfo::setAppVsyncOffsetNanos(appVsyncOffsetNanos); DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos); DeviceInfo::setSupportFp16ForHdr(supportFp16ForHdr); + DeviceInfo::setSupportMixedColorSpaces(supportMixedColorSpaces); } static void android_view_ThreadedRenderer_setDrawingEnabled(JNIEnv*, jclass, jboolean enabled) { @@ -991,7 +990,7 @@ static const JNINativeMethod gMethods[] = { {"nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark}, {"nSetDisplayDensityDpi", "(I)V", (void*)android_view_ThreadedRenderer_setDisplayDensityDpi}, - {"nInitDisplayInfo", "(IIFIJJZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, + {"nInitDisplayInfo", "(IIFIJJZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, {"preload", "()V", (void*)android_view_ThreadedRenderer_preload}, {"isWebViewOverlaysEnabled", "()Z", (void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled}, diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index dc72aead4873..a4960ea17c79 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -24,6 +24,7 @@ #include "SkClipStack.h" #include "SkRect.h" #include "SkM44.h" +#include "include/gpu/GpuTypes.h" // from Skia #include "utils/GLUtils.h" namespace android { @@ -92,7 +93,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { SkImageInfo surfaceInfo = canvas->imageInfo().makeWH(clipBounds.width(), clipBounds.height()); tmpSurface = - SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes, surfaceInfo); + SkSurface::MakeRenderTarget(directContext, skgpu::Budgeted::kYes, surfaceInfo); tmpSurface->getCanvas()->clear(SK_ColorTRANSPARENT); GrGLFramebufferInfo fboInfo; diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp index fcfc4f82abed..f0dc5eb4dd0e 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp @@ -146,6 +146,16 @@ bool SkiaDisplayList::prepareListAndChildren( } } + for (auto& lottie : mLotties) { + // If any animated image in the display list needs updated, then damage the node. + if (lottie->isDirty()) { + isDirty = true; + } + if (lottie->isRunning()) { + info.out.hasAnimations = true; + } + } + for (auto& [vectorDrawable, cachedMatrix] : mVectorDrawables) { // If any vector drawable in the display list needs update, damage the node. if (vectorDrawable->isDirty()) { diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h index 2a677344b7b2..39217fcf1a56 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.h +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h @@ -22,6 +22,7 @@ #include "RenderNodeDrawable.h" #include "TreeInfo.h" #include "hwui/AnimatedImageDrawable.h" +#include "hwui/LottieDrawable.h" #include "utils/LinearAllocator.h" #include "utils/Pair.h" @@ -186,6 +187,8 @@ public: return mHasHolePunches; } + // TODO(b/257304231): create common base class for Lotties and AnimatedImages + std::vector<LottieDrawable*> mLotties; std::vector<AnimatedImageDrawable*> mAnimatedImages; DisplayListData mDisplayList; diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 1a336c5855d9..3692f0940b28 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -36,6 +36,7 @@ #include <SkStream.h> #include <SkString.h> #include <SkTypeface.h> +#include "include/gpu/GpuTypes.h" // from Skia #include <android-base/properties.h> #include <unistd.h> @@ -187,7 +188,7 @@ bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator SkSurfaceProps props(0, kUnknown_SkPixelGeometry); SkASSERT(mRenderThread.getGrContext() != nullptr); node->setLayerSurface(SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), - SkBudgeted::kYes, info, 0, + skgpu::Budgeted::kYes, info, 0, this->getSurfaceOrigin(), &props)); if (node->getLayerSurface()) { // update the transform in window of the layer to reset its origin wrt light source diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp index 1f87865f2672..db449d608c1f 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -188,6 +188,11 @@ void SkiaRecordingCanvas::drawWebViewFunctor(int functor) { #endif } +void SkiaRecordingCanvas::drawLottie(LottieDrawable* lottie) { + drawDrawable(lottie); + mDisplayList->mLotties.push_back(lottie); +} + void SkiaRecordingCanvas::drawVectorDrawable(VectorDrawableRoot* tree) { mRecorder.drawVectorDrawable(tree); SkMatrix mat; diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h index 7844e2cc2a73..c823d8d0a755 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h @@ -78,6 +78,7 @@ public: uirenderer::CanvasPropertyPaint* paint) override; virtual void drawRipple(const RippleDrawableParams& params) override; + virtual void drawLottie(LottieDrawable* lottieDrawable) override; virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override; virtual void enableZ(bool enableZ) override; diff --git a/libs/hwui/pipeline/skia/StretchMask.cpp b/libs/hwui/pipeline/skia/StretchMask.cpp index b169c9200e88..cad3703d8d2b 100644 --- a/libs/hwui/pipeline/skia/StretchMask.cpp +++ b/libs/hwui/pipeline/skia/StretchMask.cpp @@ -18,6 +18,8 @@ #include "SkBlendMode.h" #include "SkCanvas.h" #include "SkSurface.h" +#include "include/gpu/GpuTypes.h" // from Skia + #include "TransformCanvas.h" #include "SkiaDisplayList.h" @@ -36,7 +38,7 @@ void StretchMask::draw(GrRecordingContext* context, // not match. mMaskSurface = SkSurface::MakeRenderTarget( context, - SkBudgeted::kYes, + skgpu::Budgeted::kYes, SkImageInfo::Make( width, height, diff --git a/libs/hwui/tests/unit/CacheManagerTests.cpp b/libs/hwui/tests/unit/CacheManagerTests.cpp index 508e1986b4e4..2b90bda87ecd 100644 --- a/libs/hwui/tests/unit/CacheManagerTests.cpp +++ b/libs/hwui/tests/unit/CacheManagerTests.cpp @@ -21,6 +21,7 @@ #include "tests/common/TestUtils.h" #include <SkImagePriv.h> +#include "include/gpu/GpuTypes.h" // from Skia using namespace android; using namespace android::uirenderer; @@ -45,7 +46,8 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, DISABLED_trimMemory) { while (getCacheUsage(grContext) <= renderThread.cacheManager().getBackgroundCacheSize()) { SkImageInfo info = SkImageInfo::MakeA8(width, height); - sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(grContext, SkBudgeted::kYes, info); + sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(grContext, skgpu::Budgeted::kYes, + info); surface->getCanvas()->drawColor(SK_AlphaTRANSPARENT); grContext->flushAndSubmit(); diff --git a/libs/hwui/utils/AutoMalloc.h b/libs/hwui/utils/AutoMalloc.h new file mode 100644 index 000000000000..05f5e9f24133 --- /dev/null +++ b/libs/hwui/utils/AutoMalloc.h @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <cstdlib> +#include <memory> +#include <type_traits> + +namespace android { +namespace uirenderer { + +/** Manages an array of T elements, freeing the array in the destructor. + * Does NOT call any constructors/destructors on T (T must be POD). + */ +template <typename T, + typename = std::enable_if_t<std::is_trivially_default_constructible<T>::value && + std::is_trivially_destructible<T>::value>> +class AutoTMalloc { +public: + /** Takes ownership of the ptr. The ptr must be a value which can be passed to std::free. */ + explicit AutoTMalloc(T* ptr = nullptr) : fPtr(ptr) {} + + /** Allocates space for 'count' Ts. */ + explicit AutoTMalloc(size_t count) : fPtr(mallocIfCountThrowOnFail(count)) {} + + AutoTMalloc(AutoTMalloc&&) = default; + AutoTMalloc& operator=(AutoTMalloc&&) = default; + + /** Resize the memory area pointed to by the current ptr preserving contents. */ + void realloc(size_t count) { fPtr.reset(reallocIfCountThrowOnFail(count)); } + + /** Resize the memory area pointed to by the current ptr without preserving contents. */ + T* reset(size_t count = 0) { + fPtr.reset(mallocIfCountThrowOnFail(count)); + return this->get(); + } + + T* get() const { return fPtr.get(); } + + operator T*() { return fPtr.get(); } + + operator const T*() const { return fPtr.get(); } + + T& operator[](int index) { return fPtr.get()[index]; } + + const T& operator[](int index) const { return fPtr.get()[index]; } + + /** + * Transfer ownership of the ptr to the caller, setting the internal + * pointer to NULL. Note that this differs from get(), which also returns + * the pointer, but it does not transfer ownership. + */ + T* release() { return fPtr.release(); } + +private: + struct FreeDeleter { + void operator()(uint8_t* p) { std::free(p); } + }; + std::unique_ptr<T, FreeDeleter> fPtr; + + T* mallocIfCountThrowOnFail(size_t count) { + T* newPtr = nullptr; + if (count) { + newPtr = (T*)std::malloc(count * sizeof(T)); + LOG_ALWAYS_FATAL_IF(!newPtr, "failed to malloc %zu bytes", count * sizeof(T)); + } + return newPtr; + } + T* reallocIfCountThrowOnFail(size_t count) { + T* newPtr = nullptr; + if (count) { + newPtr = (T*)std::realloc(fPtr.release(), count * sizeof(T)); + LOG_ALWAYS_FATAL_IF(!newPtr, "failed to realloc %zu bytes", count * sizeof(T)); + } + return newPtr; + } +}; + +} // namespace uirenderer +} // namespace android diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 4475aeddf944..24c5b4172732 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -7022,6 +7022,10 @@ public class AudioManager { * Returns an array of {@link AudioDeviceInfo} objects corresponding to the audio devices * currently connected to the system and meeting the criteria specified in the * <code>flags</code> parameter. + * Notes that Android audio framework only support one device per device type. In that case, + * if there are multiple audio device with the same device type connected to the Android device, + * only the last reported device will be known by Android audio framework and returned by this + * API. * @param flags A set of bitflags specifying the criteria to test. * @see #GET_DEVICES_OUTPUTS * @see #GET_DEVICES_INPUTS diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index 582a28ee278e..015602e95533 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -21,11 +21,12 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.content.Context; +import android.hardware.cas.AidlCasPluginDescriptor; +import android.hardware.cas.ICas; +import android.hardware.cas.ICasListener; +import android.hardware.cas.IMediaCasService; +import android.hardware.cas.Status; import android.hardware.cas.V1_0.HidlCasPluginDescriptor; -import android.hardware.cas.V1_0.ICas; -import android.hardware.cas.V1_0.IMediaCasService; -import android.hardware.cas.V1_2.ICasListener; -import android.hardware.cas.V1_2.Status; import android.media.MediaCasException.*; import android.media.tv.TvInputService.PriorityHintUseCaseType; import android.media.tv.tunerresourcemanager.CasSessionRequest; @@ -39,6 +40,7 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; +import android.os.ServiceManager; import android.util.Log; import android.util.Singleton; @@ -47,6 +49,7 @@ import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -114,9 +117,10 @@ import java.util.Objects; */ public final class MediaCas implements AutoCloseable { private static final String TAG = "MediaCas"; - private ICas mICas; - private android.hardware.cas.V1_1.ICas mICasV11; - private android.hardware.cas.V1_2.ICas mICasV12; + private ICas mICas = null; + private android.hardware.cas.V1_0.ICas mICasHidl = null; + private android.hardware.cas.V1_1.ICas mICasHidl11 = null; + private android.hardware.cas.V1_2.ICas mICasHidl12 = null; private EventListener mListener; private HandlerThread mHandlerThread; private EventHandler mEventHandler; @@ -133,88 +137,84 @@ public final class MediaCas implements AutoCloseable { * * @hide */ - @IntDef(prefix = "SCRAMBLING_MODE_", - value = {SCRAMBLING_MODE_RESERVED, SCRAMBLING_MODE_DVB_CSA1, SCRAMBLING_MODE_DVB_CSA2, - SCRAMBLING_MODE_DVB_CSA3_STANDARD, - SCRAMBLING_MODE_DVB_CSA3_MINIMAL, SCRAMBLING_MODE_DVB_CSA3_ENHANCE, - SCRAMBLING_MODE_DVB_CISSA_V1, SCRAMBLING_MODE_DVB_IDSA, - SCRAMBLING_MODE_MULTI2, SCRAMBLING_MODE_AES128, SCRAMBLING_MODE_AES_ECB, - SCRAMBLING_MODE_AES_SCTE52, SCRAMBLING_MODE_TDES_ECB, SCRAMBLING_MODE_TDES_SCTE52}) + @IntDef( + prefix = "SCRAMBLING_MODE_", + value = { + SCRAMBLING_MODE_RESERVED, + SCRAMBLING_MODE_DVB_CSA1, + SCRAMBLING_MODE_DVB_CSA2, + SCRAMBLING_MODE_DVB_CSA3_STANDARD, + SCRAMBLING_MODE_DVB_CSA3_MINIMAL, + SCRAMBLING_MODE_DVB_CSA3_ENHANCE, + SCRAMBLING_MODE_DVB_CISSA_V1, + SCRAMBLING_MODE_DVB_IDSA, + SCRAMBLING_MODE_MULTI2, + SCRAMBLING_MODE_AES128, + SCRAMBLING_MODE_AES_CBC, + SCRAMBLING_MODE_AES_ECB, + SCRAMBLING_MODE_AES_SCTE52, + SCRAMBLING_MODE_TDES_ECB, + SCRAMBLING_MODE_TDES_SCTE52 + }) @Retention(RetentionPolicy.SOURCE) public @interface ScramblingMode {} - /** - * DVB (Digital Video Broadcasting) reserved mode. - */ - public static final int SCRAMBLING_MODE_RESERVED = - android.hardware.cas.V1_2.ScramblingMode.RESERVED; - /** - * DVB (Digital Video Broadcasting) Common Scrambling Algorithm (CSA) 1. - */ - public static final int SCRAMBLING_MODE_DVB_CSA1 = - android.hardware.cas.V1_2.ScramblingMode.DVB_CSA1; - /** - * DVB CSA 2. - */ - public static final int SCRAMBLING_MODE_DVB_CSA2 = - android.hardware.cas.V1_2.ScramblingMode.DVB_CSA2; - /** - * DVB CSA 3 in standard mode. - */ + /** DVB (Digital Video Broadcasting) reserved mode. */ + public static final int SCRAMBLING_MODE_RESERVED = android.hardware.cas.ScramblingMode.RESERVED; + + /** DVB (Digital Video Broadcasting) Common Scrambling Algorithm (CSA) 1. */ + public static final int SCRAMBLING_MODE_DVB_CSA1 = android.hardware.cas.ScramblingMode.DVB_CSA1; + + /** DVB CSA 2. */ + public static final int SCRAMBLING_MODE_DVB_CSA2 = android.hardware.cas.ScramblingMode.DVB_CSA2; + + /** DVB CSA 3 in standard mode. */ public static final int SCRAMBLING_MODE_DVB_CSA3_STANDARD = - android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_STANDARD; - /** - * DVB CSA 3 in minimally enhanced mode. - */ + android.hardware.cas.ScramblingMode.DVB_CSA3_STANDARD; + + /** DVB CSA 3 in minimally enhanced mode. */ public static final int SCRAMBLING_MODE_DVB_CSA3_MINIMAL = - android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_MINIMAL; - /** - * DVB CSA 3 in fully enhanced mode. - */ + android.hardware.cas.ScramblingMode.DVB_CSA3_MINIMAL; + + /** DVB CSA 3 in fully enhanced mode. */ public static final int SCRAMBLING_MODE_DVB_CSA3_ENHANCE = - android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_ENHANCE; - /** - * DVB Common IPTV Software-oriented Scrambling Algorithm (CISSA) Version 1. - */ + android.hardware.cas.ScramblingMode.DVB_CSA3_ENHANCE; + + /** DVB Common IPTV Software-oriented Scrambling Algorithm (CISSA) Version 1. */ public static final int SCRAMBLING_MODE_DVB_CISSA_V1 = - android.hardware.cas.V1_2.ScramblingMode.DVB_CISSA_V1; - /** - * ATIS-0800006 IIF Default Scrambling Algorithm (IDSA). - */ - public static final int SCRAMBLING_MODE_DVB_IDSA = - android.hardware.cas.V1_2.ScramblingMode.DVB_IDSA; - /** - * A symmetric key algorithm. - */ - public static final int SCRAMBLING_MODE_MULTI2 = - android.hardware.cas.V1_2.ScramblingMode.MULTI2; - /** - * Advanced Encryption System (AES) 128-bit Encryption mode. - */ - public static final int SCRAMBLING_MODE_AES128 = - android.hardware.cas.V1_2.ScramblingMode.AES128; - /** - * Advanced Encryption System (AES) Electronic Code Book (ECB) mode. - */ - public static final int SCRAMBLING_MODE_AES_ECB = - android.hardware.cas.V1_2.ScramblingMode.AES_ECB; + android.hardware.cas.ScramblingMode.DVB_CISSA_V1; + + /** ATIS-0800006 IIF Default Scrambling Algorithm (IDSA). */ + public static final int SCRAMBLING_MODE_DVB_IDSA = android.hardware.cas.ScramblingMode.DVB_IDSA; + + /** A symmetric key algorithm. */ + public static final int SCRAMBLING_MODE_MULTI2 = android.hardware.cas.ScramblingMode.MULTI2; + + /** Advanced Encryption System (AES) 128-bit Encryption mode. */ + public static final int SCRAMBLING_MODE_AES128 = android.hardware.cas.ScramblingMode.AES128; + + /** Advanced Encryption System (AES) Cipher Block Chaining (CBC) mode. */ + public static final int SCRAMBLING_MODE_AES_CBC = android.hardware.cas.ScramblingMode.AES_CBC; + + /** Advanced Encryption System (AES) Electronic Code Book (ECB) mode. */ + public static final int SCRAMBLING_MODE_AES_ECB = android.hardware.cas.ScramblingMode.AES_ECB; + /** * Advanced Encryption System (AES) Society of Cable Telecommunications Engineers (SCTE) 52 * mode. */ public static final int SCRAMBLING_MODE_AES_SCTE52 = - android.hardware.cas.V1_2.ScramblingMode.AES_SCTE52; - /** - * Triple Data Encryption Algorithm (TDES) Electronic Code Book (ECB) mode. - */ - public static final int SCRAMBLING_MODE_TDES_ECB = - android.hardware.cas.V1_2.ScramblingMode.TDES_ECB; + android.hardware.cas.ScramblingMode.AES_SCTE52; + + /** Triple Data Encryption Algorithm (TDES) Electronic Code Book (ECB) mode. */ + public static final int SCRAMBLING_MODE_TDES_ECB = android.hardware.cas.ScramblingMode.TDES_ECB; + /** * Triple Data Encryption Algorithm (TDES) Society of Cable Telecommunications Engineers (SCTE) * 52 mode. */ public static final int SCRAMBLING_MODE_TDES_SCTE52 = - android.hardware.cas.V1_2.ScramblingMode.TDES_SCTE52; + android.hardware.cas.ScramblingMode.TDES_SCTE52; /** * Usages used to open cas sessions. @@ -226,25 +226,21 @@ public final class MediaCas implements AutoCloseable { SESSION_USAGE_TIMESHIFT}) @Retention(RetentionPolicy.SOURCE) public @interface SessionUsage {} - /** - * Cas session is used to descramble live streams. - */ - public static final int SESSION_USAGE_LIVE = android.hardware.cas.V1_2.SessionIntent.LIVE; - /** - * Cas session is used to descramble recoreded streams. - */ - public static final int SESSION_USAGE_PLAYBACK = - android.hardware.cas.V1_2.SessionIntent.PLAYBACK; - /** - * Cas session is used to descramble live streams and encrypt local recorded content - */ - public static final int SESSION_USAGE_RECORD = android.hardware.cas.V1_2.SessionIntent.RECORD; + + /** Cas session is used to descramble live streams. */ + public static final int SESSION_USAGE_LIVE = android.hardware.cas.SessionIntent.LIVE; + + /** Cas session is used to descramble recoreded streams. */ + public static final int SESSION_USAGE_PLAYBACK = android.hardware.cas.SessionIntent.PLAYBACK; + + /** Cas session is used to descramble live streams and encrypt local recorded content */ + public static final int SESSION_USAGE_RECORD = android.hardware.cas.SessionIntent.RECORD; + /** * Cas session is used to descramble live streams , encrypt local recorded content and playback * local encrypted content. */ - public static final int SESSION_USAGE_TIMESHIFT = - android.hardware.cas.V1_2.SessionIntent.TIMESHIFT; + public static final int SESSION_USAGE_TIMESHIFT = android.hardware.cas.SessionIntent.TIMESHIFT; /** * Plugin status events sent from cas system. @@ -261,63 +257,90 @@ public final class MediaCas implements AutoCloseable { * physical CAS modules. */ public static final int PLUGIN_STATUS_PHYSICAL_MODULE_CHANGED = - android.hardware.cas.V1_2.StatusEvent.PLUGIN_PHYSICAL_MODULE_CHANGED; - /** - * The event to indicate that the number of CAS system's session is changed. - */ - public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = - android.hardware.cas.V1_2.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED; + android.hardware.cas.StatusEvent.PLUGIN_PHYSICAL_MODULE_CHANGED; - private static final Singleton<IMediaCasService> sService = new Singleton<IMediaCasService>() { - @Override - protected IMediaCasService create() { - try { - Log.d(TAG, "Trying to get cas@1.2 service"); - android.hardware.cas.V1_2.IMediaCasService serviceV12 = - android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/); - if (serviceV12 != null) { - return serviceV12; + /** The event to indicate that the number of CAS system's session is changed. */ + public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = + android.hardware.cas.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED; + + private static final Singleton<IMediaCasService> sService = + new Singleton<IMediaCasService>() { + @Override + protected IMediaCasService create() { + try { + Log.d(TAG, "Trying to get AIDL service"); + IMediaCasService serviceAidl = + IMediaCasService.Stub.asInterface( + ServiceManager.getService( + IMediaCasService.DESCRIPTOR + "/default")); + if (serviceAidl != null) { + return serviceAidl; + } + } catch (Exception eAidl) { + Log.d(TAG, "Failed to get cas AIDL service"); + } + return null; } - } catch (Exception eV1_2) { - Log.d(TAG, "Failed to get cas@1.2 service"); - } + }; + + private static final Singleton<android.hardware.cas.V1_0.IMediaCasService> sServiceHidl = + new Singleton<android.hardware.cas.V1_0.IMediaCasService>() { + @Override + protected android.hardware.cas.V1_0.IMediaCasService create() { + try { + Log.d(TAG, "Trying to get cas@1.2 service"); + android.hardware.cas.V1_2.IMediaCasService serviceV12 = + android.hardware.cas.V1_2.IMediaCasService.getService( + true /*wait*/); + if (serviceV12 != null) { + return serviceV12; + } + } catch (Exception eV1_2) { + Log.d(TAG, "Failed to get cas@1.2 service"); + } - try { - Log.d(TAG, "Trying to get cas@1.1 service"); - android.hardware.cas.V1_1.IMediaCasService serviceV11 = - android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/); - if (serviceV11 != null) { - return serviceV11; + try { + Log.d(TAG, "Trying to get cas@1.1 service"); + android.hardware.cas.V1_1.IMediaCasService serviceV11 = + android.hardware.cas.V1_1.IMediaCasService.getService( + true /*wait*/); + if (serviceV11 != null) { + return serviceV11; + } + } catch (Exception eV1_1) { + Log.d(TAG, "Failed to get cas@1.1 service"); } - } catch (Exception eV1_1) { - Log.d(TAG, "Failed to get cas@1.1 service"); - } - try { - Log.d(TAG, "Trying to get cas@1.0 service"); - return IMediaCasService.getService(true /*wait*/); - } catch (Exception eV1_0) { - Log.d(TAG, "Failed to get cas@1.0 service"); - } + try { + Log.d(TAG, "Trying to get cas@1.0 service"); + return android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/); + } catch (Exception eV1_0) { + Log.d(TAG, "Failed to get cas@1.0 service"); + } - return null; - } - }; + return null; + } + }; static IMediaCasService getService() { return sService.get(); } + static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() { + return sServiceHidl.get(); + } + private void validateInternalStates() { - if (mICas == null) { + if (mICas == null && mICasHidl == null) { throw new IllegalStateException(); } } private void cleanupAndRethrowIllegalState() { mICas = null; - mICasV11 = null; - mICasV12 = null; + mICasHidl = null; + mICasHidl11 = null; + mICasHidl12 = null; throw new IllegalStateException(); } @@ -341,7 +364,7 @@ public final class MediaCas implements AutoCloseable { toBytes((ArrayList<Byte>) msg.obj)); } else if (msg.what == MSG_CAS_SESSION_EVENT) { Bundle bundle = msg.getData(); - ArrayList<Byte> sessionId = toByteArray(bundle.getByteArray(SESSION_KEY)); + byte[] sessionId = bundle.getByteArray(SESSION_KEY); mListener.onSessionEvent(MediaCas.this, createFromSessionId(sessionId), msg.arg1, msg.arg2, bundle.getByteArray(DATA_KEY)); @@ -357,40 +380,94 @@ public final class MediaCas implements AutoCloseable { } } - private final ICasListener.Stub mBinder = new ICasListener.Stub() { - @Override - public void onEvent(int event, int arg, @Nullable ArrayList<Byte> data) - throws RemoteException { - if (mEventHandler != null) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - EventHandler.MSG_CAS_EVENT, event, arg, data)); - } - } - @Override - public void onSessionEvent(@NonNull ArrayList<Byte> sessionId, - int event, int arg, @Nullable ArrayList<Byte> data) - throws RemoteException { - if (mEventHandler != null) { - Message msg = mEventHandler.obtainMessage(); - msg.what = EventHandler.MSG_CAS_SESSION_EVENT; - msg.arg1 = event; - msg.arg2 = arg; - Bundle bundle = new Bundle(); - bundle.putByteArray(EventHandler.SESSION_KEY, toBytes(sessionId)); - bundle.putByteArray(EventHandler.DATA_KEY, toBytes(data)); - msg.setData(bundle); - mEventHandler.sendMessage(msg); - } - } - @Override - public void onStatusUpdate(byte status, int arg) - throws RemoteException { - if (mEventHandler != null) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - EventHandler.MSG_CAS_STATUS_EVENT, status, arg)); - } - } - }; + private final ICasListener.Stub mBinder = + new ICasListener.Stub() { + @Override + public void onEvent(int event, int arg, byte[] data) throws RemoteException { + if (mEventHandler != null) { + mEventHandler.sendMessage( + mEventHandler.obtainMessage( + EventHandler.MSG_CAS_EVENT, event, arg, data)); + } + } + + @Override + public void onSessionEvent(byte[] sessionId, int event, int arg, byte[] data) + throws RemoteException { + if (mEventHandler != null) { + Message msg = mEventHandler.obtainMessage(); + msg.what = EventHandler.MSG_CAS_SESSION_EVENT; + msg.arg1 = event; + msg.arg2 = arg; + Bundle bundle = new Bundle(); + bundle.putByteArray(EventHandler.SESSION_KEY, sessionId); + bundle.putByteArray(EventHandler.DATA_KEY, data); + msg.setData(bundle); + mEventHandler.sendMessage(msg); + } + } + + @Override + public void onStatusUpdate(byte status, int arg) throws RemoteException { + if (mEventHandler != null) { + mEventHandler.sendMessage( + mEventHandler.obtainMessage( + EventHandler.MSG_CAS_STATUS_EVENT, status, arg)); + } + } + + @Override + public synchronized String getInterfaceHash() throws android.os.RemoteException { + return ICasListener.Stub.HASH; + } + + @Override + public int getInterfaceVersion() throws android.os.RemoteException { + return ICasListener.Stub.VERSION; + } + }; + + private final android.hardware.cas.V1_2.ICasListener.Stub mBinderHidl = + new android.hardware.cas.V1_2.ICasListener.Stub() { + @Override + public void onEvent(int event, int arg, @Nullable ArrayList<Byte> data) + throws RemoteException { + if (mEventHandler != null) { + mEventHandler.sendMessage( + mEventHandler.obtainMessage( + EventHandler.MSG_CAS_EVENT, event, arg, data)); + } + } + + @Override + public void onSessionEvent( + @NonNull ArrayList<Byte> sessionId, + int event, + int arg, + @Nullable ArrayList<Byte> data) + throws RemoteException { + if (mEventHandler != null) { + Message msg = mEventHandler.obtainMessage(); + msg.what = EventHandler.MSG_CAS_SESSION_EVENT; + msg.arg1 = event; + msg.arg2 = arg; + Bundle bundle = new Bundle(); + bundle.putByteArray(EventHandler.SESSION_KEY, toBytes(sessionId)); + bundle.putByteArray(EventHandler.DATA_KEY, toBytes(data)); + msg.setData(bundle); + mEventHandler.sendMessage(msg); + } + } + + @Override + public void onStatusUpdate(byte status, int arg) throws RemoteException { + if (mEventHandler != null) { + mEventHandler.sendMessage( + mEventHandler.obtainMessage( + EventHandler.MSG_CAS_STATUS_EVENT, status, arg)); + } + } + }; private final TunerResourceManager.ResourcesReclaimListener mResourceListener = new TunerResourceManager.ResourcesReclaimListener() { @@ -422,6 +499,11 @@ public final class MediaCas implements AutoCloseable { mName = null; } + PluginDescriptor(@NonNull AidlCasPluginDescriptor descriptor) { + mCASystemId = descriptor.caSystemId; + mName = descriptor.name; + } + PluginDescriptor(@NonNull HidlCasPluginDescriptor descriptor) { mCASystemId = descriptor.caSystemId; mName = descriptor.name; @@ -467,19 +549,20 @@ public final class MediaCas implements AutoCloseable { } return data; } + /** * Class for an open session with the CA system. */ public final class Session implements AutoCloseable { - final ArrayList<Byte> mSessionId; + final byte[] mSessionId; boolean mIsClosed = false; - Session(@NonNull ArrayList<Byte> sessionId) { - mSessionId = new ArrayList<Byte>(sessionId); + Session(@NonNull byte[] sessionId) { + mSessionId = sessionId; } private void validateSessionInternalStates() { - if (mICas == null) { + if (mICas == null && mICasHidl == null) { throw new IllegalStateException(); } if (mIsClosed) { @@ -496,7 +579,7 @@ public final class MediaCas implements AutoCloseable { */ public boolean equals(Object obj) { if (obj instanceof Session) { - return mSessionId.equals(((Session) obj).mSessionId); + return Arrays.equals(mSessionId, ((Session) obj).mSessionId); } return false; } @@ -515,8 +598,13 @@ public final class MediaCas implements AutoCloseable { validateSessionInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.setSessionPrivateData(mSessionId, toByteArray(data, 0, data.length))); + if (mICas != null) { + mICas.setSessionPrivateData(mSessionId, data); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.setSessionPrivateData( + toByteArray(mSessionId), toByteArray(data, 0, data.length))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -539,8 +627,13 @@ public final class MediaCas implements AutoCloseable { validateSessionInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.processEcm(mSessionId, toByteArray(data, offset, length))); + if (mICas != null) { + mICas.processEcm(mSessionId, data); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.processEcm( + toByteArray(mSessionId), toByteArray(data, offset, length))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -576,15 +669,23 @@ public final class MediaCas implements AutoCloseable { public void sendSessionEvent(int event, int arg, @Nullable byte[] data) throws MediaCasException { validateSessionInternalStates(); + if (mICas != null) { + try { + mICas.sendSessionEvent(mSessionId, event, arg, data); + } catch (RemoteException e) { + cleanupAndRethrowIllegalState(); + } + } - if (mICasV11 == null) { + if (mICasHidl11 == null) { Log.d(TAG, "Send Session Event isn't supported by cas@1.0 interface"); throw new UnsupportedCasException("Send Session Event is not supported"); } try { MediaCasException.throwExceptionIfNeeded( - mICasV11.sendSessionEvent(mSessionId, event, arg, toByteArray(data))); + mICasHidl11.sendSessionEvent( + toByteArray(mSessionId), event, arg, toByteArray(data))); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -600,7 +701,7 @@ public final class MediaCas implements AutoCloseable { @NonNull public byte[] getSessionId() { validateSessionInternalStates(); - return toBytes(mSessionId); + return mSessionId; } /** @@ -613,8 +714,12 @@ public final class MediaCas implements AutoCloseable { public void close() { validateSessionInternalStates(); try { - MediaCasStateException.throwExceptionIfNeeded( - mICas.closeSession(mSessionId)); + if (mICas != null) { + mICas.closeSession(mSessionId); + } else { + MediaCasStateException.throwExceptionIfNeeded( + mICasHidl.closeSession(toByteArray(mSessionId))); + } mIsClosed = true; removeSessionFromResourceMap(this); } catch (RemoteException e) { @@ -623,8 +728,8 @@ public final class MediaCas implements AutoCloseable { } } - Session createFromSessionId(@NonNull ArrayList<Byte> sessionId) { - if (sessionId == null || sessionId.size() == 0) { + Session createFromSessionId(byte[] sessionId) { + if (sessionId == null || sessionId.length == 0) { return null; } return new Session(sessionId); @@ -638,12 +743,20 @@ public final class MediaCas implements AutoCloseable { * @return Whether the specified CA system is supported on this device. */ public static boolean isSystemIdSupported(int CA_system_id) { - IMediaCasService service = getService(); - + IMediaCasService service = sService.get(); if (service != null) { try { return service.isSystemIdSupported(CA_system_id); } catch (RemoteException e) { + return false; + } + } + + android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + if (serviceHidl != null) { + try { + return serviceHidl.isSystemIdSupported(CA_system_id); + } catch (RemoteException e) { } } return false; @@ -655,12 +768,26 @@ public final class MediaCas implements AutoCloseable { * @return an array of descriptors for the available CA plugins. */ public static PluginDescriptor[] enumeratePlugins() { - IMediaCasService service = getService(); - + IMediaCasService service = sService.get(); if (service != null) { try { - ArrayList<HidlCasPluginDescriptor> descriptors = - service.enumeratePlugins(); + AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins(); + if (descriptors.length == 0) { + return null; + } + PluginDescriptor[] results = new PluginDescriptor[descriptors.length]; + for (int i = 0; i < results.length; i++) { + results[i] = new PluginDescriptor(descriptors[i]); + } + return results; + } catch (RemoteException e) { + } + } + + android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + if (serviceHidl != null) { + try { + ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.enumeratePlugins(); if (descriptors.size() == 0) { return null; } @@ -680,29 +807,40 @@ public final class MediaCas implements AutoCloseable { mCasSystemId = casSystemId; mUserId = Process.myUid(); IMediaCasService service = getService(); - android.hardware.cas.V1_2.IMediaCasService serviceV12 = - android.hardware.cas.V1_2.IMediaCasService.castFrom(service); - if (serviceV12 == null) { - android.hardware.cas.V1_1.IMediaCasService serviceV11 = - android.hardware.cas.V1_1.IMediaCasService.castFrom(service); - if (serviceV11 == null) { + if (service != null) { + Log.d(TAG, "Use CAS AIDL interface to create plugin"); + mICas = service.createPlugin(casSystemId, mBinder); + } else { + android.hardware.cas.V1_0.IMediaCasService serviceV10 = getServiceHidl(); + android.hardware.cas.V1_2.IMediaCasService serviceV12 = + android.hardware.cas.V1_2.IMediaCasService.castFrom(serviceV10); + if (serviceV12 == null) { + android.hardware.cas.V1_1.IMediaCasService serviceV11 = + android.hardware.cas.V1_1.IMediaCasService.castFrom(serviceV10); + if (serviceV11 == null) { Log.d(TAG, "Used cas@1_0 interface to create plugin"); - mICas = service.createPlugin(casSystemId, mBinder); - } else { + mICasHidl = serviceV10.createPlugin(casSystemId, mBinderHidl); + } else { Log.d(TAG, "Used cas@1.1 interface to create plugin"); - mICas = mICasV11 = serviceV11.createPluginExt(casSystemId, mBinder); + mICasHidl = + mICasHidl11 = serviceV11.createPluginExt(casSystemId, mBinderHidl); + } + } else { + Log.d(TAG, "Used cas@1.2 interface to create plugin"); + mICasHidl = + mICasHidl11 = + mICasHidl12 = + android.hardware.cas.V1_2.ICas.castFrom( + serviceV12.createPluginExt( + casSystemId, mBinderHidl)); } - } else { - Log.d(TAG, "Used cas@1.2 interface to create plugin"); - mICas = mICasV11 = mICasV12 = - android.hardware.cas.V1_2.ICas - .castFrom(serviceV12.createPluginExt(casSystemId, mBinder)); } } catch(Exception e) { Log.e(TAG, "Failed to create plugin: " + e); mICas = null; + mICasHidl = null; } finally { - if (mICas == null) { + if (mICas == null && mICasHidl == null) { throw new UnsupportedCasException( "Unsupported casSystemId " + casSystemId); } @@ -783,9 +921,22 @@ public final class MediaCas implements AutoCloseable { } IHwBinder getBinder() { + if (mICas != null) { + return null; // Return IHwBinder only for HIDL + } + validateInternalStates(); - return mICas.asBinder(); + return mICasHidl.asBinder(); + } + + /** + * Check if the HAL is an AIDL implementation + * + * @hide + */ + public boolean isAidlHal() { + return mICas != null; } /** @@ -886,8 +1037,12 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.setPrivateData(toByteArray(data, 0, data.length))); + if (mICas != null) { + mICas.setPrivateData(data); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.setPrivateData(toByteArray(data, 0, data.length))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -899,7 +1054,7 @@ public final class MediaCas implements AutoCloseable { @Override public void onValues(int status, ArrayList<Byte> sessionId) { mStatus = status; - mSession = createFromSessionId(sessionId); + mSession = createFromSessionId(toBytes(sessionId)); } } @@ -912,7 +1067,7 @@ public final class MediaCas implements AutoCloseable { @Override public void onValues(int status, ArrayList<Byte> sessionId) { mStatus = status; - mSession = createFromSessionId(sessionId); + mSession = createFromSessionId(toBytes(sessionId)); } } @@ -971,15 +1126,19 @@ public final class MediaCas implements AutoCloseable { int sessionResourceHandle = getSessionResourceHandle(); try { - OpenSessionCallback cb = new OpenSessionCallback(); - mICas.openSession(cb); - MediaCasException.throwExceptionIfNeeded(cb.mStatus); - addSessionToResourceMap(cb.mSession, sessionResourceHandle); - Log.d(TAG, "Write Stats Log for succeed to Open Session."); - FrameworkStatsLog - .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId, + if (mICasHidl != null) { + OpenSessionCallback cb = new OpenSessionCallback(); + mICasHidl.openSession(cb); + MediaCasException.throwExceptionIfNeeded(cb.mStatus); + addSessionToResourceMap(cb.mSession, sessionResourceHandle); + Log.d(TAG, "Write Stats Log for succeed to Open Session."); + FrameworkStatsLog.write( + FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, + mUserId, + mCasSystemId, FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED); - return cb.mSession; + return cb.mSession; + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -1012,14 +1171,30 @@ public final class MediaCas implements AutoCloseable { throws MediaCasException { int sessionResourceHandle = getSessionResourceHandle(); - if (mICasV12 == null) { + if (mICas != null) { + try { + byte[] sessionId = mICas.openSession(sessionUsage, scramblingMode); + Session session = createFromSessionId(sessionId); + addSessionToResourceMap(session, sessionResourceHandle); + Log.d(TAG, "Write Stats Log for succeed to Open Session."); + FrameworkStatsLog.write( + FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, + mUserId, + mCasSystemId, + FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED); + return session; + } catch (RemoteException e) { + cleanupAndRethrowIllegalState(); + } + } + if (mICasHidl12 == null) { Log.d(TAG, "Open Session with scrambling mode is only supported by cas@1.2+ interface"); throw new UnsupportedCasException("Open Session with scrambling mode is not supported"); } try { OpenSession_1_2_Callback cb = new OpenSession_1_2_Callback(); - mICasV12.openSession_1_2(sessionUsage, scramblingMode, cb); + mICasHidl12.openSession_1_2(sessionUsage, scramblingMode, cb); MediaCasException.throwExceptionIfNeeded(cb.mStatus); addSessionToResourceMap(cb.mSession, sessionResourceHandle); Log.d(TAG, "Write Stats Log for succeed to Open Session."); @@ -1053,8 +1228,12 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.processEmm(toByteArray(data, offset, length))); + if (mICas != null) { + mICas.processEmm(Arrays.copyOfRange(data, offset, length)); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.processEmm(toByteArray(data, offset, length))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -1092,8 +1271,12 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.sendEvent(event, arg, toByteArray(data))); + if (mICas != null) { + mICas.sendEvent(event, arg, data); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.sendEvent(event, arg, toByteArray(data))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -1114,8 +1297,11 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.provision(provisionString)); + if (mICas != null) { + mICas.provision(provisionString); + } else { + MediaCasException.throwExceptionIfNeeded(mICasHidl.provision(provisionString)); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -1136,8 +1322,12 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.refreshEntitlements(refreshType, toByteArray(refreshData))); + if (mICas != null) { + mICas.refreshEntitlements(refreshType, refreshData); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.refreshEntitlements(refreshType, toByteArray(refreshData))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -1163,6 +1353,13 @@ public final class MediaCas implements AutoCloseable { } finally { mICas = null; } + } else if (mICasHidl != null) { + try { + mICasHidl.release(); + } catch (RemoteException e) { + } finally { + mICasHidl = mICasHidl11 = mICasHidl12 = null; + } } if (mTunerResourceManager != null) { diff --git a/media/java/android/media/MediaDescrambler.java b/media/java/android/media/MediaDescrambler.java index 99bd2549cbc7..b4bdf93db3ab 100644 --- a/media/java/android/media/MediaDescrambler.java +++ b/media/java/android/media/MediaDescrambler.java @@ -17,14 +17,26 @@ package android.media; import android.annotation.NonNull; -import android.hardware.cas.V1_0.*; +import android.hardware.cas.DestinationBuffer; +import android.hardware.cas.IDescrambler; +import android.hardware.cas.ScramblingControl; +import android.hardware.cas.SharedBuffer; +import android.hardware.cas.SubSample; +import android.hardware.cas.V1_0.IDescramblerBase; +import android.hardware.common.Ashmem; +import android.hardware.common.NativeHandle; import android.media.MediaCasException.UnsupportedCasException; import android.os.IHwBinder; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceSpecificException; +import android.os.SharedMemory; +import android.system.ErrnoException; import android.util.Log; +import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; /** * MediaDescrambler class can be used in conjunction with {@link android.media.MediaCodec} @@ -39,7 +51,198 @@ import java.nio.ByteBuffer; */ public final class MediaDescrambler implements AutoCloseable { private static final String TAG = "MediaDescrambler"; - private IDescramblerBase mIDescrambler; + private DescramblerWrapper mIDescrambler; + + private interface DescramblerWrapper { + + IHwBinder asBinder(); + + int descramble( + @NonNull ByteBuffer srcBuf, + @NonNull ByteBuffer dstBuf, + @NonNull MediaCodec.CryptoInfo cryptoInfo) + throws RemoteException; + + boolean requiresSecureDecoderComponent(@NonNull String mime) throws RemoteException; + + void setMediaCasSession(byte[] sessionId) throws RemoteException; + + void release() throws RemoteException; + } + ; + + private long getSubsampleInfo( + int numSubSamples, + int[] numBytesOfClearData, + int[] numBytesOfEncryptedData, + SubSample[] subSamples) { + long totalSize = 0; + + for (int i = 0; i < numSubSamples; i++) { + totalSize += numBytesOfClearData[i]; + subSamples[i].numBytesOfClearData = numBytesOfClearData[i]; + totalSize += numBytesOfEncryptedData[i]; + subSamples[i].numBytesOfEncryptedData = numBytesOfEncryptedData[i]; + } + return totalSize; + } + + private ParcelFileDescriptor createSharedMemory(ByteBuffer buffer, String name) + throws RemoteException { + byte[] source = buffer.array(); + if (source.length == 0) { + return null; + } + ParcelFileDescriptor fd = null; + try { + SharedMemory ashmem = SharedMemory.create(name == null ? "" : name, source.length); + ByteBuffer ptr = ashmem.mapReadWrite(); + ptr.put(buffer); + ashmem.unmap(ptr); + fd = ashmem.getFdDup(); + return fd; + } catch (ErrnoException | IOException e) { + throw new RemoteException(e); + } + } + + private class AidlDescrambler implements DescramblerWrapper { + + IDescrambler mAidlDescrambler; + + AidlDescrambler(IDescrambler aidlDescrambler) { + mAidlDescrambler = aidlDescrambler; + } + + @Override + public IHwBinder asBinder() { + return null; + } + + @Override + public int descramble( + @NonNull ByteBuffer src, + @NonNull ByteBuffer dst, + @NonNull MediaCodec.CryptoInfo cryptoInfo) + throws RemoteException { + SubSample[] subSamples = new SubSample[cryptoInfo.numSubSamples]; + long totalLength = + getSubsampleInfo( + cryptoInfo.numSubSamples, + cryptoInfo.numBytesOfClearData, + cryptoInfo.numBytesOfEncryptedData, + subSamples); + SharedBuffer srcBuffer = new SharedBuffer(); + DestinationBuffer dstBuffer; + srcBuffer.heapBase = new Ashmem(); + srcBuffer.heapBase.fd = createSharedMemory(src, "Descrambler Source Buffer"); + srcBuffer.heapBase.size = src.array().length; + if (dst == null) { + dstBuffer = DestinationBuffer.nonsecureMemory(srcBuffer); + } else { + ParcelFileDescriptor pfd = + createSharedMemory(dst, "Descrambler Destination Buffer"); + NativeHandle nh = new NativeHandle(); + nh.fds = new ParcelFileDescriptor[] {pfd}; + nh.ints = new int[] {1}; // Mark 1 since source buffer also uses it? + dstBuffer = DestinationBuffer.secureMemory(nh); + } + @ScramblingControl int control = cryptoInfo.key[0]; + + return mAidlDescrambler.descramble( + (byte) control, + subSamples, + srcBuffer, + src.position(), + dstBuffer, + dst.position()); + } + + @Override + public boolean requiresSecureDecoderComponent(@NonNull String mime) throws RemoteException { + return mAidlDescrambler.requiresSecureDecoderComponent(mime); + } + + @Override + public void setMediaCasSession(byte[] sessionId) throws RemoteException { + mAidlDescrambler.setMediaCasSession(sessionId); + } + + @Override + public void release() throws RemoteException { + mAidlDescrambler.release(); + } + } + + private class HidlDescrambler implements DescramblerWrapper { + + IDescramblerBase mHidlDescrambler; + + HidlDescrambler(IDescramblerBase hidlDescrambler) { + mHidlDescrambler = hidlDescrambler; + native_setup(hidlDescrambler.asBinder()); + } + + @Override + public IHwBinder asBinder() { + return mHidlDescrambler.asBinder(); + } + + @Override + public int descramble( + @NonNull ByteBuffer srcBuf, + @NonNull ByteBuffer dstBuf, + @NonNull MediaCodec.CryptoInfo cryptoInfo) + throws RemoteException { + + try { + return native_descramble( + cryptoInfo.key[0], + cryptoInfo.key[1], + cryptoInfo.numSubSamples, + cryptoInfo.numBytesOfClearData, + cryptoInfo.numBytesOfEncryptedData, + srcBuf, + srcBuf.position(), + srcBuf.limit(), + dstBuf, + dstBuf.position(), + dstBuf.limit()); + } catch (ServiceSpecificException e) { + MediaCasStateException.throwExceptionIfNeeded(e.errorCode, e.getMessage()); + } catch (RemoteException e) { + cleanupAndRethrowIllegalState(); + } + return -1; + } + + @Override + public boolean requiresSecureDecoderComponent(@NonNull String mime) throws RemoteException { + return mHidlDescrambler.requiresSecureDecoderComponent(mime); + } + + @Override + public void setMediaCasSession(byte[] sessionId) throws RemoteException { + ArrayList<Byte> byteArray = new ArrayList<>(); + + if (sessionId != null) { + int length = sessionId.length; + byteArray = new ArrayList<Byte>(length); + for (int i = 0; i < length; i++) { + byteArray.add(Byte.valueOf(sessionId[i])); + } + } + + MediaCasStateException.throwExceptionIfNeeded( + mHidlDescrambler.setMediaCasSession(byteArray)); + } + + @Override + public void release() throws RemoteException { + mHidlDescrambler.release(); + native_release(); + } + } private final void validateInternalStates() { if (mIDescrambler == null) { @@ -61,7 +264,14 @@ public final class MediaDescrambler implements AutoCloseable { */ public MediaDescrambler(int CA_system_id) throws UnsupportedCasException { try { - mIDescrambler = MediaCas.getService().createDescrambler(CA_system_id); + if (MediaCas.getService() != null) { + mIDescrambler = + new AidlDescrambler(MediaCas.getService().createDescrambler(CA_system_id)); + } else if (MediaCas.getServiceHidl() != null) { + mIDescrambler = + new HidlDescrambler( + MediaCas.getServiceHidl().createDescrambler(CA_system_id)); + } } catch(Exception e) { Log.e(TAG, "Failed to create descrambler: " + e); mIDescrambler = null; @@ -70,7 +280,6 @@ public final class MediaDescrambler implements AutoCloseable { throw new UnsupportedCasException("Unsupported CA_system_id " + CA_system_id); } } - native_setup(mIDescrambler.asBinder()); } IHwBinder getBinder() { @@ -117,8 +326,7 @@ public final class MediaDescrambler implements AutoCloseable { validateInternalStates(); try { - MediaCasStateException.throwExceptionIfNeeded( - mIDescrambler.setMediaCasSession(session.mSessionId)); + mIDescrambler.setMediaCasSession(session.mSessionId); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -126,27 +334,31 @@ public final class MediaDescrambler implements AutoCloseable { /** * Scramble control value indicating that the samples are not scrambled. + * * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo) */ - public static final byte SCRAMBLE_CONTROL_UNSCRAMBLED = 0; + public static final byte SCRAMBLE_CONTROL_UNSCRAMBLED = (byte) ScramblingControl.UNSCRAMBLED; /** * Scramble control value reserved and shouldn't be used currently. + * * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo) */ - public static final byte SCRAMBLE_CONTROL_RESERVED = 1; + public static final byte SCRAMBLE_CONTROL_RESERVED = (byte) ScramblingControl.RESERVED; /** * Scramble control value indicating that the even key is used. + * * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo) */ - public static final byte SCRAMBLE_CONTROL_EVEN_KEY = 2; + public static final byte SCRAMBLE_CONTROL_EVEN_KEY = (byte) ScramblingControl.EVENKEY; /** * Scramble control value indicating that the odd key is used. + * * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo) */ - public static final byte SCRAMBLE_CONTROL_ODD_KEY = 3; + public static final byte SCRAMBLE_CONTROL_ODD_KEY = (byte) ScramblingControl.ODDKEY; /** * Scramble flag for a hint indicating that the descrambling request is for @@ -207,14 +419,7 @@ public final class MediaDescrambler implements AutoCloseable { } try { - return native_descramble( - cryptoInfo.key[0], - cryptoInfo.key[1], - cryptoInfo.numSubSamples, - cryptoInfo.numBytesOfClearData, - cryptoInfo.numBytesOfEncryptedData, - srcBuf, srcBuf.position(), srcBuf.limit(), - dstBuf, dstBuf.position(), dstBuf.limit()); + return mIDescrambler.descramble(srcBuf, dstBuf, cryptoInfo); } catch (ServiceSpecificException e) { MediaCasStateException.throwExceptionIfNeeded(e.errorCode, e.getMessage()); } catch (RemoteException e) { @@ -233,7 +438,6 @@ public final class MediaDescrambler implements AutoCloseable { mIDescrambler = null; } } - native_release(); } @Override @@ -256,4 +460,4 @@ public final class MediaDescrambler implements AutoCloseable { } private long mNativeContext; -}
\ No newline at end of file +} diff --git a/media/java/android/media/MediaExtractor.java b/media/java/android/media/MediaExtractor.java index dab188e40c1f..b11a81047bf8 100644 --- a/media/java/android/media/MediaExtractor.java +++ b/media/java/android/media/MediaExtractor.java @@ -36,7 +36,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -325,14 +324,6 @@ public final class MediaExtractor { } } - private ArrayList<Byte> toByteArray(@NonNull byte[] data) { - ArrayList<Byte> byteArray = new ArrayList<Byte>(data.length); - for (int i = 0; i < data.length; i++) { - byteArray.add(i, Byte.valueOf(data[i])); - } - return byteArray; - } - /** * Retrieves the information about the conditional access system used to scramble * a track. @@ -357,7 +348,7 @@ public final class MediaExtractor { buf.rewind(); final byte[] sessionId = new byte[buf.remaining()]; buf.get(sessionId); - session = mMediaCas.createFromSessionId(toByteArray(sessionId)); + session = mMediaCas.createFromSessionId(sessionId); } return new CasInfo(systemId, session, privateData); } diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index d8705a7ce9ca..5b0c2a203022 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -2432,13 +2432,12 @@ static void android_media_MediaCodec_native_queueLinearBlock( throwExceptionAsNecessary(env, BAD_VALUE); return; } - NativeCryptoInfo cryptoInfo = [env, cryptoInfoObj, size]{ - if (cryptoInfoObj == nullptr) { - return NativeCryptoInfo{size}; - } else { - return NativeCryptoInfo{env, cryptoInfoObj}; - } - }(); + auto cryptoInfo = + cryptoInfoObj ? NativeCryptoInfo{size} : NativeCryptoInfo{env, cryptoInfoObj}; + if (env->ExceptionCheck()) { + // Creation of cryptoInfo failed. Let the exception bubble up. + return; + } err = codec->queueEncryptedLinearBlock( index, memory, diff --git a/media/tests/AudioPolicyTest/res/values/strings.xml b/media/tests/AudioPolicyTest/res/values/strings.xml index 036592770450..128c3c52aaff 100644 --- a/media/tests/AudioPolicyTest/res/values/strings.xml +++ b/media/tests/AudioPolicyTest/res/values/strings.xml @@ -2,4 +2,7 @@ <resources> <!-- name of the app [CHAR LIMIT=25]--> <string name="app_name">Audio Policy APIs Tests</string> + <string name="capture_duration_key">captureDurationMs</string> + <string name="callback_key">callback</string> + <string name="status_key">result</string> </resources> diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java index 841804b02354..48c51af26d3a 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java @@ -20,6 +20,8 @@ import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeNoException; +import static org.junit.Assume.assumeTrue; import android.content.BroadcastReceiver; import android.content.Context; @@ -30,6 +32,8 @@ import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; +import android.os.Bundle; +import android.os.RemoteCallback; import android.platform.test.annotations.Presubmit; import android.util.Log; @@ -39,33 +43,62 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + @Presubmit @RunWith(AndroidJUnit4.class) public class AudioPolicyDeathTest { private static final String TAG = "AudioPolicyDeathTest"; private static final int SAMPLE_RATE = 48000; - private static final int PLAYBACK_TIME_MS = 2000; + private static final int PLAYBACK_TIME_MS = 4000; + private static final int RECORD_TIME_MS = 1000; + private static final int ACTIVITY_TIMEOUT_SEC = 5; + private static final int BROADCAST_TIMEOUT_SEC = 10; + private static final int MAX_ATTEMPTS = 5; + private static final int DELAY_BETWEEN_ATTEMPTS_MS = 2000; private static final IntentFilter AUDIO_NOISY_INTENT_FILTER = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); private class MyBroadcastReceiver extends BroadcastReceiver { - private boolean mReceived = false; + private CountDownLatch mLatch = new CountDownLatch(1); + @Override public void onReceive(Context context, Intent intent) { if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { - synchronized (this) { - mReceived = true; - notify(); - } + mLatch.countDown(); } } - public synchronized boolean received() { - return mReceived; + public void reset() { + mLatch = new CountDownLatch(1); + } + + public boolean waitForBroadcast() { + boolean received = false; + long startTimeMs = System.currentTimeMillis(); + long elapsedTimeMs = 0; + + Log.i(TAG, "waiting for broadcast"); + + while (elapsedTimeMs < BROADCAST_TIMEOUT_SEC && !received) { + try { + received = mLatch.await(BROADCAST_TIMEOUT_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.w(TAG, "wait interrupted"); + } + elapsedTimeMs = System.currentTimeMillis() - startTimeMs; + } + Log.i(TAG, "broadcast " + (received ? "" : "NOT ") + "received"); + return received; } } + private final MyBroadcastReceiver mReceiver = new MyBroadcastReceiver(); private Context mContext; @@ -85,31 +118,55 @@ public class AudioPolicyDeathTest { public void testPolicyClientDeathSendBecomingNoisyIntent() { mContext.registerReceiver(mReceiver, AUDIO_NOISY_INTENT_FILTER); - // Launch process registering a dynamic auido policy and dying after PLAYBACK_TIME_MS/2 ms - Intent intent = new Intent(mContext, AudioPolicyDeathTestActivity.class); - intent.putExtra("captureDurationMs", PLAYBACK_TIME_MS / 2); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - mContext.startActivity(intent); - - AudioTrack track = createAudioTrack(); - track.play(); - synchronized (mReceiver) { - long startTimeMs = System.currentTimeMillis(); - long elapsedTimeMs = 0; - while (elapsedTimeMs < PLAYBACK_TIME_MS && !mReceiver.received()) { + boolean result = false; + for (int numAttempts = 1; numAttempts <= MAX_ATTEMPTS && !result; numAttempts++) { + mReceiver.reset(); + + CompletableFuture<Integer> callbackReturn = new CompletableFuture<>(); + RemoteCallback cb = new RemoteCallback((Bundle res) -> { + callbackReturn.complete( + res.getInt(mContext.getResources().getString(R.string.status_key))); + }); + + // Launch process registering a dynamic auido policy and dying after RECORD_TIME_MS ms + // RECORD_TIME_MS must be shorter than PLAYBACK_TIME_MS + Intent intent = new Intent(mContext, AudioPolicyDeathTestActivity.class); + intent.putExtra(mContext.getResources().getString(R.string.capture_duration_key), + RECORD_TIME_MS); + intent.putExtra(mContext.getResources().getString(R.string.callback_key), cb); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + mContext.startActivity(intent); + + Integer status = AudioManager.ERROR; + try { + status = callbackReturn.get(ACTIVITY_TIMEOUT_SEC, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + assumeNoException(e); + } + assumeTrue(status != null && status == AudioManager.SUCCESS); + + Log.i(TAG, "Activity started"); + AudioTrack track = null; + try { + track = createAudioTrack(); + track.play(); + result = mReceiver.waitForBroadcast(); + } finally { + if (track != null) { + track.stop(); + track.release(); + } + } + if (!result) { try { - mReceiver.wait(PLAYBACK_TIME_MS - elapsedTimeMs); + Log.i(TAG, "Retrying after attempt: " + numAttempts); + Thread.sleep(DELAY_BETWEEN_ATTEMPTS_MS); } catch (InterruptedException e) { - Log.w(TAG, "wait interrupted"); } - elapsedTimeMs = System.currentTimeMillis() - startTimeMs; } } - - track.stop(); - track.release(); - - assertTrue(mReceiver.received()); + assertTrue(result); } private AudioTrack createAudioTrack() { diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java index 957e719ab71f..ce5f56c9e556 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java @@ -26,6 +26,7 @@ import android.media.audiopolicy.AudioMixingRule; import android.media.audiopolicy.AudioPolicy; import android.os.Bundle; import android.os.Looper; +import android.os.RemoteCallback; import android.util.Log; // This activity will register a dynamic audio policy to intercept media playback and launch @@ -71,19 +72,29 @@ public class AudioPolicyDeathTestActivity extends Activity { mAudioPolicy = audioPolicyBuilder.build(); int result = mAudioManager.registerAudioPolicy(mAudioPolicy); - if (result != AudioManager.SUCCESS) { + if (result == AudioManager.SUCCESS) { + AudioRecord audioRecord = mAudioPolicy.createAudioRecordSink(audioMix); + if (audioRecord != null && audioRecord.getState() != AudioRecord.STATE_UNINITIALIZED) { + int captureDurationMs = getIntent().getIntExtra( + getString(R.string.capture_duration_key), RECORD_TIME_MS); + AudioCapturingThread thread = + new AudioCapturingThread(audioRecord, captureDurationMs); + thread.start(); + } else { + Log.w(TAG, "AudioRecord creation failed"); + result = AudioManager.ERROR_NO_INIT; + } + } else { Log.w(TAG, "registerAudioPolicy failed, status: " + result); - return; - } - AudioRecord audioRecord = mAudioPolicy.createAudioRecordSink(audioMix); - if (audioRecord == null) { - Log.w(TAG, "AudioRecord creation failed"); - return; } - int captureDurationMs = getIntent().getIntExtra("captureDurationMs", RECORD_TIME_MS); - AudioCapturingThread thread = new AudioCapturingThread(audioRecord, captureDurationMs); - thread.start(); + RemoteCallback cb = + (RemoteCallback) getIntent().getExtras().get(getString(R.string.callback_key)); + Bundle res = new Bundle(); + res.putInt(getString(R.string.status_key), result); + Log.i(TAG, "policy " + (result == AudioManager.SUCCESS ? "" : "un") + + "successfully registered"); + cb.sendResult(res); } @Override diff --git a/native/webview/TEST_MAPPING b/native/webview/TEST_MAPPING index bd25200ffc38..c1bc6d720ece 100644 --- a/native/webview/TEST_MAPPING +++ b/native/webview/TEST_MAPPING @@ -9,6 +9,14 @@ ] }, { + "name": "CtsSdkSandboxWebkitTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { "name": "CtsHostsideWebViewTests", "options": [ { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 1e5bd7bf843e..7d433648df28 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -152,22 +152,6 @@ class CredentialManagerRepo( return CreateFlowUtils.toRequestDisplayInfo(requestInfo, context) } - companion object { - // TODO: find a way to resolve this static field leak problem - lateinit var repo: CredentialManagerRepo - - fun setup( - context: Context, - intent: Intent, - ) { - repo = CredentialManagerRepo(context, intent) - } - - fun getInstance(): CredentialManagerRepo { - return repo - } - } - // TODO: below are prototype functionalities. To be removed for productionization. private fun testCreateCredentialEnabledProviderList(): List<CreateCredentialProviderData> { return listOf( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 686415f9d3e9..0620f9aa8c82 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -45,19 +45,19 @@ import kotlinx.coroutines.launch class CredentialSelectorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - CredentialManagerRepo.setup(this, intent) + val credManRepo = CredentialManagerRepo(this, intent) UserConfigRepo.setup(this) - val requestInfo = CredentialManagerRepo.getInstance().requestInfo + val requestInfo = credManRepo.requestInfo setContent { CredentialSelectorTheme { - CredentialManagerBottomSheet(DialogType.toDialogType(requestInfo.type)) + CredentialManagerBottomSheet(DialogType.toDialogType(requestInfo.type), credManRepo) } } } @ExperimentalMaterialApi @Composable - fun CredentialManagerBottomSheet(dialogType: DialogType) { + fun CredentialManagerBottomSheet(dialogType: DialogType, credManRepo: CredentialManagerRepo) { val providerActivityResult = remember { mutableStateOf<ProviderActivityResult?>(null) } val launcher = rememberLauncherForActivityResult( ActivityResultContracts.StartIntentSenderForResult() @@ -66,7 +66,9 @@ class CredentialSelectorActivity : ComponentActivity() { } when (dialogType) { DialogType.CREATE_PASSKEY -> { - val viewModel: CreateCredentialViewModel = viewModel() + val viewModel: CreateCredentialViewModel = viewModel{ + CreateCredentialViewModel(credManRepo) + } lifecycleScope.launch { viewModel.observeDialogResult().collect{ dialogResult -> onCancel(dialogResult) @@ -79,7 +81,9 @@ class CredentialSelectorActivity : ComponentActivity() { CreateCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher) } DialogType.GET_CREDENTIALS -> { - val viewModel: GetCredentialViewModel = viewModel() + val viewModel: GetCredentialViewModel = viewModel{ + GetCredentialViewModel(credManRepo) + } lifecycleScope.launch { viewModel.observeDialogResult().collect{ dialogResult -> onCancel(dialogResult) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 9803fc64cd2f..09f9b5eaaf6a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -44,6 +44,7 @@ import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL import com.android.credentialmanager.jetpack.provider.Action +import com.android.credentialmanager.jetpack.provider.AuthenticationAction import com.android.credentialmanager.jetpack.provider.CredentialCountInformation import com.android.credentialmanager.jetpack.provider.CredentialEntry import com.android.credentialmanager.jetpack.provider.CreateEntry @@ -140,16 +141,20 @@ class GetFlowUtils { providerIcon: Drawable, authEntry: Entry?, ): AuthenticationEntryInfo? { - // TODO: should also call fromSlice after getting the official jetpack code. - if (authEntry == null) { return null } + val authStructuredEntry = AuthenticationAction.fromSlice( + authEntry!!.slice) + if (authStructuredEntry == null) { + return null + } + return AuthenticationEntryInfo( providerId = providerId, entryKey = authEntry.key, entrySubkey = authEntry.subkey, - pendingIntent = authEntry.pendingIntent, + pendingIntent = authStructuredEntry.pendingIntent, fillInIntent = authEntry.frameworkExtrasIntent, title = providerDisplayName, icon = providerIcon, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt index aadbbc6deb6c..ac84503583a8 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt @@ -52,10 +52,9 @@ data class CreateCredentialUiState( ) class CreateCredentialViewModel( - credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance(), - userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance() + private val credManRepo: CredentialManagerRepo, + userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance(), ) : ViewModel() { - var providerEnableListUiState = credManRepo.getCreateProviderEnableListInitialUiState() var providerDisableListUiState = credManRepo.getCreateProviderDisableListInitialUiState() @@ -125,7 +124,9 @@ class CreateCredentialViewModel( fun onEntrySelectedFromMoreOptionScreen(activeEntry: ActiveEntry) { uiState = uiState.copy( - currentScreenState = CreateScreenState.MORE_OPTIONS_ROW_INTRO, + currentScreenState = if ( + activeEntry.activeProvider.id == UserConfigRepo.getInstance().getDefaultProviderId() + ) CreateScreenState.CREATION_OPTION_SELECTION else CreateScreenState.MORE_OPTIONS_ROW_INTRO, activeEntry = activeEntry ) } @@ -140,12 +141,12 @@ class CreateCredentialViewModel( } fun onDisabledProvidersSelected() { - CredentialManagerRepo.getInstance().onCancel() + credManRepo.onCancel() dialogResult.tryEmit(DialogResult(ResultState.LAUNCH_SETTING_CANCELED)) } fun onCancel() { - CredentialManagerRepo.getInstance().onCancel() + credManRepo.onCancel() dialogResult.tryEmit(DialogResult(ResultState.NORMAL_CANCELED)) } @@ -187,7 +188,7 @@ class CreateCredentialViewModel( hidden = true, ) } else { - CredentialManagerRepo.getInstance().onOptionSelected( + credManRepo.onOptionSelected( providerId, entryKey, entrySubkey @@ -241,7 +242,7 @@ class CreateCredentialViewModel( "$providerId, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " + "resultCode=$resultCode, resultData=$resultData}" ) - CredentialManagerRepo.getInstance().onOptionSelected( + credManRepo.onOptionSelected( providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData, ) } else { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt index af59a0a39c72..6f0f76b72e50 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt @@ -46,9 +46,7 @@ data class GetCredentialUiState( val isNoAccount: Boolean = false, ) -class GetCredentialViewModel( - credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance() -) : ViewModel() { +class GetCredentialViewModel(private val credManRepo: CredentialManagerRepo) : ViewModel() { var uiState by mutableStateOf(credManRepo.getCredentialInitialUiState()) private set @@ -70,9 +68,7 @@ class GetCredentialViewModel( hidden = true, ) } else { - CredentialManagerRepo.getInstance().onOptionSelected( - entry.providerId, entry.entryKey, entry.entrySubkey, - ) + credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey) dialogResult.tryEmit(DialogResult(ResultState.COMPLETE)) } } @@ -110,7 +106,7 @@ class GetCredentialViewModel( "${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " + "resultCode=$resultCode, resultData=$resultData}" ) - CredentialManagerRepo.getInstance().onOptionSelected( + credManRepo.onOptionSelected( entry.providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData, ) @@ -144,7 +140,7 @@ class GetCredentialViewModel( } fun onCancel() { - CredentialManagerRepo.getInstance().onCancel() + credManRepo.onCancel() dialogResult.tryEmit(DialogResult(ResultState.NORMAL_CANCELED)) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt new file mode 100644 index 000000000000..283c7ba16fa5 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt @@ -0,0 +1,55 @@ +/* + * 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.credentialmanager.jetpack.provider + +import android.app.PendingIntent +import android.app.slice.Slice +import android.util.Log +import androidx.annotation.VisibleForTesting + +/** + * UI representation for a credential entry used during the get credential flow. + * + * TODO: move to jetpack. + */ +class AuthenticationAction constructor( + val pendingIntent: PendingIntent +) { + + + companion object { + private const val TAG = "AuthenticationAction" + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val SLICE_HINT_PENDING_INTENT = + "androidx.credentials.provider.authenticationAction.SLICE_HINT_PENDING_INTENT" + + @JvmStatic + fun fromSlice(slice: Slice): AuthenticationAction? { + slice.items.forEach { + if (it.hasHint(SLICE_HINT_PENDING_INTENT)) { + return try { + AuthenticationAction(it.action) + } catch (e: Exception) { + Log.i(TAG, "fromSlice failed with: " + e.message) + null + } + } + } + return null + } + } +} diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml index b713c1420928..cb2baa974b0c 100644 --- a/packages/PackageInstaller/res/values/strings.xml +++ b/packages/PackageInstaller/res/values/strings.xml @@ -37,6 +37,11 @@ <string name="install_confirm_question">Do you want to install this app?</string> <!-- Message for updating an existing app [CHAR LIMIT=NONE] --> <string name="install_confirm_question_update">Do you want to update this app?</string> + <!-- TODO(b/244413073) Revise the description after getting UX input and UXR on this. --> + <!-- Message for updating an existing app when updating owner changed [CHAR LIMIT=NONE] --> + <string name="install_confirm_question_update_owner_changed">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nBy updating, you\'ll get future updates from <xliff:g id="new_update_owner">%2$s</xliff:g> instead.</string> + <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] --> + <string name="install_confirm_question_update_owner_reminder">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nDo you want to install this update from <xliff:g id="new_update_owner">%2$s</xliff:g>.</string> <!-- [CHAR LIMIT=100] --> <string name="install_failed">App not installed.</string> <!-- Reason displayed when installation fails because the package was blocked diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index 313815839f53..49c9188a2cab 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -33,10 +33,12 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.InstallSourceInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; @@ -47,9 +49,11 @@ import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Button; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.StringRes; @@ -88,6 +92,7 @@ public class PackageInstallerActivity extends AlertActivity { private int mOriginatingUid = Process.INVALID_UID; private String mOriginatingPackage; // The package name corresponding to #mOriginatingUid private int mActivityResultCode = Activity.RESULT_CANCELED; + private int mPendingUserActionReason = -1; private final boolean mLocalLOGV = false; PackageManager mPm; @@ -132,10 +137,27 @@ public class PackageInstallerActivity extends AlertActivity { private boolean mEnableOk = false; private void startInstallConfirm() { - View viewToEnable; + TextView viewToEnable; if (mAppInfo != null) { viewToEnable = requireViewById(R.id.install_confirm_question_update); + + final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(); + final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mCallingPackage); + if (!TextUtils.isEmpty(existingUpdateOwnerLabel)) { + if (mPendingUserActionReason == PackageInstaller.REASON_OWNERSHIP_CHANGED) { + viewToEnable.setText( + getString(R.string.install_confirm_question_update_owner_changed, + existingUpdateOwnerLabel, requestedUpdateOwnerLabel)); + } else if (mPendingUserActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP + || mPendingUserActionReason + == PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE) { + viewToEnable.setText( + getString(R.string.install_confirm_question_update_owner_reminder, + existingUpdateOwnerLabel, requestedUpdateOwnerLabel)); + } + } + mOk.setText(R.string.update); } else { // This is a new application with no permissions. @@ -149,6 +171,27 @@ public class PackageInstallerActivity extends AlertActivity { mOk.setFilterTouchesWhenObscured(true); } + private CharSequence getExistingUpdateOwnerLabel() { + try { + final String packageName = mPkgInfo.packageName; + final InstallSourceInfo sourceInfo = mPm.getInstallSourceInfo(packageName); + final String existingUpdateOwner = sourceInfo.getUpdateOwnerPackageName(); + return getApplicationLabel(existingUpdateOwner); + } catch (NameNotFoundException e) { + return null; + } + } + + private CharSequence getApplicationLabel(String packageName) { + try { + final ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, + ApplicationInfoFlags.of(0)); + return mPm.getApplicationLabel(appInfo); + } catch (NameNotFoundException e) { + return null; + } + } + /** * Replace any dialog shown by the dialog with the one for the given {@link #createDialog(int)}. * @@ -344,6 +387,7 @@ public class PackageInstallerActivity extends AlertActivity { packageSource = Uri.fromFile(new File(resolvedBaseCodePath)); mOriginatingURI = null; mReferrerURI = null; + mPendingUserActionReason = info.getPendingUserActionReason(); } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(action)) { final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1 /* defaultValue */); @@ -358,11 +402,13 @@ public class PackageInstallerActivity extends AlertActivity { packageSource = info; mOriginatingURI = null; mReferrerURI = null; + mPendingUserActionReason = info.getPendingUserActionReason(); } else { mSessionId = -1; packageSource = intent.getData(); mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER); + mPendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE; } // if there's nothing to do, quietly slip into the ether diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 73c109994025..bf4ad758e465 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -88,6 +88,7 @@ dependencies { implementation "com.airbnb.android:lottie-compose:5.2.0" androidTestImplementation project(":testutils") + androidTestImplementation 'androidx.lifecycle:lifecycle-runtime-testing' androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.28.1" } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt new file mode 100644 index 000000000000..e91fa65401a4 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt @@ -0,0 +1,46 @@ +/* + * 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.settingslib.spa.framework.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver + +@Composable +fun LifecycleEffect( + onStart: () -> Unit = {}, + onStop: () -> Unit = {}, +) { + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + onStart() + } else if (event == Lifecycle.Event.ON_STOP) { + onStop() + } + } + + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt index 271443e86dac..73eae07a4ba9 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt @@ -18,55 +18,41 @@ package com.android.settingslib.spa.framework.util import android.os.Bundle import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.core.os.bundleOf -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import com.android.settingslib.spa.framework.common.LOG_DATA_DISPLAY_NAME import com.android.settingslib.spa.framework.common.LOG_DATA_SESSION_NAME import com.android.settingslib.spa.framework.common.LogCategory import com.android.settingslib.spa.framework.common.LogEvent +import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.LifecycleEffect import com.android.settingslib.spa.framework.compose.LocalNavController +import com.android.settingslib.spa.framework.compose.NavControllerWrapper @Composable internal fun SettingsPageProvider.PageEvent(arguments: Bundle? = null) { val page = remember(arguments) { createSettingsPage(arguments) } - val lifecycleOwner = LocalLifecycleOwner.current val navController = LocalNavController.current - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - val logPageEvent: (event: LogEvent) -> Unit = { - SpaEnvironmentFactory.instance.logger.event( - id = page.id, - event = it, - category = LogCategory.FRAMEWORK, - extraData = bundleOf( - LOG_DATA_DISPLAY_NAME to page.displayName, - LOG_DATA_SESSION_NAME to navController.sessionSourceName, - ).apply { - val normArguments = parameter.normalize(arguments) - if (normArguments != null) putAll(normArguments) - } - ) - } - if (event == Lifecycle.Event.ON_START) { - logPageEvent(LogEvent.PAGE_ENTER) - } else if (event == Lifecycle.Event.ON_STOP) { - logPageEvent(LogEvent.PAGE_LEAVE) - } - } - - // Add the observer to the lifecycle - lifecycleOwner.lifecycle.addObserver(observer) + LifecycleEffect( + onStart = { page.logPageEvent(LogEvent.PAGE_ENTER, navController) }, + onStop = { page.logPageEvent(LogEvent.PAGE_LEAVE, navController) }, + ) +} - // When the effect leaves the Composition, remove the observer - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) +private fun SettingsPage.logPageEvent(event: LogEvent, navController: NavControllerWrapper) { + SpaEnvironmentFactory.instance.logger.event( + id = id, + event = event, + category = LogCategory.FRAMEWORK, + extraData = bundleOf( + LOG_DATA_DISPLAY_NAME to displayName, + LOG_DATA_SESSION_NAME to navController.sessionSourceName, + ).apply { + val normArguments = parameter.normalize(arguments) + if (normArguments != null) putAll(normArguments) } - } -} + ) +}
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/tests/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp index f9e64aee1513..b4c67ccda6f2 100644 --- a/packages/SettingsLib/Spa/tests/Android.bp +++ b/packages/SettingsLib/Spa/tests/Android.bp @@ -31,6 +31,7 @@ android_test { "SpaLib", "SpaLibTestUtils", "androidx.compose.runtime_runtime", + "androidx.lifecycle_lifecycle-runtime-testing", "androidx.test.ext.junit", "androidx.test.runner", "mockito-target-minus-junit4", diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt new file mode 100644 index 000000000000..fe7baff43101 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt @@ -0,0 +1,62 @@ +/* + * 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.settingslib.spa.framework.compose + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.testing.TestLifecycleOwner +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LifecycleEffectTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun onStart_isCalled() { + var onStartIsCalled = false + composeTestRule.setContent { + LifecycleEffect(onStart = { onStartIsCalled = true }) + } + + assertThat(onStartIsCalled).isTrue() + } + + @Test + fun onStop_isCalled() { + var onStopIsCalled = false + val testLifecycleOwner = TestLifecycleOwner() + + composeTestRule.setContent { + CompositionLocalProvider(LocalLifecycleOwner provides testLifecycleOwner) { + LifecycleEffect(onStop = { onStopIsCalled = true }) + } + LaunchedEffect(Unit) { + testLifecycleOwner.currentState = Lifecycle.State.CREATED + } + } + + assertThat(onStopIsCalled).isTrue() + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml index 2efa10744bb3..d1dceb309b99 100644 --- a/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml +++ b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml @@ -15,4 +15,8 @@ limitations under the License. --> -<manifest package="com.android.settingslib.spaprivileged" /> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.spaprivileged"> +<uses-permission android:name="android.permission.MANAGE_USERS" /> +</manifest> + diff --git a/packages/SettingsLib/SpaPrivileged/res/values/strings.xml b/packages/SettingsLib/SpaPrivileged/res/values/strings.xml index 25dbe007bac7..e1e7649de5c1 100644 --- a/packages/SettingsLib/SpaPrivileged/res/values/strings.xml +++ b/packages/SettingsLib/SpaPrivileged/res/values/strings.xml @@ -27,4 +27,6 @@ <string name="app_permission_summary_not_allowed">Not allowed</string> <!-- Manage applications, version string displayed in app snippet --> <string name="version_text">version <xliff:g id="version_num">%1$s</xliff:g></string> + <!-- Label of an app on App Info page of Cloned Apps menu [CHAR LIMIT=40] --> + <string name="cloned_app_info_label"><xliff:g id="package_label">%1$s</xliff:g> clone</string> </resources> diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt index b2ea4a084e48..a2fb101e4e4c 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt @@ -22,11 +22,9 @@ import android.content.Intent import android.content.IntentFilter import android.os.UserHandle import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver +import com.android.settingslib.spa.framework.compose.LifecycleEffect /** * A [BroadcastReceiver] which registered when on start and unregistered when on stop. @@ -39,28 +37,22 @@ fun DisposableBroadcastReceiverAsUser( onReceive: (Intent) -> Unit, ) { val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner) { - val broadcastReceiver = object : BroadcastReceiver() { + val broadcastReceiver = remember { + object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { onReceive(intent) } } - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_START) { - context.registerReceiverAsUser( - broadcastReceiver, userHandle, intentFilter, null, null - ) - onStart() - } else if (event == Lifecycle.Event.ON_STOP) { - context.unregisterReceiver(broadcastReceiver) - } - } - - lifecycleOwner.lifecycle.addObserver(observer) - - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } } + LifecycleEffect( + onStart = { + context.registerReceiverAsUser( + broadcastReceiver, userHandle, intentFilter, null, null + ) + onStart() + }, + onStop = { + context.unregisterReceiver(broadcastReceiver) + }, + ) } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt index 90710db6388b..18b207337ad4 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt @@ -19,9 +19,11 @@ package com.android.settingslib.spaprivileged.model.app import android.content.Context import android.content.pm.ApplicationInfo import android.graphics.drawable.Drawable +import android.os.UserManager import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.produceState +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.android.settingslib.Utils import com.android.settingslib.spa.framework.compose.rememberContext @@ -36,12 +38,24 @@ interface AppRepository { fun loadLabel(app: ApplicationInfo): String @Composable - fun produceLabel(app: ApplicationInfo) = - produceState(initialValue = stringResource(R.string.summary_placeholder), app) { + fun produceLabel(app: ApplicationInfo, isClonedAppPage: Boolean = false): State<String> { + val context = LocalContext.current + return produceState(initialValue = stringResource(R.string.summary_placeholder), app) { withContext(Dispatchers.IO) { - value = loadLabel(app) + if (isClonedAppPage || isCloneApp(context, app)) { + value = context.getString(R.string.cloned_app_info_label, loadLabel(app)) + } else { + value = loadLabel(app) + } } } + } + + private fun isCloneApp(context: Context, app: ApplicationInfo): Boolean { + val userManager = context.getSystemService(UserManager::class.java)!! + val userInfo = userManager.getUserInfo(app.userId) + return userInfo != null && userInfo.isCloneProfile + } @Composable fun produceIcon(app: ApplicationInfo): State<Drawable?> diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt index 16ca70fe90b9..602df54ed3fb 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt @@ -43,7 +43,7 @@ import com.android.settingslib.spaprivileged.model.app.rememberAppRepository class AppInfoProvider(private val packageInfo: PackageInfo) { @Composable - fun AppInfo(displayVersion: Boolean = false) { + fun AppInfo(displayVersion: Boolean = false, isClonedAppPage: Boolean = false) { Column( modifier = Modifier .fillMaxWidth() @@ -57,7 +57,7 @@ class AppInfoProvider(private val packageInfo: PackageInfo) { Box(modifier = Modifier.padding(SettingsDimension.itemPaddingAround)) { AppIcon(app = app, size = SettingsDimension.appIconInfoSize) } - AppLabel(app) + AppLabel(app, isClonedAppPage) InstallType(app) if (displayVersion) AppVersion() } @@ -99,7 +99,7 @@ internal fun AppIcon(app: ApplicationInfo, size: Dp) { } @Composable -internal fun AppLabel(app: ApplicationInfo) { +internal fun AppLabel(app: ApplicationInfo, isClonedAppPage: Boolean = false) { val appRepository = rememberAppRepository() - SettingsTitle(title = appRepository.produceLabel(app), useMediumWeight = true) + SettingsTitle(title = appRepository.produceLabel(app, isClonedAppPage), useMediumWeight = true) } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 90874bbacb0b..06c34767b183 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -123,7 +123,7 @@ public class SecureSettings { Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW, Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, - Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, + Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED, Settings.Secure.ACTIVE_UNLOCK_ON_WAKE, Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT, Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 62f4c412506a..d72d4d51136e 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -178,7 +178,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, NON_NEGATIVE_INTEGER_VALIDATOR); - VALIDATORS.put(Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SFPS_PERFORMANT_AUTH_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_MEDIA_WHEN_BYPASSING, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FACE_UNLOCK_APP_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, BOOLEAN_VALIDATOR); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index d56300e6781a..d716b327f94d 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -170,6 +170,7 @@ <uses-permission android:name="android.permission.SET_ORIENTATION" /> <uses-permission android:name="android.permission.INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.INSTALL_PACKAGE_UPDATES" /> + <uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" /> <uses-permission android:name="android.permission.INSTALL_DPC_PACKAGES" /> <uses-permission android:name="com.android.permission.USE_INSTALLER_V2" /> <uses-permission android:name="android.permission.INSTALL_TEST_ONLY_PACKAGE" /> diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 4be1d30c8fe5..ecb88f68d46a 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -415,7 +415,6 @@ <service android:name=".screenshot.ScreenshotCrossProfileService" android:permission="com.android.systemui.permission.SELF" - android:process=":screenshot_cross_profile" android:exported="false" /> <service android:name=".screenrecord.RecordingService" diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp index a494f5e086ae..0b1a3e272d01 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp +++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp @@ -20,6 +20,17 @@ package { android_app { name: "AccessibilityMenu", + + static_libs: [ + "androidx.coordinatorlayout_coordinatorlayout", + "androidx.core_core", + "androidx.viewpager_viewpager", + ], + + uses_libs: [ + "org.apache.http.legacy", + ], + srcs: [ "src/**/*.java", ], diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/color/footer_icon_tint_color.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/color/footer_icon_tint_color.xml new file mode 100644 index 000000000000..c89e4c318805 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/color/footer_icon_tint_color.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:color="@color/footer_icon_disabled_color" /> <!-- disabled --> + <item android:color="@color/footer_icon_enabled_color" /> <!-- default --> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/a11ymenu_intro.png b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/a11ymenu_intro.png Binary files differnew file mode 100644 index 000000000000..6149ee4b2365 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/a11ymenu_intro.png diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_left.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_left.xml new file mode 100644 index 000000000000..5ff245d6a11e --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_left.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + + <item> + <ripple + android:color="@color/ripple_material_color"> + <item android:id="@android:id/mask"> + <color android:color="@color/overlay_bg_color"/> + </item> + </ripple> + </item> + +</layer-list>
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_right.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_right.xml new file mode 100644 index 000000000000..5ff245d6a11e --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/footer_button_background_right.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + + <item> + <ripple + android:color="@color/ripple_material_color"> + <item android:id="@android:id/mask"> + <color android:color="@color/overlay_bg_color"/> + </item> + </ripple> + </item> + +</layer-list>
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_a11y_menu_round.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_a11y_menu_round.xml new file mode 100644 index 000000000000..a2eaf95dcef0 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_a11y_menu_round.xml @@ -0,0 +1,20 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector android:height="108dp" android:viewportHeight="24" + android:viewportWidth="24" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#ffffff" android:fillType="evenOdd" android:pathData="M7.875,10.625C7.1188,10.625 6.5,11.2437 6.5,12C6.5,12.7562 7.1188,13.375 7.875,13.375C8.6313,13.375 9.25,12.7562 9.25,12C9.25,11.2437 8.6313,10.625 7.875,10.625ZM16.125,10.625C15.3687,10.625 14.75,11.2437 14.75,12C14.75,12.7562 15.3687,13.375 16.125,13.375C16.8813,13.375 17.5,12.7562 17.5,12C17.5,11.2437 16.8813,10.625 16.125,10.625ZM10.625,12C10.625,11.2437 11.2438,10.625 12,10.625C12.7563,10.625 13.375,11.2437 13.375,12C13.375,12.7562 12.7563,13.375 12,13.375C11.2438,13.375 10.625,12.7562 10.625,12Z"/> +</vector> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_add_32dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_add_32dp.xml new file mode 100644 index 000000000000..7e1262c2b4e7 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_add_32dp.xml @@ -0,0 +1,5 @@ +<vector android:height="32dp" android:tint="#FFFFFF" + android:viewportHeight="24.0" android:viewportWidth="24.0" + android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> +</vector> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_back_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_back_24dp.xml new file mode 100644 index 000000000000..f6af270095c3 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_back_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/footer_arrow_length" + android:height="@dimen/footer_arrow_length" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="@color/footer_icon_color" + android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/> +</vector> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_forward_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_forward_24dp.xml new file mode 100644 index 000000000000..2f7b632d6adc --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_arrow_forward_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/footer_arrow_length" + android:height="@dimen/footer_arrow_length" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="@color/footer_icon_color" + android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/> +</vector> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_menu.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_menu.xml new file mode 100644 index 000000000000..79e0e08d8899 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_menu.xml @@ -0,0 +1,33 @@ +<vector android:height="48dp" android:viewportHeight="192.0" + android:viewportWidth="192.0" android:width="48dp" + xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#34a853" android:pathData="M37.14,173.74l-28.53,-90a14.53,14.53 0,0 1,5 -15.63L87.15,11a14.21,14.21 0,0 1,17.61 0.12l73.81,58.94a14.53,14.53 0,0 1,4.8 15.57l-28.48,88.18A14.32,14.32 0,0 1,141.22 184H50.84A14.33,14.33 0,0 1,37.14 173.74Z"/> + <path android:pathData="M137.61,94.07l-17,17 -17,-17 -17,17L70.3,94.72l-17,17L125.66,184h15.56a14.32,14.32 0,0 0,13.67 -10.19l15.25,-47.21Z"> + <aapt:attr name="android:fillColor"> + <gradient android:endX="27152.64" + android:endY="32745.600000000002" + android:startX="20910.72" + android:startY="21934.079999999998" android:type="linear"> + <item android:color="#33263238" android:offset="0.0"/> + <item android:color="#11205432" android:offset="0.47"/> + <item android:color="#051E6130" android:offset="1.0"/> + </gradient> + </aapt:attr> + </path> + <path android:fillAlpha="0.2" android:fillColor="#263238" android:pathData="M50.14,100.11a12,12 0,1 1,11.39 15.77,11.72 11.72,0 0,1 -5,-1.1l-3.41,-3.4ZM129.4,91.88a12,12 0,1 1,-12 12A12,12 0,0 1,129.4 91.88ZM95.4,91.88a12,12 0,1 1,-12 12A12,12 0,0 1,95.42 91.88Z"/> + <path android:fillColor="#fff" + android:pathData="M61.53,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,61.53 90.88ZM129.41,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,129.41 90.88ZM95.41,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,95.42 90.88Z" + android:strokeAlpha="0" android:strokeColor="#000" + android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.5"/> + <path android:fillAlpha="0.2" android:fillColor="#263238" android:pathData="M184,80.91a14.33,14.33 0,0 1,-0.63 4.7l-28.48,88.18A14.33,14.33 0,0 1,141.21 184H50.84a14.33,14.33 0,0 1,-13.7 -10.26l-28.53,-90A14.49,14.49 0,0 1,8 79.11a14.3,14.3 0,0 0,0.61 3.64l28.53,90A14.33,14.33 0,0 0,50.84 183h90.37a14.33,14.33 0,0 0,13.67 -10.19l28.48,-88.18A14.79,14.79 0,0 0,184 80.91Z"/> + <path android:fillAlpha="0.2" android:fillColor="#fff" android:pathData="M184,81.89A14.46,14.46 0,0 0,178.57 71L104.76,12.1A14.21,14.21 0,0 0,87.15 12L13.58,69.12A14.5,14.5 0,0 0,8 80.09a14.5,14.5 0,0 1,5.57 -12L87.15,11a14.21,14.21 0,0 1,17.61 0.12L178.57,70A14.48,14.48 0,0 1,184 81.89Z"/> + <path android:pathData="M37.14,173.74l-28.53,-90a14.53,14.53 0,0 1,5 -15.63L87.15,11a14.21,14.21 0,0 1,17.61 0.12l73.81,58.94a14.53,14.53 0,0 1,4.8 15.57l-28.48,88.18A14.32,14.32 0,0 1,141.22 184H50.84A14.33,14.33 0,0 1,37.14 173.74Z"/> + <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@color/colorAccessibilityMenuIcon" /> + <foreground> + <inset + android:drawable="@drawable/ic_a11y_menu_round" + android:inset="21.88%" /> + </foreground> + </adaptive-icon> +</vector> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_assistant_32dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_assistant_32dp.xml new file mode 100644 index 000000000000..ebeebf81eedc --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_assistant_32dp.xml @@ -0,0 +1,8 @@ +<vector android:height="32dp" + android:viewportHeight="192.0" android:viewportWidth="192.0" + android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#34A853" android:pathData="M172,60m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0"/> + <path android:fillColor="#EA4335" android:pathData="M136,88m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/> + <path android:fillColor="#FBBC05" android:pathData="M136,148m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"/> + <path android:fillColor="#4285F4" android:pathData="M56,64m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0"/> +</vector> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_0deg.9.png b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_0deg.9.png Binary files differnew file mode 100644 index 000000000000..b0d169642461 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_0deg.9.png diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_270deg.9.png b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_270deg.9.png Binary files differnew file mode 100644 index 000000000000..b777ffee62fd --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_270deg.9.png diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_90deg.9.png b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_90deg.9.png Binary files differnew file mode 100644 index 000000000000..998bd9037462 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/shadow_90deg.9.png diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/view_background.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/view_background.xml new file mode 100644 index 000000000000..c1f76f370c52 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/view_background.xml @@ -0,0 +1,5 @@ +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/overlay_bg_color" /> +</shape> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/footerlayout_switch_page.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/footerlayout_switch_page.xml new file mode 100644 index 000000000000..658c03bd388f --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/footerlayout_switch_page.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/footerlayout" + android:layout_width="match_parent" + android:layout_height="@dimen/grid_item_btn_view_height" + android:layout_alignParentBottom="true" + android:layout_gravity="bottom" + android:layoutDirection="ltr" + android:orientation="vertical" + android:visibility="gone"> + + <View + android:id="@+id/top_listDivider" + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?android:attr/listDivider"/> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="horizontal"> + + <ImageButton + android:id="@+id/menu_prev_button" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="@drawable/footer_button_background_left" + android:contentDescription="@string/previous_button_content_description" + android:scaleType="centerInside" + android:src="@drawable/ic_arrow_back_24dp" + android:tint="@color/footer_icon_tint_color"/> + + <View + android:layout_width="1dp" + android:layout_height="match_parent" + android:background="?android:attr/listDivider"/> + + <ImageButton + android:id="@+id/menu_next_button" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="@drawable/footer_button_background_right" + android:contentDescription="@string/next_button_content_description" + android:scaleType="centerInside" + android:src="@drawable/ic_arrow_forward_24dp" + android:tint="@color/footer_icon_tint_color"/> + + </LinearLayout> + + <View + android:id="@+id/bottom_listDivider" + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?android:attr/listDivider"/> + +</LinearLayout> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml new file mode 100644 index 000000000000..39e5a8c6876b --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="@dimen/grid_item_padding" + android:paddingBottom="@dimen/grid_item_padding" + android:gravity="center"> + + <ImageButton + android:id="@+id/shortcutIconBtn" + android:layout_width="@dimen/image_button_width" + android:layout_height="@dimen/image_button_height" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:scaleType="fitCenter"></ImageButton> + +<TextView + android:id="@+id/shortcutLabel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/grid_item_text_view_margin_top" + android:layout_below="@+id/shortcutIconBtn" + android:layout_centerHorizontal="true" + android:ellipsize="end" + android:gravity="center_horizontal" + android:importantForAccessibility="no" + android:lines="2" + android:textSize="@dimen/label_text_size" + android:textAlignment="center" + android:textAppearance="@android:style/TextAppearance.DeviceDefault.Widget.Button"/> + +</RelativeLayout> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_view.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_view.xml new file mode 100644 index 000000000000..c198443415dd --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_view.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<GridView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/gridview" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:horizontalSpacing="@dimen/a11ymenu_grid_layout_margin" + android:listSelector="@android:color/transparent" + android:numColumns="3" + android:overScrollMode="never" + android:stretchMode="columnWidth"> +</GridView> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/paged_menu.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/paged_menu.xml new file mode 100644 index 000000000000..28a633e5d17a --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/paged_menu.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/row_width" + android:layout_height="match_parent" + android:id="@+id/coordinatorLayout" + android:background="@drawable/view_background" + > + <LinearLayout + android:layout_width="@dimen/row_width" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <androidx.viewpager.widget.ViewPager + android:id="@+id/view_pager" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="@dimen/table_margin_top" + android:paddingBottom="@dimen/a11ymenu_layout_margin" + android:paddingLeft="@dimen/a11ymenu_layout_margin" + android:paddingRight="@dimen/a11ymenu_layout_margin" + android:layout_gravity="center" + android:gravity="center" + /> + + <include layout="@layout/footerlayout_switch_page"/> + </LinearLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values-land/dimens.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values-land/dimens.xml new file mode 100644 index 000000000000..69f09343b1d1 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values-land/dimens.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="table_margin_top">0dp</dimen> + <dimen name="row_width">388dp</dimen> + <dimen name="image_button_height">45dp</dimen> + <dimen name="image_button_width">45dp</dimen> + <dimen name="image_button_marginBottom">1dp</dimen> + + <!-- dimens for gridview layout. --> + <dimen name="grid_item_padding">4dp</dimen> + +</resources> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/colors.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/colors.xml new file mode 100644 index 000000000000..33c0cca5131e --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/colors.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> + +<resources> + <color name="power_color">#dadce0</color> + <color name="quick_settings_color">#78d9ec</color> + <color name="a11y_settings_color">#d9affe</color> + <color name="recent_apps_color">#f0a5dd</color> + <color name="lockscreen_color">#85e4a0</color> + <color name="volume_color">#7ae3d4</color> + <color name="notifications_color">#f496ac</color> + <color name="screenshot_color">#adcbff</color> + <color name="assistant_color">#F1F3F4</color> + <color name="brightness_color">#fdd663</color> + + <color name="ripple_material_color">#10FFFFFF</color> + + <color name="overlay_bg_color">#313235</color> + <color name="footer_icon_color">#E8EAED</color> + <color name="footer_icon_enabled_color">#E8EAED</color> + <color name="footer_icon_disabled_color">#5F6368</color> + <color name="colorControlNormal">#202124</color> + +</resources> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml new file mode 100644 index 000000000000..81b3152375ff --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <!--Adds the theme to support SnackBar component and user configurable theme. --> + <style name="ServiceTheme" parent="android:Theme.DeviceDefault.DayNight"> + <item name="android:colorControlNormal">@color/colorControlNormal</item> + </style> + +</resources> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/bool.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/bool.xml new file mode 100644 index 000000000000..2f9d6b5c8a19 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/bool.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="isAtLeastP">true</bool> + +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/colors.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/colors.xml new file mode 100644 index 000000000000..36d1fc1263c4 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/colors.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> + +<resources> + <color name="power_color">#757575</color> + <color name="quick_settings_color">#2196F3</color> + <color name="a11y_settings_color">#5806C9</color> + <color name="recent_apps_color">#AD2EC6</color> + <color name="lockscreen_color">#0F9D58</color> + <color name="volume_color">#01A2A0</color> + <color name="notifications_color">#F15B8D</color> + <color name="screenshot_color">#26459C</color> + <color name="assistant_color">#F1F3F4</color> + <color name="brightness_color">#E59810</color> + <color name="colorAccent">#1a73e8</color> + + <color name="ripple_material_color">#1f000000</color> + + <color name="overlay_bg_color">@android:color/white</color> + <color name="footer_icon_color">@android:color/black</color> + <color name="footer_icon_enabled_color">@android:color/black</color> + <color name="footer_icon_disabled_color">#ddd</color> + <color name="colorControlNormal">@android:color/white</color> + + <color name="colorAccessibilityMenuIcon">#3AA757</color> +</resources> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/dimens.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/dimens.xml new file mode 100644 index 000000000000..7ed18977cd54 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/dimens.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- the curve radius for the background of the complete layout --> + <dimen name="table_margin_top">22dp</dimen> + <dimen name="row_width">@dimen/custom_match_parent</dimen> + <dimen name="image_button_height">60dp</dimen> + <dimen name="image_button_width">60dp</dimen> + <dimen name="image_button_marginBottom">2dp</dimen> + <dimen name="a11ymenu_layout_margin">4dp</dimen> + <dimen name="custom_match_parent">-1px</dimen> + + <!-- dimens for gridview layout. --> + <dimen name="grid_item_text_view_margin_top">2dp</dimen> + <dimen name="grid_item_padding">10dp</dimen> + <dimen name="grid_item_btn_view_height">48dp</dimen> + <dimen name="a11ymenu_grid_layout_margin">8dp</dimen> + + <!-- dimens for a11y menu footer layout. --> + <dimen name="footer_arrow_length">24dp</dimen> + + <!-- text size for shortcut label when large button settings in on. --> + <dimen name="large_label_text_size">18sp</dimen> + <dimen name="label_text_size">14sp</dimen> + +</resources> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/donottranslate.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/donottranslate.xml new file mode 100644 index 000000000000..0c25ec4353a5 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/donottranslate.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- user customized shortcuts preference --> + <string name="pref_user_shortcuts">accessibility_menu_user_shortcuts</string> + <!-- key for user customized shortcuts --> + <string name="pref_user_shortcuts_key">pref_user_shortcuts_key</string> + <!-- value for empty shortcut --> + <string name="pref_user_shortcuts_value_empty">[]</string> + <!-- empty string for shortcut label --> + <string name="empty_content"></string> + + <string name="pref_large_buttons">pref_large_buttons</string> + + <!-- key for Help&feedback settings [CHAR_LIMIT=NONE] --> + <string name="pref_help">pref_help</string> + +</resources> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/strings.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/strings.xml new file mode 100644 index 000000000000..30fd0173ff3f --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/strings.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="utf-8"?> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- String defining the service name --> + <string name="accessibility_menu_service_name">Accessibility Menu</string> + <!-- Accessibility Menu detail intro. [CHAR_LIMIT=NONE] --> + <string name="accessibility_menu_intro"> + The Accessibility Menu provides a large on-screen menu to control your device. You can lock your device, control volume and brightness, take screenshots, and more. + </string> + <!-- String defining the label for the assistant button --> + <string name="assistant_label">Assistant</string> + <!-- String defining utterance for the assistant button for screen readers --> + <string name="assistant_utterance">Google Assistant</string> + <!-- String defining the label for the accessibility settings button --> + <string name="a11y_settings_label">Accessibility Settings</string> + <!-- String defining the label for the volume button --> + <string name="volume_label">Volume</string> + <!-- String defining utterance for the volume button for screen readers --> + <string name="volume_utterance">Volume controls</string> + <!-- String defining the label for the power button --> + <string name="power_label">Power</string> + <!-- String defining utterance for the power button for screen readers --> + <string name="power_utterance">Power options</string> + <!-- String defining the label for the recent apps button --> + <string name="recent_apps_label">Recent apps</string> + <!-- String defining the label for the lockscreen button --> + <string name="lockscreen_label">Lock screen</string> + <!-- String defining the label for the quick settings button --> + <string name="quick_settings_label">Quick Settings</string> + <!-- String defining the label for the notifications button --> + <string name="notifications_label">Notifications</string> + <!-- String defining the label for the screenshot button --> + <string name="screenshot_label">Screenshot</string> + <!-- String defining the utterance for the screenshot button for screen readers --> + <string name="screenshot_utterance">Take screenshot</string> + <!-- String defining the label for the volume up/down button --> + <string name="volume_up_label">Volume up</string> + <string name="volume_down_label">Volume down</string> + <!-- String defining the label for the brightness up/down button --> + <string name="brightness_up_label">Brightness up</string> + <string name="brightness_down_label">Brightness down</string> + <!-- String defining the content description for the footer previous/next button --> + <string name="previous_button_content_description">Go to previous screen</string> + <string name="next_button_content_description">Go to next screen</string> + + <string name="accessibility_menu_description"> + The Accessibility Menu provides a large on-screen menu to control your device. You can lock your device, control volume and brightness, take screenshots, and more. + </string> + <!-- Short summary of app that appears as subtext on the service preference in Settings --> + <string name="accessibility_menu_summary">Control device via large menu</string> + + <!-- TODO(b/113371047): string need to be reviewed --> + <!-- String defining the settings name --> + <string name="accessibility_menu_settings_name">Accessibility Menu Settings</string> + + <!-- String defining the title of Large button setting --> + <string name="accessibility_menu_large_buttons_title">Large buttons</string> + <!-- String defining the summary of Large button setting --> + <string name="accessibility_menu_large_buttons_summary">Increase size of Accessibility Menu Buttons</string> + <!-- String defining the title of the preference to show help and feedback menu [CHAR LIMIT=40] --> + <string name="pref_help_and_feedback_title">Help & feedback</string> + <!-- String defining the title of the preference to show help menu [CHAR LIMIT=40] --> + <string name="pref_help_title">Help</string> + + <!-- The percentage of the brightness, and double "%" is required to represent the symbol "%" --> + <string name="brightness_percentage_label">Brightness <xliff:g id="percentage">%1$s</xliff:g> %%</string> + <!-- The percentage of the music volume, and double "%" is required to represent the symbol "%" --> + <string name="music_volume_percentage_label">Music volume <xliff:g id="percentage">%1$s</xliff:g> %%</string> + + <!-- The label of a settings item that displays legal information about the licenses used in this app. [CHAR LIMIT=NONE] --> + <string name="pref_item_licenses">Open Source Licenses</string> + +</resources> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml new file mode 100644 index 000000000000..a2cf26730960 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> + +<resources> + <!--The theme is for preference CollapsingToolbarBaseActivity settings--> + <style name="AccessibilityMenuSettings" parent="android:Theme.DeviceDefault.Light" /> + + <!--Adds the theme to support SnackBar component and user configurable theme. --> + <style name="ServiceTheme" parent="android:Theme.DeviceDefault.Light"> + <item name="android:colorControlNormal">@color/colorControlNormal</item> + </style> + + <!--The basic theme for service and test case only--> + <style name="A11yMenuBaseTheme" parent="android:Theme.DeviceDefault.Light"> + <item name="android:windowActionBar">false</item> + </style> +</resources> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml index 96882d335d4b..3dbbb1a658c9 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml @@ -13,4 +13,12 @@ See the License for the specific language governing permissions and limitations under the License. --> -<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"/>
\ No newline at end of file +<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" + android:accessibilityFeedbackType="feedbackGeneric" + android:accessibilityFlags="flagRequestAccessibilityButton|flagRequestFilterKeyEvents" + android:canRequestFilterKeyEvents="true" + android:summary="@string/accessibility_menu_summary" + android:intro="@string/accessibility_menu_intro" + android:animatedImageDrawable="@drawable/a11ymenu_intro" + android:isAccessibilityTool="true" +/>
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java index 8b759004f657..5c4fdcc0e5d8 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java @@ -17,16 +17,39 @@ package com.android.systemui.accessibility.accessibilitymenu; import android.accessibilityservice.AccessibilityService; +import android.view.MotionEvent; +import android.view.View; import android.view.accessibility.AccessibilityEvent; +import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuOverlayLayout; + /** @hide */ -public class AccessibilityMenuService extends AccessibilityService { +public class AccessibilityMenuService extends AccessibilityService implements View.OnTouchListener { + private static final String TAG = "A11yMenuService"; + + private A11yMenuOverlayLayout mA11yMenuLayout; + + @Override + public void onCreate() { + super.onCreate(); + } @Override - public void onAccessibilityEvent(AccessibilityEvent event) { + protected void onServiceConnected() { + mA11yMenuLayout = new A11yMenuOverlayLayout(this); + super.onServiceConnected(); + mA11yMenuLayout.toggleVisibility(); } @Override + public void onAccessibilityEvent(AccessibilityEvent event) {} + + @Override public void onInterrupt() { } + + @Override + public boolean onTouch(View v, MotionEvent event) { + return false; + } } diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java new file mode 100644 index 000000000000..fa42e61899fd --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java @@ -0,0 +1,91 @@ +/* + * 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.accessibility.accessibilitymenu.model; + +import com.android.systemui.accessibility.accessibilitymenu.R; + +/** Provides a data structure for a11y menu shortcuts. */ +public class A11yMenuShortcut { + + public enum ShortcutId { + UNSPECIFIED_ID_VALUE, + ID_ASSISTANT_VALUE, + ID_A11YSETTING_VALUE, + ID_POWER_VALUE, + ID_VOLUME_DOWN_VALUE, + ID_VOLUME_UP_VALUE, + ID_RECENT_VALUE, + ID_BRIGHTNESS_DOWN_VALUE, + ID_BRIGHTNESS_UP_VALUE, + ID_LOCKSCREEN_VALUE, + ID_QUICKSETTING_VALUE, + ID_NOTIFICATION_VALUE, + ID_SCREENSHOT_VALUE + } + + private static final String TAG = "A11yMenuShortcut"; + + /** Shortcut id used to identify. */ + private int mShortcutId = ShortcutId.UNSPECIFIED_ID_VALUE.ordinal(); + + // Resource IDs of shortcut button and label. + public int imageSrc; + public int imageColor; + public int imgContentDescription; + public int labelText; + + public A11yMenuShortcut(int id) { + setId(id); + } + + /** + * Sets Id to shortcut, checks the value first and updates shortcut resources. It will set id to + * + * @param id id set to shortcut + */ + public void setId(int id) { + mShortcutId = id; + + // TODO(jonesriley) load the proper resources based on id + imageSrc = R.drawable.ic_logo_assistant_32dp; + imageColor = android.R.color.darker_gray; + imgContentDescription = R.string.empty_content; + labelText = R.string.empty_content; + } + + public int getId() { + return mShortcutId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof A11yMenuShortcut)) { + return false; + } + + A11yMenuShortcut targetObject = (A11yMenuShortcut) o; + + return mShortcutId == targetObject.mShortcutId; + } + + @Override + public int hashCode() { + return mShortcutId; + } +} diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/utils/ShortcutDrawableUtils.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/utils/ShortcutDrawableUtils.java new file mode 100644 index 000000000000..28ba4b54107f --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/utils/ShortcutDrawableUtils.java @@ -0,0 +1,98 @@ +/* + * 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.accessibility.accessibilitymenu.utils; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.RippleDrawable; + +import com.android.systemui.accessibility.accessibilitymenu.R; + +/** Creates background drawable for a11y menu shortcut. */ +public class ShortcutDrawableUtils { + + /** + * To make the circular background of shortcut icons have higher resolution. The higher value of + * LENGTH is, the higher resolution of the circular background are. + */ + private static final int LENGTH = 480; + + private static final int RADIUS = LENGTH / 2; + private static final int COORDINATE = LENGTH / 2; + private static final int RIPPLE_COLOR_ID = R.color.ripple_material_color; + + private final Context mContext; + private final ColorStateList mRippleColorStateList; + + // Placeholder of drawable to prevent NullPointerException + private final ColorDrawable mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT); + + public ShortcutDrawableUtils(Context context) { + this.mContext = context; + + int rippleColor = context.getColor(RIPPLE_COLOR_ID); + mRippleColorStateList = ColorStateList.valueOf(rippleColor); + } + + /** + * Creates a circular drawable in specific color for shortcut. + * + * @param colorResId color resource ID + * @return drawable circular drawable + */ + public Drawable createCircularDrawable(int colorResId) { + Bitmap output = Bitmap.createBitmap(LENGTH, LENGTH, Config.ARGB_8888); + Canvas canvas = new Canvas(output); + int color = mContext.getColor(colorResId); + Paint paint = new Paint(); + paint.setColor(color); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStyle(Style.FILL); + canvas.drawCircle(COORDINATE, COORDINATE, RADIUS, paint); + + BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(), output); + return drawable; + } + + /** + * Creates an adaptive icon drawable in specific color for shortcut. + * + * @param colorResId color resource ID + * @return drawable for adaptive icon + */ + public Drawable createAdaptiveIconDrawable(int colorResId) { + Drawable circleLayer = createCircularDrawable(colorResId); + RippleDrawable rippleLayer = new RippleDrawable(mRippleColorStateList, null, null); + + AdaptiveIconDrawable adaptiveIconDrawable = + new AdaptiveIconDrawable(circleLayer, mTransparentDrawable); + + Drawable[] layers = {adaptiveIconDrawable, rippleLayer}; + LayerDrawable drawable = new LayerDrawable(layers); + return drawable; + } +} diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java new file mode 100644 index 000000000000..e3401a9a7915 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.accessibilitymenu.view; + +import android.graphics.Rect; +import android.view.LayoutInflater; +import android.view.TouchDelegate; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.TextView; + +import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; +import com.android.systemui.accessibility.accessibilitymenu.R; +import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; +import com.android.systemui.accessibility.accessibilitymenu.utils.ShortcutDrawableUtils; + +import java.util.List; + +/** GridView Adapter for a11y menu overlay. */ +public class A11yMenuAdapter extends BaseAdapter { + + // The large scale of shortcut icon and label. + private static final float LARGE_BUTTON_SCALE = 1.5f; + private final int mLargeTextSize; + + private final AccessibilityMenuService mService; + private final LayoutInflater mInflater; + private final List<A11yMenuShortcut> mShortcutDataList; + private final ShortcutDrawableUtils mShortcutDrawableUtils; + + public A11yMenuAdapter( + AccessibilityMenuService service, List<A11yMenuShortcut> shortcutDataList) { + this.mService = service; + this.mShortcutDataList = shortcutDataList; + mInflater = LayoutInflater.from(service); + + mShortcutDrawableUtils = new ShortcutDrawableUtils(service); + + mLargeTextSize = + service.getResources().getDimensionPixelOffset(R.dimen.large_label_text_size); + } + + @Override + public int getCount() { + return mShortcutDataList.size(); + } + + @Override + public Object getItem(int position) { + return mShortcutDataList.get(position); + } + + @Override + public long getItemId(int position) { + return mShortcutDataList.get(position).getId(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + convertView = mInflater.inflate(R.layout.grid_item, null); + + A11yMenuShortcut shortcutItem = (A11yMenuShortcut) getItem(position); + // Sets shortcut icon and label resource. + configureShortcutView(convertView, shortcutItem); + + expandIconTouchArea(convertView); + setActionForMenuShortcut(convertView); + return convertView; + } + + /** + * Expand shortcut icon touch area to the border of grid item. + * The height is from the top of icon to the bottom of label. + * The width is from the left border of grid item to the right border of grid item. + */ + private void expandIconTouchArea(View convertView) { + ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn); + TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel); + + shortcutIconButton.post( + () -> { + Rect iconHitRect = new Rect(); + shortcutIconButton.getHitRect(iconHitRect); + Rect labelHitRect = new Rect(); + shortcutLabel.getHitRect(labelHitRect); + + final int widthAdjustment = iconHitRect.left; + iconHitRect.left = 0; + iconHitRect.right += widthAdjustment; + iconHitRect.top = 0; + iconHitRect.bottom = labelHitRect.bottom; + ((View) shortcutIconButton.getParent()) + .setTouchDelegate(new TouchDelegate(iconHitRect, shortcutIconButton)); + }); + } + + private void setActionForMenuShortcut(View convertView) { + ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn); + + shortcutIconButton.setOnClickListener( + (View v) -> { + // Handles shortcut click event by AccessibilityMenuService. + // service.handleClick(v); + }); + } + + private void configureShortcutView(View convertView, A11yMenuShortcut shortcutItem) { + ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn); + TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel); + + // TODO: Enlarge shortcut icon & label when large button setting is on. + + if (shortcutItem.getId() == A11yMenuShortcut.ShortcutId.UNSPECIFIED_ID_VALUE.ordinal()) { + // Sets empty shortcut icon and label when the shortcut is ADD_ITEM. + shortcutIconButton.setImageResource(android.R.color.transparent); + shortcutIconButton.setBackground(null); + } else { + // Sets shortcut ID as tagId, to handle menu item click in AccessibilityMenuService. + shortcutIconButton.setTag(shortcutItem.getId()); + shortcutIconButton.setContentDescription( + mService.getString(shortcutItem.imgContentDescription)); + shortcutLabel.setText(shortcutItem.labelText); + shortcutIconButton.setImageResource(shortcutItem.imageSrc); + + shortcutIconButton.setBackground( + mShortcutDrawableUtils.createAdaptiveIconDrawable(shortcutItem.imageColor)); + } + } +} diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuFooter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuFooter.java new file mode 100644 index 000000000000..20c63df885d2 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuFooter.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.accessibilitymenu.view; + +import android.graphics.Rect; +import android.view.TouchDelegate; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.widget.ImageButton; + +import androidx.annotation.Nullable; + +import com.android.systemui.accessibility.accessibilitymenu.R; + +/** + * This class is for Accessibility menu footer layout. Handles switching between a11y menu pages. + */ +public class A11yMenuFooter { + + /** Provides an interface for footer of a11yMenu. */ + public interface A11yMenuFooterCallBack { + + /** Calls back when user clicks the left button. */ + void onLeftButtonClicked(); + + /** Calls back when user clicks the right button. */ + void onRightButtonClicked(); + } + + private final FooterButtonClickListener mFooterButtonClickListener; + + private ImageButton mPreviousPageBtn; + private ImageButton mNextPageBtn; + private View mTopListDivider; + private View mBottomListDivider; + private final A11yMenuFooterCallBack mCallBack; + + public A11yMenuFooter(ViewGroup menuLayout, A11yMenuFooterCallBack callBack) { + this.mCallBack = callBack; + mFooterButtonClickListener = new FooterButtonClickListener(); + configureFooterLayout(menuLayout); + } + + public @Nullable ImageButton getPreviousPageBtn() { + return mPreviousPageBtn; + } + + public @Nullable ImageButton getNextPageBtn() { + return mNextPageBtn; + } + + private void configureFooterLayout(ViewGroup menuLayout) { + ViewGroup footerContainer = menuLayout.findViewById(R.id.footerlayout); + footerContainer.setVisibility(View.VISIBLE); + + mPreviousPageBtn = menuLayout.findViewById(R.id.menu_prev_button); + mNextPageBtn = menuLayout.findViewById(R.id.menu_next_button); + mTopListDivider = menuLayout.findViewById(R.id.top_listDivider); + mBottomListDivider = menuLayout.findViewById(R.id.bottom_listDivider); + + // Registers listeners for footer buttons. + setListener(mPreviousPageBtn); + setListener(mNextPageBtn); + + menuLayout + .getViewTreeObserver() + .addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + menuLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this); + expandBtnTouchArea(mPreviousPageBtn, menuLayout); + expandBtnTouchArea(mNextPageBtn, (View) mNextPageBtn.getParent()); + } + }); + } + + private void expandBtnTouchArea(ImageButton btn, View btnParent) { + Rect btnRect = new Rect(); + btn.getHitRect(btnRect); + btnRect.top -= getHitRectHeight(mTopListDivider); + btnRect.bottom += getHitRectHeight(mBottomListDivider); + btnParent.setTouchDelegate(new TouchDelegate(btnRect, btn)); + } + + private static int getHitRectHeight(View listDivider) { + Rect hitRect = new Rect(); + listDivider.getHitRect(hitRect); + return hitRect.height(); + } + + private void setListener(@Nullable View view) { + if (view != null) { + view.setOnClickListener(mFooterButtonClickListener); + } + } + + /** Handles click event for footer buttons. */ + private class FooterButtonClickListener implements OnClickListener { + @Override + public void onClick(View view) { + if (view.getId() == R.id.menu_prev_button) { + mCallBack.onLeftButtonClicked(); + } else if (view.getId() == R.id.menu_next_button) { + mCallBack.onRightButtonClicked(); + } + } + } +} diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java new file mode 100644 index 000000000000..740bc8a412af --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.accessibilitymenu.view; + +import static java.lang.Math.max; + +import android.content.res.Configuration; +import android.graphics.Insets; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.view.Display; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; +import android.widget.FrameLayout; +import android.widget.Toast; + +import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; +import com.android.systemui.accessibility.accessibilitymenu.R; +import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; + +import java.util.ArrayList; +import java.util.List; + +/** + * Provides functionality for Accessibility menu layout in a11y menu overlay. There are functions to + * configure or update Accessibility menu layout when orientation and display size changed, and + * functions to toggle menu visibility when button clicked or screen off. + */ +public class A11yMenuOverlayLayout { + + /** Predefined default shortcuts when large button setting is off. */ + private static final int[] SHORTCUT_LIST_DEFAULT = { + A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(), + A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal() + }; + + private final AccessibilityMenuService mService; + private final WindowManager mWindowManager; + private ViewGroup mLayout; + private WindowManager.LayoutParams mLayoutParameter; + private A11yMenuViewPager mA11yMenuViewPager; + + public A11yMenuOverlayLayout(AccessibilityMenuService service) { + mService = service; + mWindowManager = mService.getSystemService(WindowManager.class); + configureLayout(); + } + + /** Creates Accessibility menu layout and configure layout parameters. */ + public View configureLayout() { + return configureLayout(A11yMenuViewPager.DEFAULT_PAGE_INDEX); + } + + // TODO(b/78292783): Find a better way to inflate layout in the test. + /** + * Creates Accessibility menu layout, configure layout parameters and apply index to ViewPager. + * + * @param pageIndex the index of the ViewPager to show. + */ + public View configureLayout(int pageIndex) { + + int lastVisibilityState = View.GONE; + if (mLayout != null) { + lastVisibilityState = mLayout.getVisibility(); + mWindowManager.removeView(mLayout); + mLayout = null; + } + + if (mLayoutParameter == null) { + initLayoutParams(); + } + + mLayout = new FrameLayout(mService); + updateLayoutPosition(); + inflateLayoutAndSetOnTouchListener(mLayout); + mA11yMenuViewPager = new A11yMenuViewPager(mService); + mA11yMenuViewPager.configureViewPagerAndFooter(mLayout, createShortcutList(), pageIndex); + mWindowManager.addView(mLayout, mLayoutParameter); + mLayout.setVisibility(lastVisibilityState); + + return mLayout; + } + + /** Updates view layout with new layout parameters only. */ + public void updateViewLayout() { + if (mLayout == null || mLayoutParameter == null) { + return; + } + updateLayoutPosition(); + mWindowManager.updateViewLayout(mLayout, mLayoutParameter); + } + + private void initLayoutParams() { + mLayoutParameter = new WindowManager.LayoutParams(); + mLayoutParameter.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; + mLayoutParameter.format = PixelFormat.TRANSLUCENT; + mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; + mLayoutParameter.setTitle(mService.getString(R.string.accessibility_menu_service_name)); + } + + private void inflateLayoutAndSetOnTouchListener(ViewGroup view) { + LayoutInflater inflater = LayoutInflater.from(mService); + inflater.inflate(R.layout.paged_menu, view); + view.setOnTouchListener(mService); + } + + /** + * Loads shortcut data from default shortcut ID array. + * + * @return A list of default shortcuts + */ + private List<A11yMenuShortcut> createShortcutList() { + List<A11yMenuShortcut> shortcutList = new ArrayList<>(); + for (int shortcutId : SHORTCUT_LIST_DEFAULT) { + shortcutList.add(new A11yMenuShortcut(shortcutId)); + } + return shortcutList; + } + + /** Updates a11y menu layout position by configuring layout params. */ + private void updateLayoutPosition() { + Display display = mLayout.getDisplay(); + final int orientation = mService.getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + switch (display.getRotation()) { + case Surface.ROTATION_90: + case Surface.ROTATION_180: + mLayoutParameter.gravity = + Gravity.END | Gravity.BOTTOM + | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; + mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT; + mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT; + mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; + mLayout.setBackgroundResource(R.drawable.shadow_90deg); + break; + case Surface.ROTATION_0: + case Surface.ROTATION_270: + mLayoutParameter.gravity = + Gravity.START | Gravity.BOTTOM + | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; + mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT; + mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT; + mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; + mLayout.setBackgroundResource(R.drawable.shadow_270deg); + break; + default: + break; + } + } else { + mLayoutParameter.gravity = Gravity.BOTTOM; + mLayoutParameter.width = WindowManager.LayoutParams.MATCH_PARENT; + mLayoutParameter.height = WindowManager.LayoutParams.WRAP_CONTENT; + mLayout.setBackgroundResource(R.drawable.shadow_0deg); + } + + // Adjusts the y position of a11y menu layout to make the layout not to overlap bottom + // navigation bar window. + updateLayoutByWindowInsetsIfNeeded(); + mLayout.setOnApplyWindowInsetsListener( + (view, insets) -> { + if (updateLayoutByWindowInsetsIfNeeded()) { + mWindowManager.updateViewLayout(mLayout, mLayoutParameter); + } + return view.onApplyWindowInsets(insets); + }); + } + + /** + * Returns {@code true} if the a11y menu layout params + * should be updated by {@link WindowManager} immediately due to window insets change. + * This method adjusts the layout position and size to + * make a11y menu not to overlap navigation bar window. + */ + private boolean updateLayoutByWindowInsetsIfNeeded() { + boolean shouldUpdateLayout = false; + WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); + Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility( + WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); + int xOffset = max(windowInsets.left, windowInsets.right); + int yOffset = windowInsets.bottom; + Rect windowBound = windowMetrics.getBounds(); + if (mLayoutParameter.x != xOffset || mLayoutParameter.y != yOffset) { + mLayoutParameter.x = xOffset; + mLayoutParameter.y = yOffset; + shouldUpdateLayout = true; + } + // for gestural navigation mode and the landscape mode, + // the layout height should be decreased by system bar + // and display cutout inset to fit the new + // frame size that doesn't overlap the navigation bar window. + int orientation = mService.getResources().getConfiguration().orientation; + if (mLayout.getHeight() != mLayoutParameter.height + && orientation == Configuration.ORIENTATION_LANDSCAPE) { + mLayoutParameter.height = windowBound.height() - yOffset; + shouldUpdateLayout = true; + } + return shouldUpdateLayout; + } + + /** + * Gets the current page index when device configuration changed. {@link + * AccessibilityMenuService#onConfigurationChanged(Configuration)} + * + * @return the current index of the ViewPager. + */ + public int getPageIndex() { + if (mA11yMenuViewPager != null) { + return mA11yMenuViewPager.mViewPager.getCurrentItem(); + } + return A11yMenuViewPager.DEFAULT_PAGE_INDEX; + } + + /** + * Hides a11y menu layout. And return if layout visibility has been changed. + * + * @return {@code true} layout visibility is toggled off; {@code false} is unchanged + */ + public boolean hideMenu() { + if (mLayout.getVisibility() == View.VISIBLE) { + mLayout.setVisibility(View.GONE); + return true; + } + return false; + } + + /** Toggles a11y menu layout visibility. */ + public void toggleVisibility() { + mLayout.setVisibility((mLayout.getVisibility() == View.VISIBLE) ? View.GONE : View.VISIBLE); + } + + /** Shows hint text on Toast. */ + public void showToast(String text) { + final View viewPos = mLayout.findViewById(R.id.coordinatorLayout); + Toast.makeText(viewPos.getContext(), text, Toast.LENGTH_SHORT).show(); + } +} diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java new file mode 100644 index 000000000000..c510b876e847 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.accessibilitymenu.view; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Insets; +import android.util.DisplayMetrics; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; +import android.widget.GridView; + +import androidx.viewpager.widget.ViewPager; + +import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; +import com.android.systemui.accessibility.accessibilitymenu.R; +import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; +import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuFooter.A11yMenuFooterCallBack; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class handles UI for viewPager and footer. + * It displays grid pages containing all shortcuts in viewPager, + * and handles the click events from footer to switch between pages. + */ +public class A11yMenuViewPager { + + /** The default index of the ViewPager. */ + public static final int DEFAULT_PAGE_INDEX = 0; + + /** + * The class holds the static parameters for grid view when large button settings is on/off. + */ + public static final class GridViewParams { + /** Total shortcuts count in the grid view when large button settings is off. */ + public static final int GRID_ITEM_COUNT = 9; + + /** The number of columns in the grid view when large button settings is off. */ + public static final int GRID_COLUMN_COUNT = 3; + + /** Total shortcuts count in the grid view when large button settings is on. */ + public static final int LARGE_GRID_ITEM_COUNT = 4; + + /** The number of columns in the grid view when large button settings is on. */ + public static final int LARGE_GRID_COLUMN_COUNT = 2; + + /** Temporary measure to test both item types. */ + private static final boolean USE_LARGE_ITEMS = true; + + /** + * Returns the number of items in the grid view. + * + * @param context The parent context + * @return Grid item count + */ + public static int getGridItemCount(Context context) { + return USE_LARGE_ITEMS + ? LARGE_GRID_ITEM_COUNT + : GRID_ITEM_COUNT; + } + + /** + * Returns the number of columns in the grid view. + * + * @param context The parent context + * @return Grid column count + */ + public static int getGridColumnCount(Context context) { + return USE_LARGE_ITEMS + ? LARGE_GRID_COLUMN_COUNT + : GRID_COLUMN_COUNT; + } + + /** + * Returns the number of rows in the grid view. + * + * @param context The parent context + * @return Grid row count + */ + public static int getGridRowCount(Context context) { + return USE_LARGE_ITEMS + ? (LARGE_GRID_ITEM_COUNT / LARGE_GRID_COLUMN_COUNT) + : (GRID_ITEM_COUNT / GRID_COLUMN_COUNT); + } + + /** + * Separates a provided list of accessibility shortcuts into multiple sub-lists. + * Does not modify the original list. + * + * @param pageItemCount The maximum size of an individual sub-list. + * @param shortcutList The list of shortcuts to be separated into sub-lists. + * @return A list of shortcut sub-lists. + */ + public static List<List<A11yMenuShortcut>> generateShortcutSubLists( + int pageItemCount, List<A11yMenuShortcut> shortcutList) { + int start = 0; + int end; + int shortcutListSize = shortcutList.size(); + List<List<A11yMenuShortcut>> subLists = new ArrayList<>(); + while (start < shortcutListSize) { + end = Math.min(start + pageItemCount, shortcutListSize); + subLists.add(shortcutList.subList(start, end)); + start = end; + } + return subLists; + } + + private GridViewParams() {} + } + + private final AccessibilityMenuService mService; + + /** + * The pager widget, which handles animation and allows swiping horizontally to access previous + * and next gridView pages. + */ + protected ViewPager mViewPager; + + private ViewPagerAdapter<GridView> mViewPagerAdapter; + private final List<GridView> mGridPageList = new ArrayList<>(); + + /** The footer, which provides buttons to switch between pages */ + protected A11yMenuFooter mA11yMenuFooter; + + /** The shortcut list intended to show in grid pages of viewPager */ + private List<A11yMenuShortcut> mA11yMenuShortcutList; + + /** The container layout for a11y menu. */ + private ViewGroup mA11yMenuLayout; + + public A11yMenuViewPager(AccessibilityMenuService service) { + this.mService = service; + } + + /** + * Configures UI for view pager and footer. + * + * @param a11yMenuLayout the container layout for a11y menu + * @param shortcutDataList the data list need to show in view pager + * @param pageIndex the index of ViewPager to show + */ + public void configureViewPagerAndFooter( + ViewGroup a11yMenuLayout, List<A11yMenuShortcut> shortcutDataList, int pageIndex) { + this.mA11yMenuLayout = a11yMenuLayout; + mA11yMenuShortcutList = shortcutDataList; + initViewPager(); + initChildPage(); + mA11yMenuFooter = new A11yMenuFooter(a11yMenuLayout, mFooterCallbacks); + updateFooterState(); + registerOnGlobalLayoutListener(); + goToPage(pageIndex); + } + + /** Initializes viewPager and its adapter. */ + private void initViewPager() { + mViewPager = mA11yMenuLayout.findViewById(R.id.view_pager); + mViewPagerAdapter = new ViewPagerAdapter<>(); + mViewPager.setAdapter(mViewPagerAdapter); + mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER); + mViewPager.addOnPageChangeListener( + new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrollStateChanged(int state) {} + + @Override + public void onPageScrolled( + int position, float positionOffset, int positionOffsetPixels) {} + + @Override + public void onPageSelected(int position) { + updateFooterState(); + } + }); + } + + /** Creates child pages of viewPager by the length of shortcuts and initializes them. */ + private void initChildPage() { + if (mA11yMenuShortcutList == null || mA11yMenuShortcutList.isEmpty()) { + return; + } + + if (!mGridPageList.isEmpty()) { + mGridPageList.clear(); + } + + // Generate pages by calculating # of items per grid. + for (List<A11yMenuShortcut> page : GridViewParams.generateShortcutSubLists( + GridViewParams.getGridItemCount(mService), mA11yMenuShortcutList) + ) { + addGridPage(page); + } + + mViewPagerAdapter.set(mGridPageList); + } + + private void addGridPage(List<A11yMenuShortcut> shortcutDataListInPage) { + LayoutInflater inflater = LayoutInflater.from(mService); + View view = inflater.inflate(R.layout.grid_view, null); + GridView gridView = view.findViewById(R.id.gridview); + A11yMenuAdapter adapter = new A11yMenuAdapter(mService, shortcutDataListInPage); + gridView.setNumColumns(GridViewParams.getGridColumnCount(mService)); + gridView.setAdapter(adapter); + mGridPageList.add(gridView); + } + + /** Updates footer's state by index of current page in view pager. */ + private void updateFooterState() { + int currentPage = mViewPager.getCurrentItem(); + int lastPage = mViewPager.getAdapter().getCount() - 1; + mA11yMenuFooter.getPreviousPageBtn().setEnabled(currentPage > 0); + mA11yMenuFooter.getNextPageBtn().setEnabled(currentPage < lastPage); + } + + private void goToPage(int pageIndex) { + if (mViewPager == null) { + return; + } + if ((pageIndex >= 0) && (pageIndex < mViewPager.getAdapter().getCount())) { + mViewPager.setCurrentItem(pageIndex); + } + } + + /** Registers OnGlobalLayoutListener to adjust menu UI by running callback at first time. */ + private void registerOnGlobalLayoutListener() { + mA11yMenuLayout + .getViewTreeObserver() + .addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + + boolean mIsFirstTime = true; + + @Override + public void onGlobalLayout() { + if (!mIsFirstTime) { + return; + } + + if (mGridPageList.isEmpty()) { + return; + } + + GridView firstGridView = mGridPageList.get(0); + if (firstGridView == null + || firstGridView.getChildAt(0) == null) { + return; + } + + mIsFirstTime = false; + + int gridItemHeight = firstGridView.getChildAt(0) + .getMeasuredHeight(); + adjustMenuUISize(gridItemHeight); + } + }); + } + + /** + * Adjusts menu UI to fit both landscape and portrait mode. + * + * <ol> + * <li>Adjust view pager's height. + * <li>Adjust vertical interval between grid items. + * <li>Adjust padding in view pager. + * </ol> + */ + private void adjustMenuUISize(int gridItemHeight) { + final int rowsInGridView = GridViewParams.getGridRowCount(mService); + final int defaultMargin = + (int) mService.getResources().getDimension(R.dimen.a11ymenu_layout_margin); + final int topMargin = (int) mService.getResources().getDimension(R.dimen.table_margin_top); + final int displayMode = mService.getResources().getConfiguration().orientation; + int viewPagerHeight = mViewPager.getMeasuredHeight(); + + if (displayMode == Configuration.ORIENTATION_PORTRAIT) { + // In portrait mode, we only need to adjust view pager's height to match its + // child's height. + viewPagerHeight = gridItemHeight * rowsInGridView + defaultMargin + topMargin; + } else if (displayMode == Configuration.ORIENTATION_LANDSCAPE) { + // In landscape mode, we need to adjust view pager's height to match screen height + // and adjust its child too, + // because a11y menu layout height is limited by the screen height. + DisplayMetrics displayMetrics = mService.getResources().getDisplayMetrics(); + float densityScale = (float) displayMetrics.densityDpi + / DisplayMetrics.DENSITY_DEVICE_STABLE; + View footerLayout = mA11yMenuLayout.findViewById(R.id.footerlayout); + // Keeps footer window height unchanged no matter the density is changed. + footerLayout.getLayoutParams().height = + (int) (footerLayout.getLayoutParams().height / densityScale); + // Adjust the view pager height for system bar and display cutout insets. + WindowManager windowManager = mService.getSystemService(WindowManager.class); + WindowMetrics windowMetric = windowManager.getCurrentWindowMetrics(); + Insets windowInsets = windowMetric.getWindowInsets().getInsetsIgnoringVisibility( + WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); + viewPagerHeight = + windowMetric.getBounds().height() + - footerLayout.getLayoutParams().height + - windowInsets.bottom; + // Sets vertical interval between grid items. + int interval = + (viewPagerHeight - topMargin - defaultMargin + - (rowsInGridView * gridItemHeight)) + / (rowsInGridView + 1); + for (GridView gridView : mGridPageList) { + gridView.setVerticalSpacing(interval); + } + + // Sets padding to view pager. + final int finalMarginTop = interval + topMargin; + mViewPager.setPadding(defaultMargin, finalMarginTop, defaultMargin, defaultMargin); + } + final ViewGroup.LayoutParams layoutParams = mViewPager.getLayoutParams(); + layoutParams.height = viewPagerHeight; + mViewPager.setLayoutParams(layoutParams); + } + + /** Callback object to handle click events from A11yMenuFooter */ + protected A11yMenuFooterCallBack mFooterCallbacks = + new A11yMenuFooterCallBack() { + @Override + public void onLeftButtonClicked() { + // Moves to previous page. + int targetPage = mViewPager.getCurrentItem() - 1; + goToPage(targetPage); + updateFooterState(); + } + + @Override + public void onRightButtonClicked() { + // Moves to next page. + int targetPage = mViewPager.getCurrentItem() + 1; + goToPage(targetPage); + updateFooterState(); + } + }; +} diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java new file mode 100644 index 000000000000..5670d72842f4 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.accessibilitymenu.view; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.viewpager.widget.PagerAdapter; + +import java.util.List; + +/** The pager adapter, which provides the pages to the view pager widget. */ +class ViewPagerAdapter<T extends View> extends PagerAdapter { + + /** The widget list in each page of view pager. */ + private List<T> mWidgetList; + + ViewPagerAdapter() {} + + public void set(List<T> tList) { + mWidgetList = tList; + notifyDataSetChanged(); + } + + @Override + public int getCount() { + if (mWidgetList == null) { + return 0; + } + return mWidgetList.size(); + } + + @Override + public int getItemPosition(Object object) { + return POSITION_NONE; + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + if (mWidgetList == null) { + return null; + } + container.addView(mWidgetList.get(position)); + return mWidgetList.get(position); + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + container.removeView((View) object); + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt index 290328894439..3d341af3b397 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt @@ -33,7 +33,9 @@ private const val FONT_ITALIC_MIN = 0f private const val FONT_ITALIC_ANIMATION_STEP = 0.1f private const val FONT_ITALIC_DEFAULT_VALUE = 0f -/** Provide interpolation of two fonts by adjusting font variation settings. */ +/** + * Provide interpolation of two fonts by adjusting font variation settings. + */ class FontInterpolator { /** @@ -59,14 +61,11 @@ class FontInterpolator { var index: Int, val sortedAxes: MutableList<FontVariationAxis> ) { - constructor( - font: Font, - axes: List<FontVariationAxis> - ) : this( - font.sourceIdentifier, - font.ttcIndex, - axes.toMutableList().apply { sortBy { it.tag } } - ) + constructor(font: Font, axes: List<FontVariationAxis>) : + this(font.sourceIdentifier, + font.ttcIndex, + axes.toMutableList().apply { sortBy { it.tag } } + ) fun set(font: Font, axes: List<FontVariationAxis>) { sourceId = font.sourceIdentifier @@ -87,7 +86,9 @@ class FontInterpolator { private val tmpInterpKey = InterpKey(null, null, 0f) private val tmpVarFontKey = VarFontKey(0, 0, mutableListOf()) - /** Linear interpolate the font variation settings. */ + /** + * Linear interpolate the font variation settings. + */ fun lerp(start: Font, end: Font, progress: Float): Font { if (progress == 0f) { return start @@ -114,34 +115,27 @@ class FontInterpolator { // this doesn't take much time since the variation axes is usually up to 5. If we need to // support more number of axes, we may want to preprocess the font and store the sorted axes // and also pre-fill the missing axes value with default value from 'fvar' table. - val newAxes = - lerp(startAxes, endAxes) { tag, startValue, endValue -> - when (tag) { - // TODO: Good to parse 'fvar' table for retrieving default value. - TAG_WGHT -> - adjustWeight( - MathUtils.lerp( + val newAxes = lerp(startAxes, endAxes) { tag, startValue, endValue -> + when (tag) { + // TODO: Good to parse 'fvar' table for retrieving default value. + TAG_WGHT -> adjustWeight( + MathUtils.lerp( startValue ?: FONT_WEIGHT_DEFAULT_VALUE, endValue ?: FONT_WEIGHT_DEFAULT_VALUE, - progress - ) - ) - TAG_ITAL -> - adjustItalic( - MathUtils.lerp( + progress)) + TAG_ITAL -> adjustItalic( + MathUtils.lerp( startValue ?: FONT_ITALIC_DEFAULT_VALUE, endValue ?: FONT_ITALIC_DEFAULT_VALUE, - progress - ) - ) - else -> { - require(startValue != null && endValue != null) { - "Unable to interpolate due to unknown default axes value : $tag" - } - MathUtils.lerp(startValue, endValue, progress) + progress)) + else -> { + require(startValue != null && endValue != null) { + "Unable to interpolate due to unknown default axes value : $tag" } + MathUtils.lerp(startValue, endValue, progress) } } + } // Check if we already make font for this axes. This is typically happens if the animation // happens backward. @@ -155,7 +149,9 @@ class FontInterpolator { // This is the first time to make the font for the axes. Build and store it to the cache. // Font.Builder#build won't throw IOException since creating fonts from existing fonts will // not do any IO work. - val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build() + val newFont = Font.Builder(start) + .setFontVariationSettings(newAxes.toTypedArray()) + .build() interpCache[InterpKey(start, end, progress)] = newFont verFontCache[VarFontKey(start, newAxes)] = newFont return newFont @@ -177,28 +173,26 @@ class FontInterpolator { val tagA = if (i < start.size) start[i].tag else null val tagB = if (j < end.size) end[j].tag else null - val comp = - when { - tagA == null -> 1 - tagB == null -> -1 - else -> tagA.compareTo(tagB) - } + val comp = when { + tagA == null -> 1 + tagB == null -> -1 + else -> tagA.compareTo(tagB) + } - val axis = - when { - comp == 0 -> { - val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue) - FontVariationAxis(tagA, v) - } - comp < 0 -> { - val v = filter(tagA!!, start[i++].styleValue, null) - FontVariationAxis(tagA, v) - } - else -> { // comp > 0 - val v = filter(tagB!!, null, end[j++].styleValue) - FontVariationAxis(tagB, v) - } + val axis = when { + comp == 0 -> { + val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue) + FontVariationAxis(tagA, v) } + comp < 0 -> { + val v = filter(tagA!!, start[i++].styleValue, null) + FontVariationAxis(tagA, v) + } + else -> { // comp > 0 + val v = filter(tagB!!, null, end[j++].styleValue) + FontVariationAxis(tagB, v) + } + } result.add(axis) } @@ -208,21 +202,21 @@ class FontInterpolator { // For the performance reasons, we animate weight with FONT_WEIGHT_ANIMATION_STEP. This helps // Cache hit ratio in the Skia glyph cache. private fun adjustWeight(value: Float) = - coerceInWithStep(value, FONT_WEIGHT_MIN, FONT_WEIGHT_MAX, FONT_WEIGHT_ANIMATION_STEP) + coerceInWithStep(value, FONT_WEIGHT_MIN, FONT_WEIGHT_MAX, FONT_WEIGHT_ANIMATION_STEP) // For the performance reasons, we animate italic with FONT_ITALIC_ANIMATION_STEP. This helps // Cache hit ratio in the Skia glyph cache. private fun adjustItalic(value: Float) = - coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP) + coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP) private fun coerceInWithStep(v: Float, min: Float, max: Float, step: Float) = - (v.coerceIn(min, max) / step).toInt() * step + (v.coerceIn(min, max) / step).toInt() * step companion object { private val EMPTY_AXES = arrayOf<FontVariationAxis>() // Returns true if given two font instance can be interpolated. fun canInterpolate(start: Font, end: Font) = - start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier + start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index 65d6c83d74a8..5f1bb83715c2 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -36,7 +36,8 @@ typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit * Currently this class can provide text style animation for text weight and text size. For example * the simple view that draws text with animating text size is like as follows: * - * ``` + * <pre> + * <code> * class SimpleTextAnimation : View { * @JvmOverloads constructor(...) * @@ -52,34 +53,39 @@ typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit * animator.setTextStyle(-1 /* unchanged weight */, sizePx, animate) * } * } - * ``` + * </code> + * </pre> */ -class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { +class TextAnimator( + layout: Layout, + private val invalidateCallback: () -> Unit +) { // Following two members are for mutable for testing purposes. public var textInterpolator: TextInterpolator = TextInterpolator(layout) - public var animator: ValueAnimator = - ValueAnimator.ofFloat(1f).apply { - duration = DEFAULT_ANIMATION_DURATION - addUpdateListener { - textInterpolator.progress = it.animatedValue as Float - invalidateCallback() - } - addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - textInterpolator.rebase() - } - override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase() - } - ) + public var animator: ValueAnimator = ValueAnimator.ofFloat(1f).apply { + duration = DEFAULT_ANIMATION_DURATION + addUpdateListener { + textInterpolator.progress = it.animatedValue as Float + invalidateCallback() } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + textInterpolator.rebase() + } + override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase() + }) + } sealed class PositionedGlyph { - /** Mutable X coordinate of the glyph position relative from drawing offset. */ + /** + * Mutable X coordinate of the glyph position relative from drawing offset. + */ var x: Float = 0f - /** Mutable Y coordinate of the glyph position relative from the baseline. */ + /** + * Mutable Y coordinate of the glyph position relative from the baseline. + */ var y: Float = 0f /** @@ -90,29 +96,40 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { /** * Mutable text size of the glyph in pixels. */ - /** Mutable text size of the glyph in pixels. */ var textSize: Float = 0f - /** Mutable color of the glyph. */ + /** + * Mutable color of the glyph. + */ var color: Int = 0 - /** Immutable character offset in the text that the current font run start. */ + /** + * Immutable character offset in the text that the current font run start. + */ abstract var runStart: Int protected set - /** Immutable run length of the font run. */ + /** + * Immutable run length of the font run. + */ abstract var runLength: Int protected set - /** Immutable glyph index of the font run. */ + /** + * Immutable glyph index of the font run. + */ abstract var glyphIndex: Int protected set - /** Immutable font instance for this font run. */ + /** + * Immutable font instance for this font run. + */ abstract var font: Font protected set - /** Immutable glyph ID for this glyph. */ + /** + * Immutable glyph ID for this glyph. + */ abstract var glyphId: Int protected set } @@ -130,30 +147,30 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { /** * GlyphFilter applied just before drawing to canvas for tweaking positions and text size. * - * This callback is called for each glyphs just before drawing the glyphs. This function will be - * called with the intrinsic position, size, color, glyph ID and font instance. You can mutate - * the position, size and color for tweaking animations. Do not keep the reference of passed - * glyph object. The interpolator reuses that object for avoiding object allocations. + * This callback is called for each glyphs just before drawing the glyphs. This function will + * be called with the intrinsic position, size, color, glyph ID and font instance. You can + * mutate the position, size and color for tweaking animations. + * Do not keep the reference of passed glyph object. The interpolator reuses that object for + * avoiding object allocations. * - * Details: The text is drawn with font run units. The font run is a text segment that draws - * with the same font. The {@code runStart} and {@code runLimit} is a range of the font run in - * the text that current glyph is in. Once the font run is determined, the system will convert - * characters into glyph IDs. The {@code glyphId} is the glyph identifier in the font and {@code - * glyphIndex} is the offset of the converted glyph array. Please note that the {@code - * glyphIndex} is not a character index, because the character will not be converted to glyph - * one-by-one. If there are ligatures including emoji sequence, etc, the glyph ID may be + * Details: + * The text is drawn with font run units. The font run is a text segment that draws with the + * same font. The {@code runStart} and {@code runLimit} is a range of the font run in the text + * that current glyph is in. Once the font run is determined, the system will convert characters + * into glyph IDs. The {@code glyphId} is the glyph identifier in the font and + * {@code glyphIndex} is the offset of the converted glyph array. Please note that the + * {@code glyphIndex} is not a character index, because the character will not be converted to + * glyph one-by-one. If there are ligatures including emoji sequence, etc, the glyph ID may be * composed from multiple characters. * * Here is an example of font runs: "fin. 終わり" * - * ``` * Characters : f i n . _ 終 わ り * Code Points: \u0066 \u0069 \u006E \u002E \u0020 \u7D42 \u308F \u308A * Font Runs : <-- Roboto-Regular.ttf --><-- NotoSans-CJK.otf --> * runStart = 0, runLength = 5 runStart = 5, runLength = 3 * Glyph IDs : 194 48 7 8 4367 1039 1002 * Glyph Index: 0 1 2 3 0 1 2 - * ``` * * In this example, the "fi" is converted into ligature form, thus the single glyph ID is * assigned for two characters, f and i. @@ -176,29 +193,28 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { */ var glyphFilter: GlyphCallback? get() = textInterpolator.glyphFilter - set(value) { - textInterpolator.glyphFilter = value - } + set(value) { textInterpolator.glyphFilter = value } fun draw(c: Canvas) = textInterpolator.draw(c) /** * Set text style with animation. * - * By passing -1 to weight, the view preserve the current weight. By passing -1 to textSize, the - * view preserve the current text size. Bu passing -1 to duration, the default text animation, - * 1000ms, is used. By passing false to animate, the text will be updated without animation. + * By passing -1 to weight, the view preserve the current weight. + * By passing -1 to textSize, the view preserve the current text size. + * Bu passing -1 to duration, the default text animation, 1000ms, is used. + * By passing false to animate, the text will be updated without animation. * * @param weight an optional text weight. * @param textSize an optional font size. - * @param colors an optional colors array that must be the same size as numLines passed to the - * TextInterpolator + * @param colors an optional colors array that must be the same size as numLines passed to + * the TextInterpolator * @param animate an optional boolean indicating true for showing style transition as animation, - * false for immediate style transition. True by default. + * false for immediate style transition. True by default. * @param duration an optional animation duration in milliseconds. This is ignored if animate is - * false. + * false. * @param interpolator an optional time interpolator. If null is passed, last set interpolator - * will be used. This is ignored if animate is false. + * will be used. This is ignored if animate is false. */ fun setTextStyle( weight: Int = -1, @@ -221,11 +237,10 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { if (weight >= 0) { // Paint#setFontVariationSettings creates Typeface instance from scratch. To reduce the // memory impact, cache the typeface result. - textInterpolator.targetPaint.typeface = - typefaceCache.getOrElse(weight) { - textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight" - textInterpolator.targetPaint.typeface - } + textInterpolator.targetPaint.typeface = typefaceCache.getOrElse(weight) { + textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight" + textInterpolator.targetPaint.typeface + } } if (color != null) { textInterpolator.targetPaint.color = color @@ -234,24 +249,22 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { if (animate) { animator.startDelay = delay - animator.duration = - if (duration == -1L) { - DEFAULT_ANIMATION_DURATION - } else { - duration - } + animator.duration = if (duration == -1L) { + DEFAULT_ANIMATION_DURATION + } else { + duration + } interpolator?.let { animator.interpolator = it } if (onAnimationEnd != null) { - val listener = - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - onAnimationEnd.run() - animator.removeListener(this) - } - override fun onAnimationCancel(animation: Animator?) { - animator.removeListener(this) - } + val listener = object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + onAnimationEnd.run() + animator.removeListener(this) } + override fun onAnimationCancel(animation: Animator?) { + animator.removeListener(this) + } + } animator.addListener(listener) } animator.start() diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt index f9fb42cd1670..0448c818f765 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt @@ -26,8 +26,12 @@ import android.util.MathUtils import com.android.internal.graphics.ColorUtils import java.lang.Math.max -/** Provide text style linear interpolation for plain text. */ -class TextInterpolator(layout: Layout) { +/** + * Provide text style linear interpolation for plain text. + */ +class TextInterpolator( + layout: Layout +) { /** * Returns base paint used for interpolation. @@ -60,11 +64,12 @@ class TextInterpolator(layout: Layout) { var baseFont: Font, var targetFont: Font ) { - val length: Int - get() = end - start + val length: Int get() = end - start } - /** A class represents text layout of a single run. */ + /** + * A class represents text layout of a single run. + */ private class Run( val glyphIds: IntArray, val baseX: FloatArray, // same length as glyphIds @@ -74,8 +79,12 @@ class TextInterpolator(layout: Layout) { val fontRuns: List<FontRun> ) - /** A class represents text layout of a single line. */ - private class Line(val runs: List<Run>) + /** + * A class represents text layout of a single line. + */ + private class Line( + val runs: List<Run> + ) private var lines = listOf<Line>() private val fontInterpolator = FontInterpolator() @@ -97,8 +106,8 @@ class TextInterpolator(layout: Layout) { /** * The layout used for drawing text. * - * Only non-styled text is supported. Even if the given layout is created from Spanned, the span - * information is not used. + * Only non-styled text is supported. Even if the given layout is created from Spanned, the + * span information is not used. * * The paint objects used for interpolation are not changed by this method call. * @@ -121,8 +130,8 @@ class TextInterpolator(layout: Layout) { /** * Recalculate internal text layout for interpolation. * - * Whenever the target paint is modified, call this method to recalculate internal text layout - * used for interpolation. + * Whenever the target paint is modified, call this method to recalculate internal + * text layout used for interpolation. */ fun onTargetPaintModified() { updatePositionsAndFonts(shapeText(layout, targetPaint), updateBase = false) @@ -131,8 +140,8 @@ class TextInterpolator(layout: Layout) { /** * Recalculate internal text layout for interpolation. * - * Whenever the base paint is modified, call this method to recalculate internal text layout - * used for interpolation. + * Whenever the base paint is modified, call this method to recalculate internal + * text layout used for interpolation. */ fun onBasePaintModified() { updatePositionsAndFonts(shapeText(layout, basePaint), updateBase = true) @@ -143,11 +152,11 @@ class TextInterpolator(layout: Layout) { * * The text interpolator does not calculate all the text position by text shaper due to * performance reasons. Instead, the text interpolator shape the start and end state and - * calculate text position of the middle state by linear interpolation. Due to this trick, the - * text positions of the middle state is likely different from the text shaper result. So, if - * you want to start animation from the middle state, you will see the glyph jumps due to this - * trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different from - * text shape result of weight 550. + * calculate text position of the middle state by linear interpolation. Due to this trick, + * the text positions of the middle state is likely different from the text shaper result. + * So, if you want to start animation from the middle state, you will see the glyph jumps due to + * this trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different + * from text shape result of weight 550. * * After calling this method, do not call onBasePaintModified() since it reshape the text and * update the base state. As in above notice, the text shaping result at current progress is @@ -159,7 +168,8 @@ class TextInterpolator(layout: Layout) { * animate weight from 200 to 400, then if you want to move back to 200 at the half of the * animation, it will look like * - * ``` + * <pre> + * <code> * val interp = TextInterpolator(layout) * * // Interpolate between weight 200 to 400. @@ -189,7 +199,9 @@ class TextInterpolator(layout: Layout) { * // progress is 0.5 * animator.start() * } - * ``` + * </code> + * </pre> + * */ fun rebase() { if (progress == 0f) { @@ -251,75 +263,69 @@ class TextInterpolator(layout: Layout) { } var maxRunLength = 0 - lines = - baseLayout.zip(targetLayout) { baseLine, targetLine -> - val runs = - baseLine.zip(targetLine) { base, target -> - require(base.glyphCount() == target.glyphCount()) { - "Inconsistent glyph count at line ${lines.size}" + lines = baseLayout.zip(targetLayout) { baseLine, targetLine -> + val runs = baseLine.zip(targetLine) { base, target -> + + require(base.glyphCount() == target.glyphCount()) { + "Inconsistent glyph count at line ${lines.size}" + } + + val glyphCount = base.glyphCount() + + // Good to recycle the array if the existing array can hold the new layout result. + val glyphIds = IntArray(glyphCount) { + base.getGlyphId(it).also { baseGlyphId -> + require(baseGlyphId == target.getGlyphId(it)) { + "Inconsistent glyph ID at $it in line ${lines.size}" } + } + } - val glyphCount = base.glyphCount() - - // Good to recycle the array if the existing array can hold the new layout - // result. - val glyphIds = - IntArray(glyphCount) { - base.getGlyphId(it).also { baseGlyphId -> - require(baseGlyphId == target.getGlyphId(it)) { - "Inconsistent glyph ID at $it in line ${lines.size}" - } - } - } + val baseX = FloatArray(glyphCount) { base.getGlyphX(it) } + val baseY = FloatArray(glyphCount) { base.getGlyphY(it) } + val targetX = FloatArray(glyphCount) { target.getGlyphX(it) } + val targetY = FloatArray(glyphCount) { target.getGlyphY(it) } + + // Calculate font runs + val fontRun = mutableListOf<FontRun>() + if (glyphCount != 0) { + var start = 0 + var baseFont = base.getFont(start) + var targetFont = target.getFont(start) + require(FontInterpolator.canInterpolate(baseFont, targetFont)) { + "Cannot interpolate font at $start ($baseFont vs $targetFont)" + } - val baseX = FloatArray(glyphCount) { base.getGlyphX(it) } - val baseY = FloatArray(glyphCount) { base.getGlyphY(it) } - val targetX = FloatArray(glyphCount) { target.getGlyphX(it) } - val targetY = FloatArray(glyphCount) { target.getGlyphY(it) } - - // Calculate font runs - val fontRun = mutableListOf<FontRun>() - if (glyphCount != 0) { - var start = 0 - var baseFont = base.getFont(start) - var targetFont = target.getFont(start) + for (i in 1 until glyphCount) { + val nextBaseFont = base.getFont(i) + val nextTargetFont = target.getFont(i) + + if (baseFont !== nextBaseFont) { + require(targetFont !== nextTargetFont) { + "Base font has changed at $i but target font has not changed." + } + // Font transition point. push run and reset context. + fontRun.add(FontRun(start, i, baseFont, targetFont)) + maxRunLength = max(maxRunLength, i - start) + baseFont = nextBaseFont + targetFont = nextTargetFont + start = i require(FontInterpolator.canInterpolate(baseFont, targetFont)) { "Cannot interpolate font at $start ($baseFont vs $targetFont)" } - - for (i in 1 until glyphCount) { - val nextBaseFont = base.getFont(i) - val nextTargetFont = target.getFont(i) - - if (baseFont !== nextBaseFont) { - require(targetFont !== nextTargetFont) { - "Base font has changed at $i but target font has not " + - "changed." - } - // Font transition point. push run and reset context. - fontRun.add(FontRun(start, i, baseFont, targetFont)) - maxRunLength = max(maxRunLength, i - start) - baseFont = nextBaseFont - targetFont = nextTargetFont - start = i - require(FontInterpolator.canInterpolate(baseFont, targetFont)) { - "Cannot interpolate font at $start ($baseFont vs " + - "$targetFont)" - } - } else { // baseFont === nextBaseFont - require(targetFont === nextTargetFont) { - "Base font has not changed at $i but target font has " + - "changed." - } - } + } else { // baseFont === nextBaseFont + require(targetFont === nextTargetFont) { + "Base font has not changed at $i but target font has changed." } - fontRun.add(FontRun(start, glyphCount, baseFont, targetFont)) - maxRunLength = max(maxRunLength, glyphCount - start) } - Run(glyphIds, baseX, baseY, targetX, targetY, fontRun) } - Line(runs) + fontRun.add(FontRun(start, glyphCount, baseFont, targetFont)) + maxRunLength = max(maxRunLength, glyphCount - start) + } + Run(glyphIds, baseX, baseY, targetX, targetY, fontRun) } + Line(runs) + } // Update float array used for drawing. if (tmpPositionArray.size < maxRunLength * 2) { @@ -351,9 +357,9 @@ class TextInterpolator(layout: Layout) { if (glyphFilter == null) { for (i in run.start until run.end) { tmpPositionArray[arrayIndex++] = - MathUtils.lerp(line.baseX[i], line.targetX[i], progress) + MathUtils.lerp(line.baseX[i], line.targetX[i], progress) tmpPositionArray[arrayIndex++] = - MathUtils.lerp(line.baseY[i], line.targetY[i], progress) + MathUtils.lerp(line.baseY[i], line.targetY[i], progress) } c.drawGlyphs(line.glyphIds, run.start, tmpPositionArray, 0, run.length, font, paint) return @@ -382,14 +388,13 @@ class TextInterpolator(layout: Layout) { tmpPaintForGlyph.color = tmpGlyph.color c.drawGlyphs( - line.glyphIds, - prevStart, - tmpPositionArray, - 0, - i - prevStart, - font, - tmpPaintForGlyph - ) + line.glyphIds, + prevStart, + tmpPositionArray, + 0, + i - prevStart, + font, + tmpPaintForGlyph) prevStart = i arrayIndex = 0 } @@ -399,14 +404,13 @@ class TextInterpolator(layout: Layout) { } c.drawGlyphs( - line.glyphIds, - prevStart, - tmpPositionArray, - 0, - run.end - prevStart, - font, - tmpPaintForGlyph - ) + line.glyphIds, + prevStart, + tmpPositionArray, + 0, + run.end - prevStart, + font, + tmpPaintForGlyph) } private fun updatePositionsAndFonts( @@ -414,7 +418,9 @@ class TextInterpolator(layout: Layout) { updateBase: Boolean ) { // Update target positions with newly calculated text layout. - check(layoutResult.size == lines.size) { "The new layout result has different line count." } + check(layoutResult.size == lines.size) { + "The new layout result has different line count." + } lines.zip(layoutResult) { line, runs -> line.runs.zip(runs) { lineRun, newGlyphs -> @@ -430,7 +436,7 @@ class TextInterpolator(layout: Layout) { } require(newFont === newGlyphs.getFont(i)) { "The new layout has different font run." + - " $newFont vs ${newGlyphs.getFont(i)} at $i" + " $newFont vs ${newGlyphs.getFont(i)} at $i" } } @@ -438,7 +444,7 @@ class TextInterpolator(layout: Layout) { // check new font can be interpolatable with base font. require(FontInterpolator.canInterpolate(newFont, run.baseFont)) { "New font cannot be interpolated with existing font. $newFont," + - " ${run.baseFont}" + " ${run.baseFont}" } if (updateBase) { @@ -474,7 +480,10 @@ class TextInterpolator(layout: Layout) { } // Shape the text and stores the result to out argument. - private fun shapeText(layout: Layout, paint: TextPaint): List<List<PositionedGlyphs>> { + private fun shapeText( + layout: Layout, + paint: TextPaint + ): List<List<PositionedGlyphs>> { val out = mutableListOf<List<PositionedGlyphs>>() for (lineNo in 0 until layout.lineCount) { // Shape all lines. val lineStart = layout.getLineStart(lineNo) @@ -486,13 +495,10 @@ class TextInterpolator(layout: Layout) { } val runs = mutableListOf<PositionedGlyphs>() - TextShaper.shapeText( - layout.text, - lineStart, - count, - layout.textDirectionHeuristic, - paint - ) { _, _, glyphs, _ -> runs.add(glyphs) } + TextShaper.shapeText(layout.text, lineStart, count, layout.textDirectionHeuristic, + paint) { _, _, glyphs, _ -> + runs.add(glyphs) + } out.add(runs) } return out @@ -500,8 +506,8 @@ class TextInterpolator(layout: Layout) { } private fun Layout.getDrawOrigin(lineNo: Int) = - if (getParagraphDirection(lineNo) == Layout.DIR_LEFT_TO_RIGHT) { - getLineLeft(lineNo) - } else { - getLineRight(lineNo) - } + if (getParagraphDirection(lineNo) == Layout.DIR_LEFT_TO_RIGHT) { + getLineLeft(lineNo) + } else { + getLineRight(lineNo) + } diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp index 40580d29380b..d3f66e333e96 100644 --- a/packages/SystemUI/checks/Android.bp +++ b/packages/SystemUI/checks/Android.bp @@ -37,12 +37,6 @@ java_library_host { java_test_host { name: "SystemUILintCheckerTest", - // TODO(b/239881504): Since this test was written, Android - // Lint was updated, and now includes classes that were - // compiled for java 15. The soong build doesn't support - // java 15 yet, so we can't compile against "lint". Disable - // the test until java 15 is supported. - enabled: false, srcs: [ "tests/**/*.kt", "tests/**/*.java", @@ -59,5 +53,19 @@ java_test_host { ], test_options: { unit_test: true, + tradefed_options: [ + { + // lint bundles in some classes that were built with older versions + // of libraries, and no longer load. Since tradefed tries to load + // all classes in the jar to look for tests, it crashes loading them. + // Exclude these classes from tradefed's search. + name: "exclude-paths", + value: "org/apache", + }, + { + name: "exclude-paths", + value: "META-INF", + }, + ], }, } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 462b90a10aee..86bd5f2bff5a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -54,7 +54,6 @@ class AnimatableClockView @JvmOverloads constructor( defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : TextView(context, attrs, defStyleAttr, defStyleRes) { - var tag: String = "UnnamedClockView" var logBuffer: LogBuffer? = null private val time = Calendar.getInstance() @@ -132,7 +131,7 @@ class AnimatableClockView @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() - logBuffer?.log(tag, DEBUG, "onAttachedToWindow") + logBuffer?.log(TAG, DEBUG, "onAttachedToWindow") refreshFormat() } @@ -148,7 +147,7 @@ class AnimatableClockView @JvmOverloads constructor( time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis() contentDescription = DateFormat.format(descFormat, time) val formattedText = DateFormat.format(format, time) - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = formattedText?.toString() }, { "refreshTime: new formattedText=$str1" } ) @@ -157,7 +156,7 @@ class AnimatableClockView @JvmOverloads constructor( // relayout if the text didn't actually change. if (!TextUtils.equals(text, formattedText)) { text = formattedText - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = formattedText?.toString() }, { "refreshTime: done setting new time text to: $str1" } ) @@ -167,17 +166,17 @@ class AnimatableClockView @JvmOverloads constructor( // without being notified TextInterpolator being notified. if (layout != null) { textAnimator?.updateLayout(layout) - logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout") + logBuffer?.log(TAG, DEBUG, "refreshTime: done updating textAnimator layout") } requestLayout() - logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout") + logBuffer?.log(TAG, DEBUG, "refreshTime: after requestLayout") } } fun onTimeZoneChanged(timeZone: TimeZone?) { time.timeZone = timeZone refreshFormat() - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = timeZone?.toString() }, { "onTimeZoneChanged newTimeZone=$str1" } ) @@ -194,7 +193,7 @@ class AnimatableClockView @JvmOverloads constructor( } else { animator.updateLayout(layout) } - logBuffer?.log(tag, DEBUG, "onMeasure") + logBuffer?.log(TAG, DEBUG, "onMeasure") } override fun onDraw(canvas: Canvas) { @@ -206,12 +205,12 @@ class AnimatableClockView @JvmOverloads constructor( } else { super.onDraw(canvas) } - logBuffer?.log(tag, DEBUG, "onDraw lastDraw") + logBuffer?.log(TAG, DEBUG, "onDraw") } override fun invalidate() { super.invalidate() - logBuffer?.log(tag, DEBUG, "invalidate") + logBuffer?.log(TAG, DEBUG, "invalidate") } override fun onTextChanged( @@ -221,7 +220,7 @@ class AnimatableClockView @JvmOverloads constructor( lengthAfter: Int ) { super.onTextChanged(text, start, lengthBefore, lengthAfter) - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = text.toString() }, { "onTextChanged text=$str1" } ) @@ -238,7 +237,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateColorChange() { - logBuffer?.log(tag, DEBUG, "animateColorChange") + logBuffer?.log(TAG, DEBUG, "animateColorChange") setTextStyle( weight = lockScreenWeight, textSize = -1f, @@ -260,7 +259,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateAppearOnLockscreen() { - logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen") + logBuffer?.log(TAG, DEBUG, "animateAppearOnLockscreen") setTextStyle( weight = dozingWeight, textSize = -1f, @@ -285,7 +284,7 @@ class AnimatableClockView @JvmOverloads constructor( if (isAnimationEnabled && textAnimator == null) { return } - logBuffer?.log(tag, DEBUG, "animateFoldAppear") + logBuffer?.log(TAG, DEBUG, "animateFoldAppear") setTextStyle( weight = lockScreenWeightInternal, textSize = -1f, @@ -312,7 +311,7 @@ class AnimatableClockView @JvmOverloads constructor( // Skip charge animation if dozing animation is already playing. return } - logBuffer?.log(tag, DEBUG, "animateCharge") + logBuffer?.log(TAG, DEBUG, "animateCharge") val startAnimPhase2 = Runnable { setTextStyle( weight = if (isDozing()) dozingWeight else lockScreenWeight, @@ -336,7 +335,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateDoze(isDozing: Boolean, animate: Boolean) { - logBuffer?.log(tag, DEBUG, "animateDoze") + logBuffer?.log(TAG, DEBUG, "animateDoze") setTextStyle( weight = if (isDozing) dozingWeight else lockScreenWeight, textSize = -1f, @@ -455,7 +454,7 @@ class AnimatableClockView @JvmOverloads constructor( isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12 else -> DOUBLE_LINE_FORMAT_12_HOUR } - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = format?.toString() }, { "refreshFormat format=$str1" } ) @@ -466,6 +465,7 @@ class AnimatableClockView @JvmOverloads constructor( fun dump(pw: PrintWriter) { pw.println("$this") + pw.println(" alpha=$alpha") pw.println(" measuredWidth=$measuredWidth") pw.println(" measuredHeight=$measuredHeight") pw.println(" singleLineInternal=$isSingleLineInternal") @@ -626,7 +626,7 @@ class AnimatableClockView @JvmOverloads constructor( } companion object { - private val TAG = AnimatableClockView::class.simpleName + private val TAG = AnimatableClockView::class.simpleName!! const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600 private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm" private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm" diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index e138ef8a1ea8..7645decfde24 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -88,13 +88,6 @@ class DefaultClockController( events.onTimeTick() } - override fun setLogBuffer(logBuffer: LogBuffer) { - smallClock.view.tag = "smallClockView" - largeClock.view.tag = "largeClockView" - smallClock.view.logBuffer = logBuffer - largeClock.view.logBuffer = logBuffer - } - open inner class DefaultClockFaceController( override val view: AnimatableClockView, ) : ClockFaceController { @@ -104,6 +97,12 @@ class DefaultClockController( private var isRegionDark = false protected var targetRegion: Rect? = null + override var logBuffer: LogBuffer? + get() = view.logBuffer + set(value) { + view.logBuffer = value + } + init { view.setColors(currentColor, currentColor) } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 66e44b9005de..a2a07095c16c 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -71,9 +71,6 @@ interface ClockController { /** Optional method for dumping debug information */ fun dump(pw: PrintWriter) {} - - /** Optional method for debug logging */ - fun setLogBuffer(logBuffer: LogBuffer) {} } /** Interface for a specific clock face version rendered by the clock */ @@ -83,6 +80,9 @@ interface ClockFaceController { /** Events specific to this clock face */ val events: ClockFaceEvents + + /** Some clocks may log debug information */ + var logBuffer: LogBuffer? } /** Events that should call when various rendering parameters change */ diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt index 6436dcb5f613..e99b2149bc1d 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt @@ -159,8 +159,13 @@ constructor( * bug report more actionable, so using the [log] with a messagePrinter to add more detail to * every log may do more to improve overall logging than adding more logs with this method. */ - fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) = - log(tag, level, { str1 = message }, { str1!! }) + @JvmOverloads + fun log( + tag: String, + level: LogLevel, + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(tag, level, { str1 = message }, { str1!! }, exception) /** * You should call [log] instead of this method. diff --git a/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml new file mode 100644 index 000000000000..08c5aaf56bf7 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml @@ -0,0 +1,26 @@ +<?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 +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="13dp" + android:height="13dp" + android:viewportWidth="48" + android:viewportHeight="48" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M18.3,34H29.65V31H21.3V25.7H29.65V22.7H21.3V17.35H29.65V14.35H18.3ZM9,42Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6H39Q40.2,6 41.1,6.9Q42,7.8 42,9V39Q42,40.2 41.1,41.1Q40.2,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM9,9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/> +</vector> diff --git a/packages/SystemUI/res/drawable/overlay_badge_background.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml index 857632edcf0d..53122c17e320 100644 --- a/packages/SystemUI/res/drawable/overlay_badge_background.xml +++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ 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,8 +14,11 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:shape="oval"> - <solid android:color="?androidprv:attr/colorSurface"/> -</shape> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:pathData="M0,0M48,48"/> +</vector> diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml index bc97e511e7f4..8cf4f4de27da 100644 --- a/packages/SystemUI/res/layout/chipbar.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -23,6 +23,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"> + <!-- Extra marginBottom to give room for the drop shadow. --> <LinearLayout android:id="@+id/chipbar_inner" android:orientation="horizontal" @@ -33,6 +34,8 @@ android:layout_marginTop="20dp" android:layout_marginStart="@dimen/notification_side_paddings" android:layout_marginEnd="@dimen/notification_side_paddings" + android:translationZ="4dp" + android:layout_marginBottom="8dp" android:clipToPadding="false" android:gravity="center_vertical" android:alpha="0.0" diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 9134f96f59e1..eec3b11519b1 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -32,26 +32,26 @@ android:elevation="4dp" android:background="@drawable/action_chip_container_background" android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - app:layout_constraintBottom_toBottomOf="@+id/actions_container" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/actions_container" - app:layout_constraintEnd_toEndOf="@+id/actions_container"/> + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toBottomOf="parent"/> <HorizontalScrollView android:id="@+id/actions_container" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" - android:paddingEnd="@dimen/overlay_action_container_padding_right" + android:paddingEnd="@dimen/overlay_action_container_padding_end" android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:scrollbars="none" - android:layout_marginBottom="4dp" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_percent="1.0" app:layout_constraintWidth_max="wrap" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/preview_border" - app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> <LinearLayout android:id="@+id/actions" android:layout_width="wrap_content" @@ -69,44 +69,30 @@ android:id="@+id/preview_border" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="@dimen/overlay_offset_x" - android:layout_marginBottom="12dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginStart="@dimen/overlay_preview_container_margin" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="@dimen/overlay_preview_container_margin" android:elevation="7dp" - app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end" - app:layout_constraintTop_toTopOf="@id/clipboard_preview_top" - android:background="@drawable/overlay_border"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/clipboard_preview_end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierMargin="@dimen/overlay_border_width" - app:barrierDirection="end" - app:constraint_referenced_ids="clipboard_preview"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/clipboard_preview_top" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="top" - app:barrierMargin="@dimen/overlay_border_width_neg" - app:constraint_referenced_ids="clipboard_preview"/> + android:background="@drawable/overlay_border" + app:layout_constraintStart_toStartOf="@id/actions_container_background" + app:layout_constraintTop_toTopOf="@id/clipboard_preview" + app:layout_constraintEnd_toEndOf="@id/clipboard_preview" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/> <FrameLayout android:id="@+id/clipboard_preview" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" + android:layout_gravity="center" android:elevation="7dp" android:background="@drawable/overlay_preview_background" android:clipChildren="true" android:clipToOutline="true" android:clipToPadding="true" - android:layout_width="@dimen/clipboard_preview_size" - android:layout_margin="@dimen/overlay_border_width" - android:layout_height="wrap_content" - android:layout_gravity="center" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintBottom_toBottomOf="@id/preview_border" app:layout_constraintStart_toStartOf="@id/preview_border" - app:layout_constraintEnd_toEndOf="@id/preview_border" - app:layout_constraintTop_toTopOf="@id/preview_border"> + app:layout_constraintBottom_toBottomOf="@id/preview_border"> <TextView android:id="@+id/text_preview" android:textFontWeight="500" android:padding="8dp" diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index a565988c14ad..d68982876448 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -148,9 +148,4 @@ <include layout="@layout/ongoing_privacy_chip"/> </FrameLayout> - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:id="@+id/space" - /> </com.android.systemui.util.NoRemeasureMotionLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml index 9add32c6ee0a..885e5e2d4441 100644 --- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml +++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml @@ -57,6 +57,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_alarm" android:tint="@android:color/white" android:visibility="gone" @@ -67,6 +68,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_qs_dnd_on" android:tint="@android:color/white" android:visibility="gone" @@ -77,6 +79,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_signal_wifi_off" android:visibility="gone" android:contentDescription="@string/dream_overlay_status_bar_wifi_off" /> diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 95aefab328df..abc83379950a 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -147,6 +147,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" /> + <!-- Explicit Indicator --> + <com.android.internal.widget.CachingIconView + android:id="@+id/media_explicit_indicator" + android:layout_width="@dimen/qs_media_explicit_indicator_icon_size" + android:layout_height="@dimen/qs_media_explicit_indicator_icon_size" + android:src="@drawable/ic_media_explicit_indicator" + /> + <!-- Artist name --> <TextView android:id="@+id/header_artist" diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml index e4e0bd45a2db..496eb6e6130e 100644 --- a/packages/SystemUI/res/layout/screenshot_static.xml +++ b/packages/SystemUI/res/layout/screenshot_static.xml @@ -27,26 +27,26 @@ android:elevation="4dp" android:background="@drawable/action_chip_container_background" android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - app:layout_constraintBottom_toBottomOf="@+id/actions_container" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/actions_container" - app:layout_constraintEnd_toEndOf="@+id/actions_container"/> + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"/> <HorizontalScrollView android:id="@+id/actions_container" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" - android:layout_marginBottom="4dp" - android:paddingEnd="@dimen/overlay_action_container_padding_right" + android:paddingEnd="@dimen/overlay_action_container_padding_end" android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:scrollbars="none" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_percent="1.0" app:layout_constraintWidth_max="wrap" - app:layout_constraintBottom_toTopOf="@id/screenshot_message_container" app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border" - app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> <LinearLayout android:id="@+id/screenshot_actions" android:layout_width="wrap_content" @@ -64,35 +64,24 @@ android:id="@+id/screenshot_preview_border" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="@dimen/overlay_offset_x" - android:layout_marginBottom="12dp" + android:layout_marginStart="@dimen/overlay_preview_container_margin" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="@dimen/overlay_preview_container_margin" android:elevation="7dp" android:alpha="0" android:background="@drawable/overlay_border" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toTopOf="@id/screenshot_message_container" - app:layout_constraintEnd_toEndOf="@id/screenshot_preview_end" - app:layout_constraintTop_toTopOf="@id/screenshot_preview_top"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/screenshot_preview_end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierMargin="@dimen/overlay_border_width" - app:barrierDirection="end" - app:constraint_referenced_ids="screenshot_preview"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/screenshot_preview_top" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="top" - app:barrierMargin="@dimen/overlay_border_width_neg" - app:constraint_referenced_ids="screenshot_preview"/> + app:layout_constraintStart_toStartOf="@id/actions_container_background" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/> <ImageView android:id="@+id/screenshot_preview" android:visibility="invisible" android:layout_width="@dimen/overlay_x_scale" - android:layout_margin="@dimen/overlay_border_width" android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" android:layout_gravity="center" android:elevation="7dp" android:contentDescription="@string/screenshot_edit_description" @@ -100,20 +89,14 @@ android:background="@drawable/overlay_preview_background" android:adjustViewBounds="true" android:clickable="true" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" app:layout_constraintStart_toStartOf="@id/screenshot_preview_border" - app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border" - app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/> + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/> <ImageView android:id="@+id/screenshot_badge" - android:layout_width="24dp" - android:layout_height="24dp" - android:padding="4dp" + android:layout_width="48dp" + android:layout_height="48dp" android:visibility="gone" - android:background="@drawable/overlay_badge_background" android:elevation="8dp" - android:src="@drawable/overlay_cancel" app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/> <FrameLayout @@ -150,7 +133,7 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal" android:layout_marginVertical="4dp" - android:paddingHorizontal="@dimen/overlay_action_container_padding_right" + android:paddingHorizontal="@dimen/overlay_action_container_padding_end" android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:background="@drawable/action_chip_container_background" diff --git a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml index fa9d7390dcf8..7eaed4356f46 100644 --- a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml +++ b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml @@ -46,7 +46,7 @@ app:layout_constraintEnd_toEndOf="parent" app:flow_horizontalBias="0.5" app:flow_verticalAlign="center" - app:flow_wrapMode="chain" + app:flow_wrapMode="chain2" app:flow_horizontalGap="@dimen/user_switcher_fullscreen_horizontal_gap" app:flow_verticalGap="44dp" app:flow_horizontalStyle="packed"/> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 7f45e5eb047f..3c2453e4c59c 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -671,6 +671,16 @@ <item>17</item> <!-- WAKE_REASON_BIOMETRIC --> </integer-array> + <!-- Whether to support posture listening for face auth, default is 0(DEVICE_POSTURE_UNKNOWN) + means systemui will try listening on all postures. + 0 : DEVICE_POSTURE_UNKNOWN + 1 : DEVICE_POSTURE_CLOSED + 2 : DEVICE_POSTURE_HALF_OPENED + 3 : DEVICE_POSTURE_OPENED + 4 : DEVICE_POSTURE_FLIPPED + --> + <integer name="config_face_auth_supported_posture">0</integer> + <!-- Whether the communal service should be enabled --> <bool name="config_communalServiceEnabled">false</bool> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ae7ab9e199e4..af6e6467039a 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -334,15 +334,22 @@ <dimen name="overlay_action_chip_spacing">8dp</dimen> <dimen name="overlay_action_chip_text_size">14sp</dimen> <dimen name="overlay_offset_x">16dp</dimen> + <!-- Used for both start and bottom margin of the preview, relative to the action container --> + <dimen name="overlay_preview_container_margin">8dp</dimen> <dimen name="overlay_action_container_margin_horizontal">8dp</dimen> + <dimen name="overlay_action_container_margin_bottom">4dp</dimen> <dimen name="overlay_bg_protection_height">242dp</dimen> <dimen name="overlay_action_container_corner_radius">18dp</dimen> <dimen name="overlay_action_container_padding_vertical">4dp</dimen> <dimen name="overlay_action_container_padding_right">8dp</dimen> + <dimen name="overlay_action_container_padding_end">8dp</dimen> <dimen name="overlay_dismiss_button_tappable_size">48dp</dimen> <dimen name="overlay_dismiss_button_margin">8dp</dimen> + <!-- must be kept aligned with overlay_border_width_neg, below; + overlay_border_width = overlay_border_width_neg * -1 --> <dimen name="overlay_border_width">4dp</dimen> - <!-- need a negative margin for some of the constraints. should be overlay_border_width * -1 --> + <!-- some constraints use a negative margin. must be aligned with overlay_border_width, above; + overlay_border_width_neg = overlay_border_width * -1 --> <dimen name="overlay_border_width_neg">-4dp</dimen> <dimen name="clipboard_preview_size">@dimen/overlay_x_scale</dimen> @@ -1034,8 +1041,6 @@ <dimen name="ongoing_appops_dialog_side_padding">16dp</dimen> - <!-- Size of the RAT type for CellularTile --> - <!-- Size of media cards in the QSPanel carousel --> <dimen name="qs_media_padding">16dp</dimen> <dimen name="qs_media_album_radius">14dp</dimen> @@ -1050,6 +1055,7 @@ <dimen name="qs_media_disabled_seekbar_height">1dp</dimen> <dimen name="qs_media_enabled_seekbar_height">2dp</dimen> <dimen name="qs_media_app_icon_size">24dp</dimen> + <dimen name="qs_media_explicit_indicator_icon_size">13dp</dimen> <dimen name="qs_media_session_enabled_seekbar_vertical_padding">15dp</dimen> <dimen name="qs_media_session_disabled_seekbar_vertical_padding">16dp</dimen> @@ -1646,6 +1652,8 @@ <dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen> + <dimen name="dream_overlay_icon_inset_dimen">0dp</dimen> + <dimen name="dream_overlay_status_bar_marginTop">22dp</dimen> <!-- Default device corner radius, used for assist UI --> <dimen name="config_rounded_mask_size">0px</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 61a6e9d5df19..e4f339af9bcb 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2470,6 +2470,8 @@ <string name="media_transfer_failed">Something went wrong. Try again.</string> <!-- Text to indicate that a media transfer is currently in-progress, aka loading. [CHAR LIMIT=NONE] --> <string name="media_transfer_loading">Loading</string> + <!-- Default name of the device. [CHAR LIMIT=30] --> + <string name="media_ttt_default_device_type">tablet</string> <!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] --> <string name="controls_error_timeout">Inactive, check app</string> @@ -2518,6 +2520,8 @@ <string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string> <!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] --> <string name="media_output_group_title_speakers_and_displays">Speakers & Displays</string> + <!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] --> + <string name="media_output_group_title_suggested_device">Suggested Devices</string> <!-- Media Output Broadcast Dialog --> <!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] --> @@ -2887,6 +2891,9 @@ <!-- Text for education page content description for unfolded animation. [CHAR_LIMIT=NONE] --> <string name="rear_display_accessibility_unfolded_animation">Foldable device being flipped around</string> - <!-- Title for notification of low stylus battery. [CHAR_LIMIT=NONE] --> - <string name="stylus_battery_low">Stylus battery low</string> + <!-- Title for notification of low stylus battery with percentage. "percentage" is + the value of the battery capacity remaining [CHAR LIMIT=none]--> + <string name="stylus_battery_low_percentage"><xliff:g id="percentage" example="16%">%s</xliff:g> battery remaining</string> + <!-- Subtitle for the notification sent when a stylus battery is low. [CHAR LIMIT=none]--> + <string name="stylus_battery_low_subtitle">Connect your stylus to a charger</string> </resources> diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml index 1eb621e0368b..d9c81af54a12 100644 --- a/packages/SystemUI/res/xml/media_session_collapsed.xml +++ b/packages/SystemUI/res/xml/media_session_collapsed.xml @@ -66,6 +66,21 @@ app:layout_constraintTop_toBottomOf="@id/icon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintHorizontal_bias="0" /> + + <Constraint + android:id="@+id/media_explicit_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_info_spacing" + android:layout_marginBottom="@dimen/qs_media_padding" + android:layout_marginTop="0dp" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toStartOf="@id/header_artist" + app:layout_constraintTop_toTopOf="@id/header_artist" + app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="packed" /> + <Constraint android:id="@+id/header_artist" android:layout_width="wrap_content" @@ -75,9 +90,8 @@ app:layout_constraintEnd_toStartOf="@id/action_button_guideline" app:layout_constrainedWidth="true" app:layout_constraintTop_toBottomOf="@id/header_title" - app:layout_constraintStart_toStartOf="@id/header_title" - app:layout_constraintVertical_bias="0" - app:layout_constraintHorizontal_bias="0" /> + app:layout_constraintStart_toEndOf="@id/media_explicit_indicator" + app:layout_constraintVertical_bias="0" /> <Constraint android:id="@+id/actionPlayPause" diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml index 7de0a5e0e8c4..0cdc0f9505bc 100644 --- a/packages/SystemUI/res/xml/media_session_expanded.xml +++ b/packages/SystemUI/res/xml/media_session_expanded.xml @@ -58,6 +58,21 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toTopOf="@id/header_artist" app:layout_constraintHorizontal_bias="0" /> + + <Constraint + android:id="@+id/media_explicit_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_info_spacing" + android:layout_marginBottom="@dimen/qs_media_padding" + android:layout_marginTop="0dp" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toStartOf="@id/header_artist" + app:layout_constraintTop_toTopOf="@id/header_artist" + app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="packed"/> + <Constraint android:id="@+id/header_artist" android:layout_width="wrap_content" @@ -67,10 +82,9 @@ android:layout_marginTop="0dp" app:layout_constrainedWidth="true" app:layout_constraintEnd_toStartOf="@id/actionPlayPause" - app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintStart_toEndOf="@id/media_explicit_indicator" app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" - app:layout_constraintVertical_bias="0" - app:layout_constraintHorizontal_bias="0" /> + app:layout_constraintVertical_bias="0" /> <Constraint android:id="@+id/actionPlayPause" diff --git a/packages/SystemUI/res/xml/qs_header.xml b/packages/SystemUI/res/xml/qs_header.xml index eca2b2acb079..d97031f35d6b 100644 --- a/packages/SystemUI/res/xml/qs_header.xml +++ b/packages/SystemUI/res/xml/qs_header.xml @@ -56,13 +56,9 @@ <Layout android:layout_width="wrap_content" android:layout_height="@dimen/new_qs_header_non_clickable_element_height" - app:layout_constrainedWidth="true" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/space" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/carrier_group" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintHorizontal_chainStyle="spread_inside" /> </Constraint> @@ -87,39 +83,27 @@ <Constraint android:id="@+id/statusIcons"> <Layout - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="@dimen/new_qs_header_non_clickable_element_height" - app:layout_constraintStart_toEndOf="@id/space" + app:layout_constraintWidth_default="wrap" + app:layout_constraintStart_toEndOf="@id/date" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" app:layout_constraintTop_toTopOf="@id/date" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintHorizontal_bias="1" + app:layout_constraintBottom_toBottomOf="@id/date" /> </Constraint> <Constraint android:id="@+id/batteryRemainingIcon"> <Layout - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + app:layout_constraintWidth_default="wrap" app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" - app:layout_constraintStart_toEndOf="@id/statusIcons" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/date" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintHorizontal_bias="1" - app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintBottom_toBottomOf="@id/date" /> </Constraint> - - <Constraint - android:id="@id/space"> - <Layout - android:layout_width="0dp" - android:layout_height="0dp" - app:layout_constraintStart_toEndOf="@id/date" - app:layout_constraintEnd_toStartOf="@id/statusIcons" - /> - </Constraint> </ConstraintSet>
\ No newline at end of file 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 index 25d272185bc0..9b73cc3ea9f8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt @@ -48,48 +48,28 @@ constructor( val drawableInsetSize: Int try { val keyShadowBlur = - attributes.getDimensionPixelSize(R.styleable.DoubleShadowTextView_keyShadowBlur, 0) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowBlur, 0f) val keyShadowOffsetX = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_keyShadowOffsetX, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetX, 0f) val keyShadowOffsetY = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_keyShadowOffsetY, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetY, 0f) val keyShadowAlpha = attributes.getFloat(R.styleable.DoubleShadowTextView_keyShadowAlpha, 0f) mKeyShadowInfo = - ShadowInfo( - keyShadowBlur.toFloat(), - keyShadowOffsetX.toFloat(), - keyShadowOffsetY.toFloat(), - keyShadowAlpha - ) + ShadowInfo(keyShadowBlur, keyShadowOffsetX, keyShadowOffsetY, keyShadowAlpha) val ambientShadowBlur = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowBlur, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowBlur, 0f) val ambientShadowOffsetX = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowOffsetX, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetX, 0f) val ambientShadowOffsetY = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowOffsetY, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetY, 0f) val ambientShadowAlpha = attributes.getFloat(R.styleable.DoubleShadowTextView_ambientShadowAlpha, 0f) mAmbientShadowInfo = ShadowInfo( - ambientShadowBlur.toFloat(), - ambientShadowOffsetX.toFloat(), - ambientShadowOffsetY.toFloat(), + ambientShadowBlur, + ambientShadowOffsetX, + ambientShadowOffsetY, ambientShadowAlpha ) drawableSize = diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 8f38e5800015..a45ce422dca5 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -38,9 +38,11 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.log.dagger.KeyguardClockLog +import com.android.systemui.log.dagger.KeyguardSmallClockLog +import com.android.systemui.log.dagger.KeyguardLargeClockLog import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG import com.android.systemui.shared.regionsampling.RegionSampler import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -73,16 +75,18 @@ open class ClockEventController @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor, @Background private val bgExecutor: Executor, - @KeyguardClockLog private val logBuffer: LogBuffer?, + @KeyguardSmallClockLog private val smallLogBuffer: LogBuffer?, + @KeyguardLargeClockLog private val largeLogBuffer: LogBuffer?, private val featureFlags: FeatureFlags ) { var clock: ClockController? = null set(value) { field = value if (value != null) { - if (logBuffer != null) { - value.setLogBuffer(logBuffer) - } + smallLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" }) + value.smallClock.logBuffer = smallLogBuffer + largeLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" }) + value.largeClock.logBuffer = largeLogBuffer value.initialize(resources, dozeAmount, 0f) updateRegionSamplers(value) @@ -325,4 +329,8 @@ open class ClockEventController @Inject constructor( } } } + + companion object { + private val TAG = ClockEventController::class.simpleName!! + } } diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt index 5bb9367fa4a5..e0cf7b6a2bc4 100644 --- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt +++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt @@ -50,6 +50,7 @@ import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_RESET import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_VISIBILITY_CHANGED import com.android.keyguard.InternalFaceAuthReasons.NON_STRONG_BIOMETRIC_ALLOWED_CHANGED import com.android.keyguard.InternalFaceAuthReasons.OCCLUDING_APP_REQUESTED +import com.android.keyguard.InternalFaceAuthReasons.POSTURE_CHANGED import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN import com.android.keyguard.InternalFaceAuthReasons.RETRY_AFTER_HW_UNAVAILABLE @@ -126,6 +127,7 @@ private object InternalFaceAuthReasons { const val STRONG_AUTH_ALLOWED_CHANGED = "Face auth stopped because strong auth allowed changed" const val NON_STRONG_BIOMETRIC_ALLOWED_CHANGED = "Face auth stopped because non strong biometric allowed changed" + const val POSTURE_CHANGED = "Face auth started/stopped due to device posture changed." } /** @@ -173,6 +175,7 @@ constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) : return PowerManager.wakeReasonToString(extraInfo) } }, + @UiEvent(doc = POSTURE_CHANGED) FACE_AUTH_UPDATED_POSTURE_CHANGED(1265, POSTURE_CHANGED), @Deprecated( "Not a face auth trigger.", ReplaceWith( diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 62babadc45d8..4acbb0aaf1d8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -7,7 +7,6 @@ import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -20,11 +19,15 @@ import com.android.keyguard.dagger.KeyguardStatusViewScope; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.plugins.ClockController; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogLevel; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import kotlin.Unit; + /** * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. */ @@ -87,6 +90,7 @@ public class KeyguardClockSwitch extends RelativeLayout { private int mClockSwitchYAmount; @VisibleForTesting boolean mChildrenAreLaidOut = false; @VisibleForTesting boolean mAnimateOnLayout = true; + private LogBuffer mLogBuffer = null; public KeyguardClockSwitch(Context context, AttributeSet attrs) { super(context, attrs); @@ -113,6 +117,14 @@ public class KeyguardClockSwitch extends RelativeLayout { onDensityOrFontScaleChanged(); } + public void setLogBuffer(LogBuffer logBuffer) { + mLogBuffer = logBuffer; + } + + public LogBuffer getLogBuffer() { + return mLogBuffer; + } + void setClock(ClockController clock, int statusBarState) { mClock = clock; @@ -121,12 +133,16 @@ public class KeyguardClockSwitch extends RelativeLayout { mLargeClockFrame.removeAllViews(); if (clock == null) { - Log.e(TAG, "No clock being shown"); + if (mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.ERROR, "No clock being shown"); + } return; } // Attach small and big clock views to hierarchy. - Log.i(TAG, "Attached new clock views to switch"); + if (mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.INFO, "Attached new clock views to switch"); + } mSmallClockFrame.addView(clock.getSmallClock().getView()); mLargeClockFrame.addView(clock.getLargeClock().getView()); updateClockTargetRegions(); @@ -152,8 +168,18 @@ public class KeyguardClockSwitch extends RelativeLayout { } private void updateClockViews(boolean useLargeClock, boolean animate) { - Log.i(TAG, "updateClockViews; useLargeClock=" + useLargeClock + "; animate=" + animate - + "; mChildrenAreLaidOut=" + mChildrenAreLaidOut); + if (mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.DEBUG, (msg) -> { + msg.setBool1(useLargeClock); + msg.setBool2(animate); + msg.setBool3(mChildrenAreLaidOut); + return Unit.INSTANCE; + }, (msg) -> "updateClockViews" + + "; useLargeClock=" + msg.getBool1() + + "; animate=" + msg.getBool2() + + "; mChildrenAreLaidOut=" + msg.getBool3()); + } + if (mClockInAnim != null) mClockInAnim.cancel(); if (mClockOutAnim != null) mClockOutAnim.cancel(); if (mStatusAreaAnim != null) mStatusAreaAnim.cancel(); @@ -183,6 +209,7 @@ public class KeyguardClockSwitch extends RelativeLayout { if (!animate) { out.setAlpha(0f); + out.setVisibility(INVISIBLE); in.setAlpha(1f); in.setVisibility(VISIBLE); mStatusArea.setTranslationY(statusAreaYTranslation); @@ -198,7 +225,10 @@ public class KeyguardClockSwitch extends RelativeLayout { direction * -mClockSwitchYAmount)); mClockOutAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mClockOutAnim = null; + if (mClockOutAnim == animation) { + out.setVisibility(INVISIBLE); + mClockOutAnim = null; + } } }); @@ -212,7 +242,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mClockInAnim.setStartDelay(CLOCK_OUT_MILLIS / 2); mClockInAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mClockInAnim = null; + if (mClockInAnim == animation) { + mClockInAnim = null; + } } }); @@ -225,7 +257,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); mStatusAreaAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mStatusAreaAnim = null; + if (mStatusAreaAnim == animation) { + mStatusAreaAnim = null; + } } }); mStatusAreaAnim.start(); @@ -269,7 +303,9 @@ public class KeyguardClockSwitch extends RelativeLayout { public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardClockSwitch:"); pw.println(" mSmallClockFrame: " + mSmallClockFrame); + pw.println(" mSmallClockFrame.alpha: " + mSmallClockFrame.getAlpha()); pw.println(" mLargeClockFrame: " + mLargeClockFrame); + pw.println(" mLargeClockFrame.alpha: " + mLargeClockFrame.getAlpha()); pw.println(" mStatusArea: " + mStatusArea); pw.println(" mDisplayedClockSize: " + mDisplayedClockSize); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 6ce84a94cc87..08567a76f741 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -38,8 +38,11 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.log.dagger.KeyguardClockLog; import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.ClockRegistry; import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; @@ -62,6 +65,8 @@ import javax.inject.Inject; */ public class KeyguardClockSwitchController extends ViewController<KeyguardClockSwitch> implements Dumpable { + private static final String TAG = "KeyguardClockSwitchController"; + private final StatusBarStateController mStatusBarStateController; private final ClockRegistry mClockRegistry; private final KeyguardSliceViewController mKeyguardSliceViewController; @@ -70,6 +75,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final SecureSettings mSecureSettings; private final DumpManager mDumpManager; private final ClockEventController mClockEventController; + private final LogBuffer mLogBuffer; private FrameLayout mSmallClockFrame; // top aligned clock private FrameLayout mLargeClockFrame; // centered clock @@ -119,7 +125,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS SecureSettings secureSettings, @Main Executor uiExecutor, DumpManager dumpManager, - ClockEventController clockEventController) { + ClockEventController clockEventController, + @KeyguardClockLog LogBuffer logBuffer) { super(keyguardClockSwitch); mStatusBarStateController = statusBarStateController; mClockRegistry = clockRegistry; @@ -131,6 +138,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mKeyguardUnlockAnimationController = keyguardUnlockAnimationController; mDumpManager = dumpManager; mClockEventController = clockEventController; + mLogBuffer = logBuffer; + mView.setLogBuffer(mLogBuffer); mClockChangedListener = () -> { setClock(mClockRegistry.createCurrentClock()); @@ -337,10 +346,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS int clockHeight = clock.getLargeClock().getView().getHeight(); return frameHeight / 2 + clockHeight / 2 + mKeyguardLargeClockTopMargin / -2; } else { - // This is only called if we've never shown the large clock as the frame is inflated - // with 'gone', but then the visibility is never set when it is animated away by - // KeyguardClockSwitch, instead it is removed from the view hierarchy. - // TODO(b/261755021): Cleanup Large Frame Visibility int clockHeight = clock.getSmallClock().getView().getHeight(); return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin; } @@ -358,15 +363,11 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS if (mLargeClockFrame.getVisibility() == View.VISIBLE) { return clock.getLargeClock().getView().getHeight(); } else { - // Is not called except in certain edge cases, see comment in getClockBottom - // TODO(b/261755021): Cleanup Large Frame Visibility return clock.getSmallClock().getView().getHeight(); } } boolean isClockTopAligned() { - // Returns false except certain edge cases, see comment in getClockBottom - // TODO(b/261755021): Cleanup Large Frame Visibility return mLargeClockFrame.getVisibility() != View.VISIBLE; } @@ -378,6 +379,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void setClock(ClockController clock) { + if (clock != null && mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.INFO, "New Clock"); + } + mClockEventController.setClock(clock); mView.setClock(clock, mStatusBarStateController.getState()); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt index deead1959b8a..1a06b5f1c767 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt @@ -39,6 +39,7 @@ data class KeyguardFaceListenModel( var keyguardGoingAway: Boolean = false, var listeningForFaceAssistant: Boolean = false, var occludingAppRequestingFaceAuth: Boolean = false, + val postureAllowsListening: Boolean = false, var primaryUser: Boolean = false, var secureCameraLaunched: Boolean = false, var supportsDetect: Boolean = false, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 4e10bffc381d..9d6bb087288b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -63,11 +63,13 @@ import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_RE import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_KEYGUARD_INIT; +import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_POSTURE_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING; import static com.android.systemui.DejankUtils.whitelistIpcs; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import android.annotation.AnyThread; import android.annotation.MainThread; @@ -154,6 +156,7 @@ import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; +import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.Assert; import com.android.systemui.util.settings.SecureSettings; @@ -341,6 +344,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final TrustManager mTrustManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; + private final DevicePostureController mPostureController; private final BroadcastDispatcher mBroadcastDispatcher; private final SecureSettings mSecureSettings; private final InteractionJankMonitor mInteractionJankMonitor; @@ -358,6 +362,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final FaceManager mFaceManager; private final LockPatternUtils mLockPatternUtils; private final boolean mWakeOnFingerprintAcquiredStart; + @VisibleForTesting + @DevicePostureController.DevicePostureInt + protected int mConfigFaceAuthSupportedPosture; private KeyguardBypassController mKeyguardBypassController; private List<SubscriptionInfo> mSubscriptionInfo; @@ -368,6 +375,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private boolean mLogoutEnabled; private boolean mIsFaceEnrolled; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + private int mPostureState = DEVICE_POSTURE_UNKNOWN; private FingerprintInteractiveToAuthProvider mFingerprintInteractiveToAuthProvider; /** @@ -696,8 +704,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab */ public void setKeyguardGoingAway(boolean goingAway) { mKeyguardGoingAway = goingAway; - // This is set specifically to stop face authentication from running. - updateBiometricListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY); + if (mKeyguardGoingAway) { + updateFaceListeningState(BIOMETRIC_ACTION_STOP, + FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY); + } + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } /** @@ -1776,6 +1787,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab }; @VisibleForTesting + final DevicePostureController.Callback mPostureCallback = + new DevicePostureController.Callback() { + @Override + public void onPostureChanged(int posture) { + mPostureState = posture; + updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_UPDATED_POSTURE_CHANGED); + } + }; + + @VisibleForTesting CancellationSignal mFingerprintCancelSignal; @VisibleForTesting CancellationSignal mFaceCancelSignal; @@ -1935,9 +1957,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab cb.onFinishedGoingToSleep(arg1); } } - // This is set specifically to stop face authentication from running. - updateBiometricListeningState(BIOMETRIC_ACTION_STOP, + updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP); + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } private void handleScreenTurnedOff() { @@ -2041,6 +2063,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Nullable FingerprintManager fingerprintManager, @Nullable BiometricManager biometricManager, FaceWakeUpTriggersConfig faceWakeUpTriggersConfig, + DevicePostureController devicePostureController, Optional<FingerprintInteractiveToAuthProvider> interactiveToAuthProvider) { mContext = context; mSubscriptionManager = subscriptionManager; @@ -2070,6 +2093,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mDreamManager = dreamManager; mTelephonyManager = telephonyManager; mDevicePolicyManager = devicePolicyManager; + mPostureController = devicePostureController; mPackageManager = packageManager; mFpm = fingerprintManager; mFaceManager = faceManager; @@ -2081,6 +2105,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab R.array.config_face_acquire_device_entry_ignorelist)) .boxed() .collect(Collectors.toSet()); + mConfigFaceAuthSupportedPosture = mContext.getResources().getInteger( + R.integer.config_face_auth_supported_posture); mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig; mHandler = new Handler(mainLooper) { @@ -2272,6 +2298,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED)); } }); + if (mConfigFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) { + mPostureController.addCallback(mPostureCallback); + } updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ON_KEYGUARD_INIT); TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); @@ -2704,7 +2733,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFingerprintInteractiveToAuthProvider != null && mFingerprintInteractiveToAuthProvider.isEnabled(getCurrentUser()); shouldListenSideFpsState = - interactiveToAuthEnabled ? isDeviceInteractive() : true; + interactiveToAuthEnabled ? isDeviceInteractive() && !mGoingToSleep : true; } boolean shouldListen = shouldListenKeyguardState && shouldListenUserState @@ -2716,7 +2745,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab user, shouldListen, biometricEnabledForUser, - mPrimaryBouncerIsOrWillBeShowing, + mPrimaryBouncerIsOrWillBeShowing, userCanSkipBouncer, mCredentialAttempted, mDeviceInteractive, @@ -2776,6 +2805,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user); final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant(); final boolean isUdfpsFingerDown = mAuthController.isUdfpsFingerDown(); + final boolean isPostureAllowedForFaceAuth = + mConfigFaceAuthSupportedPosture == 0 /* DEVICE_POSTURE_UNKNOWN */ ? true + : (mPostureState == mConfigFaceAuthSupportedPosture); // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware. @@ -2792,7 +2824,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && faceAuthAllowedOrDetectionIsNeeded && mIsPrimaryUser && (!mSecureCameraLaunched || mOccludingAppRequestingFace) && faceAndFpNotAuthenticated - && !mGoingToSleep; + && !mGoingToSleep + && isPostureAllowedForFaceAuth; // Aggregate relevant fields for debug logging. logListenerModelData( @@ -2812,6 +2845,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mKeyguardGoingAway, shouldListenForFaceAssistant, mOccludingAppRequestingFace, + isPostureAllowedForFaceAuth, mIsPrimaryUser, mSecureCameraLaunched, supportsDetect, @@ -2897,7 +2931,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab getKeyguardSessionId(), faceAuthUiEvent.getExtraInfo() ); - + mLogger.logFaceUnlockPossible(unlockPossible); if (unlockPossible) { mFaceCancelSignal = new CancellationSignal(); diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt index b106fec11eb5..2c7eceba48a2 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -17,36 +17,46 @@ package com.android.keyguard.logging import com.android.systemui.log.dagger.KeyguardLog -import com.android.systemui.plugins.log.ConstantStringsLogger -import com.android.systemui.plugins.log.ConstantStringsLoggerImpl import com.android.systemui.plugins.log.LogBuffer -import com.android.systemui.plugins.log.LogLevel.DEBUG -import com.android.systemui.plugins.log.LogLevel.ERROR -import com.android.systemui.plugins.log.LogLevel.INFO -import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogLevel import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject -private const val TAG = "KeyguardLog" +private const val BIO_TAG = "KeyguardLog" /** * Generic logger for keyguard that's wrapping [LogBuffer]. This class should be used for adding * temporary logs or logs for smaller classes when creating whole new [LogBuffer] wrapper might be * an overkill. */ -class KeyguardLogger @Inject constructor(@KeyguardLog val buffer: LogBuffer) : - ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) { - - fun logException(ex: Exception, @CompileTimeConstant logMsg: String) { - buffer.log(TAG, ERROR, {}, { logMsg }, exception = ex) - } - - fun v(msg: String, arg: Any) { - buffer.log(TAG, VERBOSE, { str1 = arg.toString() }, { "$msg: $str1" }) - } +class KeyguardLogger +@Inject +constructor( + @KeyguardLog val buffer: LogBuffer, +) { + @JvmOverloads + fun log( + tag: String, + level: LogLevel, + @CompileTimeConstant msg: String, + ex: Throwable? = null, + ) = buffer.log(tag, level, msg, ex) - fun i(msg: String, arg: Any) { - buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" }) + fun log( + tag: String, + level: LogLevel, + @CompileTimeConstant msg: String, + arg: Any, + ) { + buffer.log( + tag, + level, + { + str1 = msg + str2 = arg.toString() + }, + { "$str1: $str2" } + ) } @JvmOverloads @@ -56,8 +66,8 @@ class KeyguardLogger @Inject constructor(@KeyguardLog val buffer: LogBuffer) : msg: String? = null ) { buffer.log( - TAG, - DEBUG, + BIO_TAG, + LogLevel.DEBUG, { str1 = context str2 = "$msgId" diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 21d3b24174b6..5b4245595be9 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -132,6 +132,12 @@ class KeyguardUpdateMonitorLogger @Inject constructor( logBuffer.log(TAG, DEBUG, { int1 = faceRunningState }, { "faceRunningState: $int1" }) } + fun logFaceUnlockPossible(isFaceUnlockPossible: Boolean) { + logBuffer.log(TAG, DEBUG, + { bool1 = isFaceUnlockPossible }, + {"isUnlockWithFacePossible: $bool1"}) + } + fun logFingerprintAuthForWrongUser(authUserId: Int) { logBuffer.log(TAG, DEBUG, { int1 = authUserId }, diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java index 0fc9ef96f6e9..632fcdc16259 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java @@ -22,8 +22,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.util.Log; -import androidx.annotation.Nullable; - import com.android.systemui.dagger.GlobalRootComponent; import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dagger.WMComponent; @@ -55,7 +53,6 @@ public abstract class SystemUIInitializer { mContext = context; } - @Nullable protected abstract GlobalRootComponent.Builder getGlobalRootComponentBuilder(); /** @@ -72,11 +69,6 @@ public abstract class SystemUIInitializer { * Starts the initialization process. This stands up the Dagger graph. */ public void init(boolean fromTest) throws ExecutionException, InterruptedException { - GlobalRootComponent.Builder globalBuilder = getGlobalRootComponentBuilder(); - if (globalBuilder == null) { - return; - } - mRootComponent = getGlobalRootComponentBuilder() .context(mContext) .instrumentationTest(fromTest) @@ -127,7 +119,6 @@ public abstract class SystemUIInitializer { .setBackAnimation(Optional.ofNullable(null)) .setDesktopMode(Optional.ofNullable(null)); } - mSysUIComponent = builder.build(); if (initializeComponents) { mSysUIComponent.init(); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt index 55c095b0be25..8aa3040c6015 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui -import android.app.Application import android.content.Context import com.android.systemui.dagger.DaggerReferenceGlobalRootComponent import com.android.systemui.dagger.GlobalRootComponent @@ -25,17 +24,7 @@ import com.android.systemui.dagger.GlobalRootComponent * {@link SystemUIInitializer} that stands up AOSP SystemUI. */ class SystemUIInitializerImpl(context: Context) : SystemUIInitializer(context) { - - override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder? { - return when (Application.getProcessName()) { - SCREENSHOT_CROSS_PROFILE_PROCESS -> null - else -> DaggerReferenceGlobalRootComponent.builder() - } - } - - companion object { - private const val SYSTEMUI_PROCESS = "com.android.systemui" - private const val SCREENSHOT_CROSS_PROFILE_PROCESS = - "$SYSTEMUI_PROCESS:screenshot_cross_profile" + override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder { + return DaggerReferenceGlobalRootComponent.builder() } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index cfbde1531335..199e630885fd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -182,8 +182,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { private int mActivePointerId = -1; // The timestamp of the most recent touch log. private long mTouchLogTime; - // The timestamp of the most recent log of the UNCHANGED interaction. - private long mLastUnchangedInteractionTime; + // The timestamp of the most recent log of a touch InteractionEvent. + private long mLastTouchInteractionTime; // Sensor has a capture (good or bad) for this touch. No need to enable the UDFPS display mode // anymore for this particular touch event. In other words, do not enable the UDFPS mode until // the user touches the sensor area again. @@ -540,12 +540,12 @@ public class UdfpsController implements DozeReceiver, Dumpable { private void logBiometricTouch(InteractionEvent event, NormalizedTouchData data) { if (event == InteractionEvent.UNCHANGED) { - long sinceLastLog = mSystemClock.elapsedRealtime() - mLastUnchangedInteractionTime; + long sinceLastLog = mSystemClock.elapsedRealtime() - mLastTouchInteractionTime; if (sinceLastLog < MIN_UNCHANGED_INTERACTION_LOG_INTERVAL) { return; } - mLastUnchangedInteractionTime = mSystemClock.elapsedRealtime(); } + mLastTouchInteractionTime = mSystemClock.elapsedRealtime(); final int biometricTouchReportedTouchType = toBiometricTouchReportedTouchType(event); final InstanceId sessionIdProvider = mSessionTracker.getSessionId( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt index 857224290752..682d38a8f1a8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt @@ -18,6 +18,7 @@ package com.android.systemui.biometrics.udfps import android.graphics.Point import android.graphics.Rect +import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import kotlin.math.cos import kotlin.math.pow @@ -50,7 +51,8 @@ class EllipseOverlapDetector(private val neededPoints: Int = 2) : OverlapDetecto return result <= 1 } - private fun calculateSensorPoints(sensorBounds: Rect): List<Point> { + @VisibleForTesting + fun calculateSensorPoints(sensorBounds: Rect): List<Point> { val sensorX = sensorBounds.centerX() val sensorY = sensorBounds.centerY() val cornerOffset: Int = sensorBounds.width() / 4 diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt index 338bf66d197e..693f64a1f93d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt @@ -27,6 +27,8 @@ import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject +private val SUPPORTED_ROTATIONS = setOf(Surface.ROTATION_90, Surface.ROTATION_270) + /** * TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations. */ @@ -129,19 +131,27 @@ private fun MotionEvent.normalize( val nativeY = naturalTouch.y / overlayParams.scaleFactor val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor + var nativeOrientation: Float = getOrientation(pointerIndex) + if (SUPPORTED_ROTATIONS.contains(overlayParams.rotation)) { + nativeOrientation = toRadVerticalFromRotated(nativeOrientation.toDouble()).toFloat() + } return NormalizedTouchData( pointerId = getPointerId(pointerIndex), x = nativeX, y = nativeY, minor = nativeMinor, major = nativeMajor, - // TODO(b/259311354): touch orientation should be reported relative to Surface.ROTATION_O. - orientation = getOrientation(pointerIndex), + orientation = nativeOrientation, time = eventTime, gestureStart = downTime, ) } +private fun toRadVerticalFromRotated(rad: Double): Double { + val piBound = ((rad % Math.PI) + Math.PI / 2) % Math.PI + return if (piBound < Math.PI / 2.0) piBound else piBound - Math.PI +} + /** * Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device * is in the [Surface.ROTATION_0] orientation. @@ -152,7 +162,7 @@ private fun MotionEvent.rotateToNaturalOrientation( ): PointF { val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex)) val rot = overlayParams.rotation - if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { + if (SUPPORTED_ROTATIONS.contains(rot)) { RotationUtils.rotatePointF( touchPoint, RotationUtils.deltaRotation(rot, Surface.ROTATION_0), diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index e8e1f2e95f5d..e9ac840cf4f4 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -176,7 +176,8 @@ public class BrightLineFalsingManager implements FalsingManager { private @Classifier.InteractionType int mPriorInteractionType = Classifier.GENERIC; @Inject - public BrightLineFalsingManager(FalsingDataProvider falsingDataProvider, + public BrightLineFalsingManager( + FalsingDataProvider falsingDataProvider, MetricsLogger metricsLogger, @Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set<FalsingClassifier> classifiers, SingleTapClassifier singleTapClassifier, LongTapClassifier longTapClassifier, @@ -399,7 +400,9 @@ public class BrightLineFalsingManager implements FalsingManager { || mDataProvider.isJustUnlockedWithFace() || mDataProvider.isDocked() || mAccessibilityManager.isTouchExplorationEnabled() - || mDataProvider.isA11yAction(); + || mDataProvider.isA11yAction() + || (mFeatureFlags.isEnabled(Flags.FALSING_OFF_FOR_UNFOLDED) + && !mDataProvider.isFolded()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java index 09ebeeac163f..5f347c158818 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java @@ -16,6 +16,7 @@ package com.android.systemui.classifier; +import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; @@ -42,6 +43,7 @@ public class FalsingDataProvider { private final int mWidthPixels; private final int mHeightPixels; private BatteryController mBatteryController; + private final FoldStateListener mFoldStateListener; private final DockManager mDockManager; private final float mXdpi; private final float mYdpi; @@ -65,12 +67,14 @@ public class FalsingDataProvider { public FalsingDataProvider( DisplayMetrics displayMetrics, BatteryController batteryController, + FoldStateListener foldStateListener, DockManager dockManager) { mXdpi = displayMetrics.xdpi; mYdpi = displayMetrics.ydpi; mWidthPixels = displayMetrics.widthPixels; mHeightPixels = displayMetrics.heightPixels; mBatteryController = batteryController; + mFoldStateListener = foldStateListener; mDockManager = dockManager; FalsingClassifier.logInfo("xdpi, ydpi: " + getXdpi() + ", " + getYdpi()); @@ -376,6 +380,10 @@ public class FalsingDataProvider { return mBatteryController.isWirelessCharging() || mDockManager.isDocked(); } + public boolean isFolded() { + return Boolean.TRUE.equals(mFoldStateListener.getFolded()); + } + /** Implement to be alerted abotu the beginning and ending of falsing tracking. */ public interface SessionListener { /** Called when the lock screen is shown and falsing-tracking begins. */ diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java index f244cb009ba4..96bce4cd3cd9 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java @@ -19,6 +19,7 @@ package com.android.systemui.dreams; import android.annotation.IntDef; import android.annotation.Nullable; import android.content.Context; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -26,6 +27,9 @@ import android.view.ViewGroup; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.systemui.R; +import com.android.systemui.shared.shadow.DoubleShadowIconDrawable; +import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo; +import com.android.systemui.statusbar.AlphaOptimizedImageView; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -60,8 +64,15 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { public static final int STATUS_ICON_PRIORITY_MODE_ON = 6; private final Map<Integer, View> mStatusIcons = new HashMap<>(); + private Context mContext; private ViewGroup mSystemStatusViewGroup; private ViewGroup mExtraSystemStatusViewGroup; + private ShadowInfo mKeyShadowInfo; + private ShadowInfo mAmbientShadowInfo; + private int mDrawableSize; + private int mDrawableInsetSize; + private static final float KEY_SHADOW_ALPHA = 0.35f; + private static final float AMBIENT_SHADOW_ALPHA = 0.4f; public DreamOverlayStatusBarView(Context context) { this(context, null); @@ -73,6 +84,7 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { public DreamOverlayStatusBarView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); + mContext = context; } public DreamOverlayStatusBarView( @@ -80,14 +92,36 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { super(context, attrs, defStyleAttr, defStyleRes); } + @Override protected void onFinishInflate() { super.onFinishInflate(); + mKeyShadowInfo = createShadowInfo( + R.dimen.dream_overlay_status_bar_key_text_shadow_radius, + R.dimen.dream_overlay_status_bar_key_text_shadow_dx, + R.dimen.dream_overlay_status_bar_key_text_shadow_dy, + KEY_SHADOW_ALPHA + ); + + mAmbientShadowInfo = createShadowInfo( + R.dimen.dream_overlay_status_bar_ambient_text_shadow_radius, + R.dimen.dream_overlay_status_bar_ambient_text_shadow_dx, + R.dimen.dream_overlay_status_bar_ambient_text_shadow_dy, + AMBIENT_SHADOW_ALPHA + ); + + mDrawableSize = mContext + .getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_status_bar_icon_size); + mDrawableInsetSize = mContext + .getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_icon_inset_dimen); + mStatusIcons.put(STATUS_ICON_WIFI_UNAVAILABLE, - fetchStatusIconForResId(R.id.dream_overlay_wifi_status)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_wifi_status))); mStatusIcons.put(STATUS_ICON_ALARM_SET, - fetchStatusIconForResId(R.id.dream_overlay_alarm_set)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_alarm_set))); mStatusIcons.put(STATUS_ICON_CAMERA_DISABLED, fetchStatusIconForResId(R.id.dream_overlay_camera_off)); mStatusIcons.put(STATUS_ICON_MIC_DISABLED, @@ -97,7 +131,7 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { mStatusIcons.put(STATUS_ICON_NOTIFICATIONS, fetchStatusIconForResId(R.id.dream_overlay_notification_indicator)); mStatusIcons.put(STATUS_ICON_PRIORITY_MODE_ON, - fetchStatusIconForResId(R.id.dream_overlay_priority_mode)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_priority_mode))); mSystemStatusViewGroup = findViewById(R.id.dream_overlay_system_status); mExtraSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items); @@ -137,4 +171,34 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { } return false; } + + private View addDoubleShadow(View icon) { + if (icon instanceof AlphaOptimizedImageView) { + AlphaOptimizedImageView i = (AlphaOptimizedImageView) icon; + Drawable drawableIcon = i.getDrawable(); + i.setImageDrawable(new DoubleShadowIconDrawable( + mKeyShadowInfo, + mAmbientShadowInfo, + drawableIcon, + mDrawableSize, + mDrawableInsetSize + )); + } + return icon; + } + + private ShadowInfo createShadowInfo(int blurId, int offsetXId, int offsetYId, float alpha) { + return new ShadowInfo( + fetchDimensionForResId(blurId), + fetchDimensionForResId(offsetXId), + fetchDimensionForResId(offsetYId), + alpha + ); + } + + private Float fetchDimensionForResId(int resId) { + return mContext + .getResources() + .getDimension(resId); + } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index e4e8d59df066..d040f8fe6c61 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -114,8 +114,6 @@ object Flags { // ** Flag retired ** // public static final BooleanFlag KEYGUARD_LAYOUT = // new BooleanFlag(200, true); - // TODO(b/254512713): Tracking Bug - @JvmField val LOCKSCREEN_ANIMATIONS = releasedFlag(201, "lockscreen_animations") // TODO(b/254512750): Tracking Bug val NEW_UNLOCK_SWIPE_ANIMATION = releasedFlag(202, "new_unlock_swipe_animation") @@ -209,6 +207,9 @@ object Flags { val AUTO_PIN_CONFIRMATION = unreleasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation") + // TODO(b/262859270): Tracking Bug + @JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded") + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite") @@ -260,10 +261,11 @@ object Flags { // TODO(b/256614751): Tracking Bug val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND = - unreleasedFlag(608, "new_status_bar_mobile_icons_backend") + unreleasedFlag(608, "new_status_bar_mobile_icons_backend", teamfood = true) // TODO(b/256613548): Tracking Bug - val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend") + val NEW_STATUS_BAR_WIFI_ICON_BACKEND = + unreleasedFlag(609, "new_status_bar_wifi_icon_backend", teamfood = true) // TODO(b/256623670): Tracking Bug @JvmField @@ -302,7 +304,7 @@ object Flags { // 900 - media // TODO(b/254512697): Tracking Bug - val MEDIA_TAP_TO_TRANSFER = unreleasedFlag(900, "media_tap_to_transfer", teamfood = true) + val MEDIA_TAP_TO_TRANSFER = releasedFlag(900, "media_tap_to_transfer") // TODO(b/254512502): Tracking Bug val MEDIA_SESSION_ACTIONS = unreleasedFlag(901, "media_session_actions") @@ -332,13 +334,17 @@ object Flags { val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE = unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true) + // TODO(b/263512203): Tracking Bug + val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true) + // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") // TODO(b/254512758): Tracking Bug @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple") - val SHOW_LOWLIGHT_ON_DIRECT_BOOT = unreleasedFlag(1003, "show_lowlight_on_direct_boot") + // TODO(b/265045965): Tracking Bug + val SHOW_LOWLIGHT_ON_DIRECT_BOOT = releasedFlag(1003, "show_lowlight_on_direct_boot") // 1100 - windowing @Keep diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 6b121b84680c..18854e513bed 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -41,6 +41,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.ActivityTaskManager; import android.app.AlarmManager; +import android.app.BroadcastOptions; import android.app.PendingIntent; import android.app.StatusBarManager; import android.app.WindowConfiguration; @@ -391,6 +392,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS); + private static final Bundle USER_PRESENT_INTENT_OPTIONS = + BroadcastOptions.makeBasic() + .setDeferUntilActive(true) + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .toBundle(); + /** * {@link #setKeyguardEnabled} waits on this condition when it re-enables * the keyguard. @@ -1921,13 +1928,23 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, return; } - // if the keyguard is already showing, don't bother. check flags in both files - // to account for the hiding animation which results in a delay and discrepancy - // between flags + // If the keyguard is already showing, see if we don't need to bother re-showing it. Check + // flags in both files to account for the hiding animation which results in a delay and + // discrepancy between flags. if (mShowing && mKeyguardStateController.isShowing()) { - if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); - resetStateLocked(); - return; + if (mPM.isInteractive()) { + // It's already showing, and we're not trying to show it while the screen is off. + // We can simply reset all of the views. + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); + resetStateLocked(); + return; + } else { + // We are trying to show the keyguard while the screen is off - this results from + // race conditions involving locking while unlocking. Don't short-circuit here and + // ensure the keyguard is fully re-shown. + Log.e(TAG, + "doKeyguard: already showing, but re-showing since we're not interactive"); + } } // In split system user mode, we never unlock system user. @@ -2319,7 +2336,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Context.USER_SERVICE); mUiBgExecutor.execute(() -> { for (int profileId : um.getProfileIdsWithDisabled(currentUser.getIdentifier())) { - mContext.sendBroadcastAsUser(USER_PRESENT_INTENT, UserHandle.of(profileId)); + mContext.sendBroadcastAsUser(USER_PRESENT_INTENT, + UserHandle.of(profileId), + null, + USER_PRESENT_INTENT_OPTIONS); } mLockPatternUtils.userPresent(currentUserId); }); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java index 017b65acd1d2..ffd8a0244a86 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java @@ -33,6 +33,7 @@ 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.util.time.SystemClock; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -63,6 +64,7 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe private final Context mContext; private final DisplayMetrics mDisplayMetrics; + private final SystemClock mSystemClock; @Nullable private final IWallpaperManager mWallpaperManagerService; @@ -71,6 +73,9 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe private @PowerManager.WakeReason int mLastWakeReason = PowerManager.WAKE_REASON_UNKNOWN; + public static final long UNKNOWN_LAST_WAKE_TIME = -1; + private long mLastWakeTime = UNKNOWN_LAST_WAKE_TIME; + @Nullable private Point mLastWakeOriginLocation = null; @@ -84,10 +89,12 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe public WakefulnessLifecycle( Context context, @Nullable IWallpaperManager wallpaperManagerService, + SystemClock systemClock, DumpManager dumpManager) { mContext = context; mDisplayMetrics = context.getResources().getDisplayMetrics(); mWallpaperManagerService = wallpaperManagerService; + mSystemClock = systemClock; dumpManager.registerDumpable(getClass().getSimpleName(), this); } @@ -104,6 +111,14 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe } /** + * Returns the most recent time (in device uptimeMillis) the display woke up. + * Returns {@link UNKNOWN_LAST_WAKE_TIME} if there hasn't been a wakeup yet. + */ + public long getLastWakeTime() { + return mLastWakeTime; + } + + /** * Returns the most recent reason the device went to sleep up. This is one of * PowerManager.GO_TO_SLEEP_REASON_*. */ @@ -117,6 +132,7 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe } setWakefulness(WAKEFULNESS_WAKING); mLastWakeReason = pmWakeReason; + mLastWakeTime = mSystemClock.uptimeMillis(); updateLastWakeOriginLocation(); if (mWallpaperManagerService != null) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index a4fd087a24b1..d99af90ab6dc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -40,6 +40,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode +import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose @@ -88,6 +89,9 @@ interface KeyguardRepository { /** Observable for whether the bouncer is showing. */ val isBouncerShowing: Flow<Boolean> + /** Is the always-on display available to be used? */ + val isAodAvailable: Flow<Boolean> + /** * Observable for whether we are in doze state. * @@ -182,6 +186,7 @@ constructor( private val keyguardStateController: KeyguardStateController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dozeTransitionListener: DozeTransitionListener, + private val dozeParameters: DozeParameters, private val authController: AuthController, private val dreamOverlayCallbackController: DreamOverlayCallbackController, ) : KeyguardRepository { @@ -220,6 +225,31 @@ constructor( } .distinctUntilChanged() + override val isAodAvailable: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : DozeParameters.Callback { + override fun onAlwaysOnChange() { + trySendWithFailureLogging( + dozeParameters.getAlwaysOn(), + TAG, + "updated isAodAvailable" + ) + } + } + + dozeParameters.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + dozeParameters.getAlwaysOn(), + TAG, + "initial isAodAvailable" + ) + + awaitClose { dozeParameters.removeCallback(callback) } + } + .distinctUntilChanged() + override val isKeyguardOccluded: Flow<Boolean> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index fd2d271e40f9..ce61f2fec92f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -21,9 +21,9 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration @@ -48,12 +48,11 @@ constructor( private fun listenForDozingToLockscreen() { scope.launch { - keyguardInteractor.dozeTransitionModel + keyguardInteractor.wakefulnessModel .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (dozeTransitionModel, lastStartedTransition) = pair + .collect { (wakefulnessModel, lastStartedTransition) -> if ( - isDozeOff(dozeTransitionModel.to) && + isWakingOrStartingToWake(wakefulnessModel) && lastStartedTransition.to == KeyguardState.DOZING ) { keyguardTransitionRepository.startTransition( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index 553fafeb92c3..9203a9b924a7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -26,7 +26,10 @@ import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton @@ -40,7 +43,7 @@ constructor( ) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) { override fun start() { - listenForGoneToAod() + listenForGoneToAodOrDozing() listenForGoneToDreaming() } @@ -56,7 +59,7 @@ constructor( name, KeyguardState.GONE, KeyguardState.DREAMING, - getAnimator(), + getAnimator(TO_DREAMING_DURATION), ) ) } @@ -64,12 +67,18 @@ constructor( } } - private fun listenForGoneToAod() { + private fun listenForGoneToAodOrDozing() { scope.launch { keyguardInteractor.wakefulnessModel - .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) - .collect { pair -> - val (wakefulnessState, keyguardState) = pair + .sample( + combine( + keyguardTransitionInteractor.finishedKeyguardState, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, keyguardState, isAodAvailable) -> if ( keyguardState == KeyguardState.GONE && wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP @@ -78,7 +87,11 @@ constructor( TransitionInfo( name, KeyguardState.GONE, - KeyguardState.AOD, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, getAnimator(), ) ) @@ -87,14 +100,15 @@ constructor( } } - private fun getAnimator(): ValueAnimator { + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) - setDuration(TRANSITION_DURATION_MS) + setDuration(duration.inWholeMilliseconds) } } companion object { - private const val TRANSITION_DURATION_MS = 500L + private val DEFAULT_DURATION = 500.milliseconds + val TO_DREAMING_DURATION = 933.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 20c6531d580b..64028ceb2fbe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -21,11 +21,11 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample import java.util.UUID @@ -54,7 +54,7 @@ constructor( listenForLockscreenToGone() listenForLockscreenToOccluded() listenForLockscreenToCamera() - listenForLockscreenToAod() + listenForLockscreenToAodOrDozing() listenForLockscreenToBouncer() listenForLockscreenToDreaming() listenForLockscreenToBouncerDragging() @@ -230,19 +230,31 @@ constructor( } } - private fun listenForLockscreenToAod() { + private fun listenForLockscreenToAodOrDozing() { scope.launch { - keyguardInteractor - .dozeTransitionTo(DozeStateModel.DOZE_AOD) - .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (dozeToAod, lastStartedStep) = pair - if (lastStartedStep.to == KeyguardState.LOCKSCREEN) { + keyguardInteractor.wakefulnessModel + .sample( + combine( + keyguardTransitionInteractor.startedKeyguardTransitionStep, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, lastStartedStep, isAodAvailable) -> + if ( + lastStartedStep.to == KeyguardState.LOCKSCREEN && + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, KeyguardState.LOCKSCREEN, - KeyguardState.AOD, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, getAnimator(), ) ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index 88789019b10f..2dc8fee25379 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -23,12 +23,14 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton @@ -44,6 +46,7 @@ constructor( override fun start() { listenForOccludedToLockscreen() listenForOccludedToDreaming() + listenForOccludedToAodOrDozing() } private fun listenForOccludedToDreaming() { @@ -70,8 +73,7 @@ constructor( scope.launch { keyguardInteractor.isKeyguardOccluded .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (isOccluded, lastStartedKeyguardState) = pair + .collect { (isOccluded, lastStartedKeyguardState) -> // Occlusion signals come from the framework, and should interrupt any // existing transition if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) { @@ -88,6 +90,39 @@ constructor( } } + private fun listenForOccludedToAodOrDozing() { + scope.launch { + keyguardInteractor.wakefulnessModel + .sample( + combine( + keyguardTransitionInteractor.startedKeyguardTransitionStep, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, lastStartedStep, isAodAvailable) -> + if ( + lastStartedStep.to == KeyguardState.OCCLUDED && + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP + ) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.OCCLUDED, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, + getAnimator(), + ) + ) + } + } + } + } + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index ac2d230ee605..490d22eb0820 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -57,6 +57,8 @@ constructor( val dozeAmount: Flow<Float> = repository.linearDozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow<Boolean> = repository.isDozing + /** Whether Always-on Display mode is available. */ + val isAodAvailable: Flow<Boolean> = repository.isAodAvailable /** Doze transition information. */ val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt index a2661d76d90d..d4e2349907bc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -19,11 +19,14 @@ package com.android.systemui.keyguard.domain.interactor import com.android.keyguard.logging.KeyguardLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.plugins.log.LogLevel.VERBOSE import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +private val TAG = KeyguardTransitionAuditLogger::class.simpleName!! + /** Collect flows of interest for auditing keyguard transitions. */ @SysUISingleton class KeyguardTransitionAuditLogger @@ -37,35 +40,47 @@ constructor( fun start() { scope.launch { - keyguardInteractor.wakefulnessModel.collect { logger.v("WakefulnessModel", it) } + keyguardInteractor.wakefulnessModel.collect { + logger.log(TAG, VERBOSE, "WakefulnessModel", it) + } } scope.launch { - keyguardInteractor.isBouncerShowing.collect { logger.v("Bouncer showing", it) } + keyguardInteractor.isBouncerShowing.collect { + logger.log(TAG, VERBOSE, "Bouncer showing", it) + } } - scope.launch { keyguardInteractor.isDozing.collect { logger.v("isDozing", it) } } + scope.launch { + keyguardInteractor.isDozing.collect { logger.log(TAG, VERBOSE, "isDozing", it) } + } - scope.launch { keyguardInteractor.isDreaming.collect { logger.v("isDreaming", it) } } + scope.launch { + keyguardInteractor.isDreaming.collect { logger.log(TAG, VERBOSE, "isDreaming", it) } + } scope.launch { interactor.finishedKeyguardTransitionStep.collect { - logger.i("Finished transition", it) + logger.log(TAG, VERBOSE, "Finished transition", it) } } scope.launch { interactor.canceledKeyguardTransitionStep.collect { - logger.i("Canceled transition", it) + logger.log(TAG, VERBOSE, "Canceled transition", it) } } scope.launch { - interactor.startedKeyguardTransitionStep.collect { logger.i("Started transition", it) } + interactor.startedKeyguardTransitionStep.collect { + logger.log(TAG, VERBOSE, "Started transition", it) + } } scope.launch { - keyguardInteractor.dozeTransitionModel.collect { logger.i("Doze transition", it) } + keyguardInteractor.dozeTransitionModel.collect { + logger.log(TAG, VERBOSE, "Doze transition", it) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt index 0e4058bf8f6d..9d8bf7deb03e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -45,7 +45,6 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewMod import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.util.kotlin.pairwise import kotlin.math.pow import kotlin.math.sqrt import kotlin.time.Duration.Companion.milliseconds @@ -129,18 +128,6 @@ object KeyguardBottomAreaViewBinder { } launch { - viewModel.startButton - .map { it.isActivated } - .pairwise() - .collect { (prev, next) -> - when { - !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated) - prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated) - } - } - } - - launch { viewModel.endButton.collect { buttonModel -> updateButton( view = endButton, @@ -153,18 +140,6 @@ object KeyguardBottomAreaViewBinder { } launch { - viewModel.endButton - .map { it.isActivated } - .pairwise() - .collect { (prev, next) -> - when { - !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated) - prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated) - } - } - } - - launch { viewModel.isOverlayContainerVisible.collect { isVisible -> overlayContainer.visibility = if (isVisible) { @@ -383,6 +358,13 @@ object KeyguardBottomAreaViewBinder { .setDuration(longPressDurationMs) .withEndAction { view.setOnClickListener { + vibratorHelper?.vibrate( + if (viewModel.isActivated) { + Vibrations.Activated + } else { + Vibrations.Deactivated + } + ) viewModel.onClicked( KeyguardQuickAffordanceViewModel.OnClickedParameters( configKey = viewModel.configKey, diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt index 0645236226bd..9f563fe4eae5 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt @@ -23,3 +23,15 @@ import javax.inject.Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class KeyguardClockLog + +/** A [com.android.systemui.plugins.log.LogBuffer] for small keyguard clock logs. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyguardSmallClockLog + +/** A [com.android.systemui.plugins.log.LogBuffer] for large keyguard clock logs. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyguardLargeClockLog 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 711bca06d985..afbd8ed9bf5d 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -335,13 +335,33 @@ public class LogModule { } /** - * Provides a {@link LogBuffer} for keyguard clock logs. + * Provides a {@link LogBuffer} for general keyguard clock logs. */ @Provides @SysUISingleton @KeyguardClockLog public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) { - return factory.create("KeyguardClockLog", 500); + return factory.create("KeyguardClockLog", 100); + } + + /** + * Provides a {@link LogBuffer} for keyguard small clock logs. + */ + @Provides + @SysUISingleton + @KeyguardSmallClockLog + public static LogBuffer provideKeyguardSmallClockLog(LogBufferFactory factory) { + return factory.create("KeyguardSmallClockLog", 100); + } + + /** + * Provides a {@link LogBuffer} for keyguard large clock logs. + */ + @Provides + @SysUISingleton + @KeyguardLargeClockLog + public static LogBuffer provideKeyguardLargeClockLog(LogBufferFactory factory) { + return factory.create("KeyguardLargeClockLog", 100); } /** diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt index 7a90a7470cd2..7ccc43ce62c2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt @@ -29,6 +29,18 @@ constructor( private val dumpManager: DumpManager, private val systemClock: SystemClock, ) { + private val existingBuffers = mutableMapOf<String, TableLogBuffer>() + + /** + * Creates a new [TableLogBuffer]. This method should only be called from static contexts, where + * it is guaranteed only to be created one time. See [getOrCreate] for a cache-aware method of + * obtaining a buffer. + * + * @param name a unique table name + * @param maxSize the buffer max size. See [adjustMaxSize] + * + * @return a new [TableLogBuffer] registered with [DumpManager] + */ fun create( name: String, maxSize: Int, @@ -37,4 +49,23 @@ constructor( dumpManager.registerNormalDumpable(name, tableBuffer) return tableBuffer } + + /** + * Log buffers are retained indefinitely by [DumpManager], so that they can be represented in + * bugreports. Because of this, many of them are created statically in the Dagger graph. + * + * In the case where you have to create a logbuffer with a name only known at runtime, this + * method can be used to lazily create a table log buffer which is then cached for reuse. + * + * @return a [TableLogBuffer] suitable for reuse + */ + fun getOrCreate( + name: String, + maxSize: Int, + ): TableLogBuffer = + existingBuffers.getOrElse(name) { + val buffer = create(name, maxSize) + existingBuffers[name] = buffer + buffer + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt index f006442906e7..be18cbec7163 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt @@ -88,7 +88,10 @@ data class MediaData( val instanceId: InstanceId, /** The UID of the app, used for logging */ - val appUid: Int + val appUid: Int, + + /** Whether explicit indicator exists */ + val isExplicit: Boolean = false, ) { companion object { /** Media is playing on the local device */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt index a8f39fa9a456..1c8bfd1fc468 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt @@ -24,6 +24,7 @@ import android.widget.ImageView import android.widget.SeekBar import android.widget.TextView import androidx.constraintlayout.widget.Barrier +import com.android.internal.widget.CachingIconView import com.android.systemui.R import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.surfaceeffects.ripple.MultiRippleView @@ -44,6 +45,7 @@ class MediaViewHolder constructor(itemView: View) { val appIcon = itemView.requireViewById<ImageView>(R.id.icon) val titleText = itemView.requireViewById<TextView>(R.id.header_title) val artistText = itemView.requireViewById<TextView>(R.id.header_artist) + val explicitIndicator = itemView.requireViewById<CachingIconView>(R.id.media_explicit_indicator) // Output switcher val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless) @@ -123,6 +125,7 @@ class MediaViewHolder constructor(itemView: View) { R.id.app_name, R.id.header_title, R.id.header_artist, + R.id.media_explicit_indicator, R.id.media_seamless, R.id.media_progress_bar, R.id.actionPlayPause, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 1cc8a1353a34..9f28d4607ab5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -46,6 +46,7 @@ import android.os.Process import android.os.UserHandle import android.provider.Settings import android.service.notification.StatusBarNotification +import android.support.v4.media.MediaMetadataCompat import android.text.TextUtils import android.util.Log import androidx.media.utils.MediaConstants @@ -661,6 +662,10 @@ class MediaDataManager( val currentEntry = mediaEntries.get(packageName) val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() val appUid = currentEntry?.appUid ?: Process.INVALID_UID + val isExplicit = + desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT && + mediaFlags.isExplicitIndicatorEnabled() val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() @@ -690,7 +695,8 @@ class MediaDataManager( hasCheckedForResume = true, lastActive = lastActive, instanceId = instanceId, - appUid = appUid + appUid = appUid, + isExplicit = isExplicit, ) ) } @@ -751,6 +757,15 @@ class MediaDataManager( song = HybridGroupManager.resolveTitle(notif) } + // Explicit Indicator + var isExplicit = false + if (mediaFlags.isExplicitIndicatorEnabled()) { + val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) + isExplicit = + mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + } + // Artist name var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) if (artist == null) { @@ -852,7 +867,8 @@ class MediaDataManager( isClearable = sbn.isClearable(), lastActive = lastActive, instanceId = instanceId, - appUid = appUid + appUid = appUid, + isExplicit = isExplicit, ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt index d5558b27ef1a..e7f7647797cd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt @@ -94,7 +94,7 @@ constructor( private var currentCarouselWidth: Int = 0 /** The current height of the carousel */ - private var currentCarouselHeight: Int = 0 + @VisibleForTesting var currentCarouselHeight: Int = 0 /** Are we currently showing only active players */ private var currentlyShowingOnlyActive: Boolean = false @@ -128,14 +128,14 @@ constructor( /** The measured height of the carousel */ private var carouselMeasureHeight: Int = 0 private var desiredHostState: MediaHostState? = null - private val mediaCarousel: MediaScrollView + @VisibleForTesting var mediaCarousel: MediaScrollView val mediaCarouselScrollHandler: MediaCarouselScrollHandler val mediaFrame: ViewGroup @VisibleForTesting lateinit var settingsButton: View private set private val mediaContent: ViewGroup - @VisibleForTesting val pageIndicator: PageIndicator + @VisibleForTesting var pageIndicator: PageIndicator private val visualStabilityCallback: OnReorderingAllowedListener private var needsReordering: Boolean = false private var keysNeedRemoval = mutableSetOf<String>() @@ -160,25 +160,20 @@ constructor( } companion object { - const val ANIMATION_BASE_DURATION = 2200f - const val DURATION = 167f - const val DETAILS_DELAY = 1067f - const val CONTROLS_DELAY = 1400f - const val PAGINATION_DELAY = 1900f - const val MEDIATITLES_DELAY = 1000f - const val MEDIACONTAINERS_DELAY = 967f val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F) - val REVERSE_BEZIER = PathInterpolator(0F, 0.68F, 1F, 0F) - - fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float { - val transformStartFraction = delay / ANIMATION_BASE_DURATION - val transformDurationFraction = duration / ANIMATION_BASE_DURATION - val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction) - return MathUtils.constrain( - (squishinessToTime - transformStartFraction) / transformDurationFraction, - 0F, - 1F - ) + + fun calculateAlpha( + squishinessFraction: Float, + startPosition: Float, + endPosition: Float + ): Float { + val transformFraction = + MathUtils.constrain( + (squishinessFraction - startPosition) / (endPosition - startPosition), + 0F, + 1F + ) + return TRANSFORM_BEZIER.getInterpolation(transformFraction) } } @@ -813,7 +808,12 @@ constructor( val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F val endAlpha = (if (endIsVisible) 1.0f else 0.0f) * - calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION) + calculateAlpha( + squishFraction, + (pageIndicator.translationY + pageIndicator.height) / + mediaCarousel.measuredHeight, + 1F + ) var alpha = 1.0f if (!endIsVisible || !startIsVisible) { var progress = currentTransitionProgress @@ -839,7 +839,8 @@ constructor( pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams pageIndicator.translationY = - (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat() + (mediaCarousel.measuredHeight - pageIndicator.height - layoutParams.bottomMargin) + .toFloat() } /** Update the dimension of this carousel. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index ee0147f55536..9d1ebb664c10 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -51,7 +51,6 @@ import android.os.Process; import android.os.Trace; import android.text.TextUtils; import android.util.Log; -import android.util.Pair; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -69,6 +68,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; +import com.android.internal.widget.CachingIconView; import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.R; @@ -123,6 +123,7 @@ import java.util.concurrent.Executor; import javax.inject.Inject; +import kotlin.Triple; import kotlin.Unit; /** @@ -400,10 +401,11 @@ public class MediaControlPanel { TextView titleText = mMediaViewHolder.getTitleText(); TextView artistText = mMediaViewHolder.getArtistText(); + CachingIconView explicitIndicator = mMediaViewHolder.getExplicitIndicator(); AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter, - Interpolators.EMPHASIZED_DECELERATE, titleText, artistText); + Interpolators.EMPHASIZED_DECELERATE, titleText, artistText, explicitIndicator); AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit, - Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText); + Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText, explicitIndicator); MultiRippleView multiRippleView = vh.getMultiRippleView(); mMultiRippleController = new MultiRippleController(multiRippleView); @@ -668,11 +670,15 @@ public class MediaControlPanel { private boolean bindSongMetadata(MediaData data) { TextView titleText = mMediaViewHolder.getTitleText(); TextView artistText = mMediaViewHolder.getArtistText(); + ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); + ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); return mMetadataAnimationHandler.setNext( - Pair.create(data.getSong(), data.getArtist()), + new Triple(data.getSong(), data.getArtist(), data.isExplicit()), () -> { titleText.setText(data.getSong()); artistText.setText(data.getArtist()); + setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator, data.isExplicit()); + setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator, data.isExplicit()); // refreshState is required here to resize the text views (and prevent ellipsis) mMediaViewController.refreshState(); diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index f7a9bc760caf..66f12d6242b0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -41,6 +41,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeStateEvents @@ -93,6 +94,7 @@ constructor( private val keyguardStateController: KeyguardStateController, private val bypassController: KeyguardBypassController, private val mediaCarouselController: MediaCarouselController, + private val mediaManager: MediaDataManager, private val keyguardViewController: KeyguardViewController, private val dreamOverlayStateController: DreamOverlayStateController, configurationController: ConfigurationController, @@ -224,9 +226,9 @@ constructor( private var inSplitShade = false - /** Is there any active media in the carousel? */ - private var hasActiveMedia: Boolean = false - get() = mediaHosts.get(LOCATION_QQS)?.visible == true + /** Is there any active media or recommendation in the carousel? */ + private var hasActiveMediaOrRecommendation: Boolean = false + get() = mediaManager.hasActiveMediaOrRecommendation() /** Are we currently waiting on an animation to start? */ private var animationPending: Boolean = false @@ -582,12 +584,8 @@ constructor( val viewHost = createUniqueObjectHost() mediaObject.hostView = viewHost mediaObject.addVisibilityChangeListener { - // If QQS changes visibility, we need to force an update to ensure the transition - // goes into the correct state - val stateUpdate = mediaObject.location == LOCATION_QQS - // Never animate because of a visibility change, only state changes should do that - updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate) + updateDesiredLocation(forceNoAnimation = true) } mediaHosts[mediaObject.location] = mediaObject if (mediaObject.location == desiredLocation) { @@ -908,7 +906,7 @@ constructor( fun isCurrentlyInGuidedTransformation(): Boolean { return hasValidStartAndEndLocations() && getTransformationProgress() >= 0 && - areGuidedTransitionHostsVisible() + (areGuidedTransitionHostsVisible() || !hasActiveMediaOrRecommendation) } private fun hasValidStartAndEndLocations(): Boolean { @@ -965,7 +963,7 @@ constructor( private fun getQSTransformationProgress(): Float { val currentHost = getHost(desiredLocation) val previousHost = getHost(previousLocation) - if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) { + if (currentHost?.location == LOCATION_QS && !inSplitShade) { if (previousHost?.location == LOCATION_QQS) { if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) { return qsExpansion @@ -1028,7 +1026,8 @@ constructor( private fun updateHostAttachment() = traceSection("MediaHierarchyManager#updateHostAttachment") { var newLocation = resolveLocationForFading() - var canUseOverlay = !isCurrentlyFading() + // Don't use the overlay when fading or when we don't have active media + var canUseOverlay = !isCurrentlyFading() && hasActiveMediaOrRecommendation if (isCrossFadeAnimatorRunning) { if ( getHost(newLocation)?.visible == true && @@ -1122,7 +1121,6 @@ constructor( dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS qsExpansion > 0.4f && onLockscreen -> LOCATION_QS - !hasActiveMedia -> LOCATION_QS onLockscreen && isSplitShadeExpanding() -> LOCATION_QS onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index 322421318cb8..2ec7be6eaa32 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -24,11 +24,6 @@ import com.android.systemui.R import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.animation.MeasurementOutput @@ -36,6 +31,8 @@ import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionLayoutController import com.android.systemui.util.animation.TransitionViewState import com.android.systemui.util.traceSection +import java.lang.Float.max +import java.lang.Float.min import javax.inject.Inject /** @@ -80,6 +77,7 @@ constructor( setOf( R.id.header_title, R.id.header_artist, + R.id.media_explicit_indicator, R.id.actionPlayPause, ) @@ -304,39 +302,106 @@ constructor( val squishedViewState = viewState.copy() val squishedHeight = (squishedViewState.measureHeight * squishFraction).toInt() squishedViewState.height = squishedHeight - controlIds.forEach { id -> - squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION) - } - } - - detailIds.forEach { id -> - squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION) - } - } - // We are not overriding the squishedViewStates height but only the children to avoid // them remeasuring the whole view. Instead it just remains as the original size backgroundIds.forEach { id -> - squishedViewState.widgetStates.get(id)?.let { state -> - state.height = squishedHeight - } + squishedViewState.widgetStates.get(id)?.let { state -> state.height = squishedHeight } } - RecommendationViewHolder.mediaContainersIds.forEach { id -> + // media player + val controlsTop = + calculateWidgetGroupAlphaForSquishiness( + controlIds, + squishedViewState.measureHeight.toFloat(), + squishedViewState, + squishFraction + ) + calculateWidgetGroupAlphaForSquishiness( + detailIds, + controlsTop, + squishedViewState, + squishFraction + ) + // recommendation card + val titlesTop = + calculateWidgetGroupAlphaForSquishiness( + RecommendationViewHolder.mediaTitlesAndSubtitlesIds, + squishedViewState.measureHeight.toFloat(), + squishedViewState, + squishFraction + ) + calculateWidgetGroupAlphaForSquishiness( + RecommendationViewHolder.mediaContainersIds, + titlesTop, + squishedViewState, + squishFraction + ) + return squishedViewState + } + + /** + * This function is to make each widget in UMO disappear before being clipped by squished UMO + * + * The general rule is that widgets in UMO has been divided into several groups, and widgets in + * one group have the same alpha during squishing It will change from alpha 0.0 when the visible + * bottom of UMO reach the bottom of this group It will change to alpha 1.0 when the visible + * bottom of UMO reach the top of the group below e.g.Album title, artist title and play-pause + * button will change alpha together. + * ``` + * And their alpha becomes 1.0 when the visible bottom of UMO reach the top of controls, + * including progress bar, next button, previous button + * ``` + * widgetGroupIds: a group of widgets have same state during UMO is squished, + * ``` + * e.g. Album title, artist title and play-pause button + * ``` + * groupEndPosition: the height of UMO, when the height reaches this value, + * ``` + * widgets in this group should have 1.0 as alpha + * e.g., the group of album title, artist title and play-pause button will become fully + * visible when the height of UMO reaches the top of controls group + * (progress bar, previous button and next button) + * ``` + * squishedViewState: hold the widgetState of each widget, which will be modified + * squishFraction: the squishFraction of UMO + */ + private fun calculateWidgetGroupAlphaForSquishiness( + widgetGroupIds: Set<Int>, + groupEndPosition: Float, + squishedViewState: TransitionViewState, + squishFraction: Float + ): Float { + val nonsquishedHeight = squishedViewState.measureHeight + var groupTop = squishedViewState.measureHeight.toFloat() + var groupBottom = 0F + widgetGroupIds.forEach { id -> squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION) + groupTop = min(groupTop, state.y) + groupBottom = max(groupBottom, state.y + state.height) } } - - RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id -> + // startPosition means to the height of squished UMO where the widget alpha should start + // changing from 0.0 + // generally, it equals to the bottom of widgets, so that we can meet the requirement that + // widget should not go beyond the bounds of background + // endPosition means to the height of squished UMO where the widget alpha should finish + // changing alpha to 1.0 + var startPosition = groupBottom + val endPosition = groupEndPosition + if (startPosition == endPosition) { + startPosition = (endPosition - 0.2 * (groupBottom - groupTop)).toFloat() + } + widgetGroupIds.forEach { id -> squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION) + state.alpha = + calculateAlpha( + squishFraction, + startPosition / nonsquishedHeight, + endPosition / nonsquishedHeight + ) } } - - return squishedViewState + return groupTop // used for the widget group above this group } /** @@ -544,11 +609,13 @@ constructor( overrideSize?.let { // To be safe we're using a maximum here. The override size should always be set // properly though. - if (result.measureHeight != it.measuredHeight - || result.measureWidth != it.measuredWidth) { + if ( + result.measureHeight != it.measuredHeight || result.measureWidth != it.measuredWidth + ) { result.measureHeight = Math.max(it.measuredHeight, result.measureHeight) result.measureWidth = Math.max(it.measuredWidth, result.measureWidth) - // The measureHeight and the shown height should both be set to the overridden height + // The measureHeight and the shown height should both be set to the overridden + // height result.height = result.measureHeight result.width = result.measureWidth // Make sure all background views are also resized such that their size is correct diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 8d4931a5d08c..5bc35caed515 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -42,4 +42,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information. */ fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES) + + /** Check whether we show explicit indicator on UMO */ + fun isExplicitIndicatorEnabled() = featureFlags.isEnabled(Flags.MEDIA_EXPLICIT_INDICATOR) } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index 316b64209f83..7bc0c0cc614b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -637,44 +637,21 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } // For the first time building list, to make sure the top device is the connected // device. + boolean needToHandleMutingExpectedDevice = + hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote(); + final MediaDevice connectedMediaDevice = + needToHandleMutingExpectedDevice ? null + : getCurrentConnectedMediaDevice(); if (mMediaItemList.isEmpty()) { - boolean needToHandleMutingExpectedDevice = - hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote(); - final MediaDevice connectedMediaDevice = - needToHandleMutingExpectedDevice ? null - : getCurrentConnectedMediaDevice(); if (connectedMediaDevice == null) { if (DEBUG) { Log.d(TAG, "No connected media device or muting expected device exist."); } - if (needToHandleMutingExpectedDevice) { - for (MediaDevice device : devices) { - if (device.isMutingExpectedDevice()) { - mMediaItemList.add(0, new MediaItem(device)); - mMediaItemList.add(1, new MediaItem(mContext.getString( - R.string.media_output_group_title_speakers_and_displays), - MediaItem.MediaItemType.TYPE_GROUP_DIVIDER)); - } else { - mMediaItemList.add(new MediaItem(device)); - } - } - mMediaItemList.add(new MediaItem()); - } else { - mMediaItemList.addAll( - devices.stream().map(MediaItem::new).collect(Collectors.toList())); - categorizeMediaItems(null); - } + categorizeMediaItems(null, devices, needToHandleMutingExpectedDevice); return; } // selected device exist - for (MediaDevice device : devices) { - if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) { - mMediaItemList.add(0, new MediaItem(device)); - } else { - mMediaItemList.add(new MediaItem(device)); - } - } - categorizeMediaItems(connectedMediaDevice); + categorizeMediaItems(connectedMediaDevice, devices, false); return; } // To keep the same list order @@ -708,31 +685,46 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } } - private void categorizeMediaItems(MediaDevice connectedMediaDevice) { + private void categorizeMediaItems(MediaDevice connectedMediaDevice, List<MediaDevice> devices, + boolean needToHandleMutingExpectedDevice) { synchronized (mMediaDevicesLock) { Set<String> selectedDevicesIds = getSelectedMediaDevice().stream().map( MediaDevice::getId).collect(Collectors.toSet()); if (connectedMediaDevice != null) { selectedDevicesIds.add(connectedMediaDevice.getId()); } - int latestSelected = 1; - for (MediaItem item : mMediaItemList) { - if (item.getMediaDevice().isPresent()) { - MediaDevice device = item.getMediaDevice().get(); - if (selectedDevicesIds.contains(device.getId())) { - latestSelected = mMediaItemList.indexOf(item) + 1; - } else { - mMediaItemList.add(latestSelected, new MediaItem(mContext.getString( - R.string.media_output_group_title_speakers_and_displays), - MediaItem.MediaItemType.TYPE_GROUP_DIVIDER)); - break; + boolean suggestedDeviceAdded = false; + boolean displayGroupAdded = false; + for (MediaDevice device : devices) { + if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) { + mMediaItemList.add(0, new MediaItem(device)); + } else if (!needToHandleMutingExpectedDevice && selectedDevicesIds.contains( + device.getId())) { + mMediaItemList.add(0, new MediaItem(device)); + } else { + if (device.isSuggestedDevice() && !suggestedDeviceAdded) { + attachGroupDivider(mContext.getString( + R.string.media_output_group_title_suggested_device)); + suggestedDeviceAdded = true; + } else if (!device.isSuggestedDevice() && !displayGroupAdded) { + attachGroupDivider(mContext.getString( + R.string.media_output_group_title_speakers_and_displays)); + displayGroupAdded = true; } + mMediaItemList.add(new MediaItem(device)); } } mMediaItemList.add(new MediaItem()); } } + private void attachGroupDivider(String title) { + synchronized (mMediaDevicesLock) { + mMediaItemList.add( + new MediaItem(title, MediaItem.MediaItemType.TYPE_GROUP_DIVIDER)); + } + } + private void attachRangeInfo(List<MediaDevice> devices) { for (MediaDevice mediaDevice : devices) { if (mNearbyDeviceInfoMap.containsKey(mediaDevice.getId())) { diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index 9f44d984124f..935f38de2e4f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -150,7 +150,12 @@ constructor( logger: MediaTttLogger<ChipbarInfo>, ): ChipbarInfo { val packageName = routeInfo.clientPackageName - val otherDeviceName = routeInfo.name.toString() + val otherDeviceName = + if (routeInfo.name.isBlank()) { + context.getString(R.string.media_ttt_default_device_type) + } else { + routeInfo.name.toString() + } return ChipbarInfo( // Display the app's icon as the start icon diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 6dd60d043a06..8356440714e6 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -57,7 +57,9 @@ constructor( * If the keyguard is locked, notes will open as a full screen experience. A locked device has * no contextual information which let us use the whole screen space available. * - * If no in multi-window or the keyguard is unlocked, notes will open as a floating experience. + * If no in multi-window or the keyguard is unlocked, notes will open as a bubble OR it will be + * collapsed if the notes bubble is already opened. + * * That will let users open other apps in full screen, and take contextual notes. */ fun showNoteTask(isInMultiWindowMode: Boolean = false) { @@ -75,7 +77,7 @@ constructor( context.startActivity(intent) } else { // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter. - bubbles.showAppBubble(intent) + bubbles.showOrHideAppBubble(intent) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index da18b5734e81..5ef71269e84b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -67,6 +67,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.util.LifecycleFragment; +import com.android.systemui.util.Utils; import java.io.PrintWriter; import java.util.Arrays; @@ -682,7 +683,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } else { mQsMediaHost.setSquishFraction(mSquishinessFraction); } - + updateMediaPositions(); } private void setAlphaAnimationProgress(float progress) { @@ -757,6 +758,22 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca - mQSPanelController.getPaddingBottom()); } + private void updateMediaPositions() { + if (Utils.useQsMediaPlayer(getContext())) { + View hostView = mQsMediaHost.getHostView(); + // Make sure the media appears a bit from the top to make it look nicer + if (mLastQSExpansion > 0 && !isKeyguardState() && !mQqsMediaHost.getVisible() + && !mQSPanelController.shouldUseHorizontalLayout() && !mInSplitShade) { + float interpolation = 1.0f - mLastQSExpansion; + interpolation = Interpolators.ACCELERATE.getInterpolation(interpolation); + float translationY = -hostView.getHeight() * 1.3f * interpolation; + hostView.setTranslationY(translationY); + } else { + hostView.setTranslationY(0); + } + } + } + private boolean headerWillBeAnimating() { return mStatusBarState == KEYGUARD && mShowCollapsedOnKeyguard && !isKeyguardState(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index 7cf63f678c1d..1da30ade951b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -36,7 +36,6 @@ public interface QSHost { void removeCallback(Callback callback); void removeTile(String tileSpec); void removeTiles(Collection<String> specs); - void unmarkTileAsAutoAdded(String tileSpec); int indexOf(String tileSpec); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 7bb672ce5880..e85d0a32cfa8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -372,18 +372,18 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr if (mUsingHorizontalLayout) { // Only height remaining parameters.getDisappearSize().set(0.0f, 0.4f); - // Disappearing on the right side on the bottom - parameters.getGonePivot().set(1.0f, 1.0f); + // Disappearing on the right side on the top + parameters.getGonePivot().set(1.0f, 0.0f); // translating a bit horizontal parameters.getContentTranslationFraction().set(0.25f, 1.0f); parameters.setDisappearEnd(0.6f); } else { // Only width remaining parameters.getDisappearSize().set(1.0f, 0.0f); - // Disappearing on the bottom - parameters.getGonePivot().set(0.0f, 1.0f); + // Disappearing on the top + parameters.getGonePivot().set(0.0f, 0.0f); // translating a bit vertical - parameters.getContentTranslationFraction().set(0.0f, 1.05f); + parameters.getContentTranslationFraction().set(0.0f, 1f); parameters.setDisappearEnd(0.95f); } parameters.setFadeStartPosition(0.95f); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index cad296b671b3..100853caa2d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -427,11 +427,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P mMainExecutor.execute(() -> changeTileSpecs(tileSpecs -> tileSpecs.removeAll(specs))); } - @Override - public void unmarkTileAsAutoAdded(String spec) { - if (mAutoTiles != null) mAutoTiles.unmarkTileAsAutoAdded(spec); - } - /** * Add a tile to the end * diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index 79fcc7d81372..17124901e4de 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -24,6 +24,7 @@ import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.widget.LinearLayout; import android.widget.Toolbar; @@ -74,8 +75,8 @@ public class QSCustomizer extends LinearLayout { toolbar.setNavigationIcon( getResources().getDrawable(value.resourceId, mContext.getTheme())); - toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, - mContext.getString(com.android.internal.R.string.reset)); + toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); toolbar.setTitle(R.string.qs_edit); mRecyclerView = findViewById(android.R.id.list); mTransparentView = findViewById(R.id.customizer_transparent_view); diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt index 9f376ae75efe..d32ef327e90e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt @@ -49,109 +49,135 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : } fun logTileAdded(tileSpec: String) { - log(DEBUG, { - str1 = tileSpec - }, { - "[$str1] Tile added" - }) + buffer.log(TAG, DEBUG, { str1 = tileSpec }, { "[$str1] Tile added" }) } fun logTileDestroyed(tileSpec: String, reason: String) { - log(DEBUG, { - str1 = tileSpec - str2 = reason - }, { - "[$str1] Tile destroyed. Reason: $str2" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + str2 = reason + }, + { "[$str1] Tile destroyed. Reason: $str2" } + ) } fun logTileChangeListening(tileSpec: String, listening: Boolean) { - log(VERBOSE, { - bool1 = listening - str1 = tileSpec - }, { - "[$str1] Tile listening=$bool1" - }) + buffer.log( + TAG, + VERBOSE, + { + bool1 = listening + str1 = tileSpec + }, + { "[$str1] Tile listening=$bool1" } + ) } fun logAllTilesChangeListening(listening: Boolean, containerName: String, allSpecs: String) { - log(DEBUG, { - bool1 = listening - str1 = containerName - str2 = allSpecs - }, { - "Tiles listening=$bool1 in $str1. $str2" - }) + buffer.log( + TAG, + DEBUG, + { + bool1 = listening + str1 = containerName + str2 = allSpecs + }, + { "Tiles listening=$bool1 in $str1. $str2" } + ) } fun logTileClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - str2 = StatusBarState.toString(statusBarState) - str3 = toStateString(state) - }, { - "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + str2 = StatusBarState.toString(statusBarState) + str3 = toStateString(state) + }, + { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" } + ) } fun logHandleClick(tileSpec: String, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - }, { - "[$str1][$int1] Tile handling click." - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + }, + { "[$str1][$int1] Tile handling click." } + ) } fun logTileSecondaryClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - str2 = StatusBarState.toString(statusBarState) - str3 = toStateString(state) - }, { - "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + str2 = StatusBarState.toString(statusBarState) + str3 = toStateString(state) + }, + { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" } + ) } fun logHandleSecondaryClick(tileSpec: String, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - }, { - "[$str1][$int1] Tile handling secondary click." - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + }, + { "[$str1][$int1] Tile handling secondary click." } + ) } fun logTileLongClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - str2 = StatusBarState.toString(statusBarState) - str3 = toStateString(state) - }, { - "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + str2 = StatusBarState.toString(statusBarState) + str3 = toStateString(state) + }, + { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" } + ) } fun logHandleLongClick(tileSpec: String, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - }, { - "[$str1][$int1] Tile handling long click." - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + }, + { "[$str1][$int1] Tile handling long click." } + ) } fun logInternetTileUpdate(tileSpec: String, lastType: Int, callback: String) { - log(VERBOSE, { - str1 = tileSpec - int1 = lastType - str2 = callback - }, { - "[$str1] mLastTileState=$int1, Callback=$str2." - }) + buffer.log( + TAG, + VERBOSE, + { + str1 = tileSpec + int1 = lastType + str2 = callback + }, + { "[$str1] mLastTileState=$int1, Callback=$str2." } + ) } // TODO(b/250618218): Remove this method once we know the root cause of b/250618218. @@ -167,58 +193,75 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : if (tileSpec != "internet") { return } - log(VERBOSE, { - str1 = tileSpec - int1 = state - bool1 = disabledByPolicy - int2 = color - }, { - "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." - }) + buffer.log( + TAG, + VERBOSE, + { + str1 = tileSpec + int1 = state + bool1 = disabledByPolicy + int2 = color + }, + { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." } + ) } fun logTileUpdated(tileSpec: String, state: QSTile.State) { - log(VERBOSE, { - str1 = tileSpec - str2 = state.label?.toString() - str3 = state.icon?.toString() - int1 = state.state - if (state is QSTile.SignalState) { - bool1 = true - bool2 = state.activityIn - bool3 = state.activityOut + buffer.log( + TAG, + VERBOSE, + { + str1 = tileSpec + str2 = state.label?.toString() + str3 = state.icon?.toString() + int1 = state.state + if (state is QSTile.SignalState) { + bool1 = true + bool2 = state.activityIn + bool3 = state.activityOut + } + }, + { + "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." + + if (bool1) " Activity in/out=$bool2/$bool3" else "" } - }, { - "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." + - if (bool1) " Activity in/out=$bool2/$bool3" else "" - }) + ) } fun logPanelExpanded(expanded: Boolean, containerName: String) { - log(DEBUG, { - str1 = containerName - bool1 = expanded - }, { - "$str1 expanded=$bool1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + bool1 = expanded + }, + { "$str1 expanded=$bool1" } + ) } fun logOnViewAttached(orientation: Int, containerName: String) { - log(DEBUG, { - str1 = containerName - int1 = orientation - }, { - "onViewAttached: $str1 orientation $int1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + int1 = orientation + }, + { "onViewAttached: $str1 orientation $int1" } + ) } fun logOnViewDetached(orientation: Int, containerName: String) { - log(DEBUG, { - str1 = containerName - int1 = orientation - }, { - "onViewDetached: $str1 orientation $int1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + int1 = orientation + }, + { "onViewDetached: $str1 orientation $int1" } + ) } fun logOnConfigurationChanged( @@ -226,13 +269,16 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : newOrientation: Int, containerName: String ) { - log(DEBUG, { - str1 = containerName - int1 = lastOrientation - int2 = newOrientation - }, { - "configuration change: $str1 orientation was $int1, now $int2" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + int1 = lastOrientation + int2 = newOrientation + }, + { "configuration change: $str1 orientation was $int1, now $int2" } + ) } fun logSwitchTileLayout( @@ -241,32 +287,41 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : force: Boolean, containerName: String ) { - log(DEBUG, { - str1 = containerName - bool1 = after - bool2 = before - bool3 = force - }, { - "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + bool1 = after + bool2 = before + bool3 = force + }, + { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" } + ) } fun logTileDistributionInProgress(tilesPerPageCount: Int, totalTilesCount: Int) { - log(DEBUG, { - int1 = tilesPerPageCount - int2 = totalTilesCount - }, { - "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" - }) + buffer.log( + TAG, + DEBUG, + { + int1 = tilesPerPageCount + int2 = totalTilesCount + }, + { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" } + ) } fun logTileDistributed(tileName: String, pageIndex: Int) { - log(DEBUG, { - str1 = tileName - int1 = pageIndex - }, { - "Adding $str1 to page number $int1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileName + int1 = pageIndex + }, + { "Adding $str1 to page number $int1" } + ) } private fun toStateString(state: Int): String { @@ -277,12 +332,4 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : else -> "wrong state" } } - - private inline fun log( - logLevel: LogLevel, - initializer: LogMessage.() -> Unit, - noinline printer: LogMessage.() -> String - ) { - buffer.log(TAG, logLevel, initializer, printer) - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java index a92c7e3c8554..24a4f60b7c00 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -33,7 +33,6 @@ import com.android.systemui.qs.tiles.BatterySaverTile; import com.android.systemui.qs.tiles.BluetoothTile; import com.android.systemui.qs.tiles.CameraToggleTile; import com.android.systemui.qs.tiles.CastTile; -import com.android.systemui.qs.tiles.CellularTile; import com.android.systemui.qs.tiles.ColorCorrectionTile; import com.android.systemui.qs.tiles.ColorInversionTile; import com.android.systemui.qs.tiles.DataSaverTile; @@ -54,7 +53,6 @@ import com.android.systemui.qs.tiles.ReduceBrightColorsTile; import com.android.systemui.qs.tiles.RotationLockTile; import com.android.systemui.qs.tiles.ScreenRecordTile; import com.android.systemui.qs.tiles.UiModeNightTile; -import com.android.systemui.qs.tiles.WifiTile; import com.android.systemui.qs.tiles.WorkModeTile; import com.android.systemui.util.leak.GarbageMonitor; @@ -68,10 +66,8 @@ public class QSFactoryImpl implements QSFactory { private static final String TAG = "QSFactory"; - private final Provider<WifiTile> mWifiTileProvider; private final Provider<InternetTile> mInternetTileProvider; private final Provider<BluetoothTile> mBluetoothTileProvider; - private final Provider<CellularTile> mCellularTileProvider; private final Provider<DndTile> mDndTileProvider; private final Provider<ColorCorrectionTile> mColorCorrectionTileProvider; private final Provider<ColorInversionTile> mColorInversionTileProvider; @@ -106,10 +102,8 @@ public class QSFactoryImpl implements QSFactory { public QSFactoryImpl( Lazy<QSHost> qsHostLazy, Provider<CustomTile.Builder> customTileBuilderProvider, - Provider<WifiTile> wifiTileProvider, Provider<InternetTile> internetTileProvider, Provider<BluetoothTile> bluetoothTileProvider, - Provider<CellularTile> cellularTileProvider, Provider<DndTile> dndTileProvider, Provider<ColorInversionTile> colorInversionTileProvider, Provider<AirplaneModeTile> airplaneModeTileProvider, @@ -139,10 +133,8 @@ public class QSFactoryImpl implements QSFactory { mQsHostLazy = qsHostLazy; mCustomTileBuilderProvider = customTileBuilderProvider; - mWifiTileProvider = wifiTileProvider; mInternetTileProvider = internetTileProvider; mBluetoothTileProvider = bluetoothTileProvider; - mCellularTileProvider = cellularTileProvider; mDndTileProvider = dndTileProvider; mColorInversionTileProvider = colorInversionTileProvider; mAirplaneModeTileProvider = airplaneModeTileProvider; @@ -186,14 +178,10 @@ public class QSFactoryImpl implements QSFactory { protected QSTileImpl createTileInternal(String tileSpec) { // Stock tiles. switch (tileSpec) { - case "wifi": - return mWifiTileProvider.get(); case "internet": return mInternetTileProvider.get(); case "bt": return mBluetoothTileProvider.get(); - case "cell": - return mCellularTileProvider.get(); case "dnd": return mDndTileProvider.get(); case "inversion": diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java deleted file mode 100644 index 04a25fc193b3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (C) 2014 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.qs.tiles; - -import static com.android.systemui.Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA; - -import android.annotation.NonNull; -import android.app.AlertDialog; -import android.app.AlertDialog.Builder; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.os.Handler; -import android.os.Looper; -import android.os.UserHandle; -import android.provider.Settings; -import android.service.quicksettings.Tile; -import android.telephony.SubscriptionManager; -import android.text.Html; -import android.text.TextUtils; -import android.view.View; -import android.view.WindowManager.LayoutParams; -import android.widget.Switch; - -import androidx.annotation.Nullable; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settingslib.net.DataUsageController; -import com.android.systemui.Prefs; -import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.qs.QSIconView; -import com.android.systemui.plugins.qs.QSTile.SignalState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.qs.QSHost; -import com.android.systemui.qs.SignalTileView; -import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.qs.tileimpl.QSTileImpl; -import com.android.systemui.statusbar.connectivity.IconState; -import com.android.systemui.statusbar.connectivity.MobileDataIndicators; -import com.android.systemui.statusbar.connectivity.NetworkController; -import com.android.systemui.statusbar.connectivity.SignalCallback; -import com.android.systemui.statusbar.phone.SystemUIDialog; -import com.android.systemui.statusbar.policy.KeyguardStateController; - -import javax.inject.Inject; - -/** Quick settings tile: Cellular **/ -public class CellularTile extends QSTileImpl<SignalState> { - private static final String ENABLE_SETTINGS_DATA_PLAN = "enable.settings.data.plan"; - - private final NetworkController mController; - private final DataUsageController mDataController; - private final KeyguardStateController mKeyguard; - private final CellSignalCallback mSignalCallback = new CellSignalCallback(); - - @Inject - public CellularTile( - QSHost host, - @Background Looper backgroundLooper, - @Main Handler mainHandler, - FalsingManager falsingManager, - MetricsLogger metricsLogger, - StatusBarStateController statusBarStateController, - ActivityStarter activityStarter, - QSLogger qsLogger, - NetworkController networkController, - KeyguardStateController keyguardStateController - - ) { - super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, - statusBarStateController, activityStarter, qsLogger); - mController = networkController; - mKeyguard = keyguardStateController; - mDataController = mController.getMobileDataController(); - mController.observe(getLifecycle(), mSignalCallback); - } - - @Override - public SignalState newTileState() { - return new SignalState(); - } - - @Override - public QSIconView createTileView(Context context) { - return new SignalTileView(context); - } - - @Override - public Intent getLongClickIntent() { - if (getState().state == Tile.STATE_UNAVAILABLE) { - return new Intent(Settings.ACTION_WIRELESS_SETTINGS); - } - return getCellularSettingIntent(); - } - - @Override - protected void handleClick(@Nullable View view) { - if (getState().state == Tile.STATE_UNAVAILABLE) { - return; - } - if (mDataController.isMobileDataEnabled()) { - maybeShowDisableDialog(); - } else { - mDataController.setMobileDataEnabled(true); - } - } - - private void maybeShowDisableDialog() { - if (Prefs.getBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, false)) { - // Directly turn off mobile data if the user has seen the dialog before. - mDataController.setMobileDataEnabled(false); - return; - } - String carrierName = mController.getMobileDataNetworkName(); - boolean isInService = mController.isMobileDataNetworkInService(); - if (TextUtils.isEmpty(carrierName) || !isInService) { - carrierName = mContext.getString(R.string.mobile_data_disable_message_default_carrier); - } - AlertDialog dialog = new Builder(mContext) - .setTitle(R.string.mobile_data_disable_title) - .setMessage(mContext.getString(R.string.mobile_data_disable_message, carrierName)) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton( - com.android.internal.R.string.alert_windows_notification_turn_off_action, - (d, w) -> { - mDataController.setMobileDataEnabled(false); - Prefs.putBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, true); - }) - .create(); - dialog.getWindow().setType(LayoutParams.TYPE_KEYGUARD_DIALOG); - SystemUIDialog.setShowForAllUsers(dialog, true); - SystemUIDialog.registerDismissListener(dialog); - SystemUIDialog.setWindowOnTop(dialog, mKeyguard.isShowing()); - dialog.show(); - } - - @Override - protected void handleSecondaryClick(@Nullable View view) { - handleLongClick(view); - } - - @Override - public CharSequence getTileLabel() { - return mContext.getString(R.string.quick_settings_cellular_detail_title); - } - - @Override - protected void handleUpdateState(SignalState state, Object arg) { - CallbackInfo cb = (CallbackInfo) arg; - if (cb == null) { - cb = mSignalCallback.mInfo; - } - - final Resources r = mContext.getResources(); - state.label = r.getString(R.string.mobile_data); - boolean mobileDataEnabled = mDataController.isMobileDataSupported() - && mDataController.isMobileDataEnabled(); - state.value = mobileDataEnabled; - state.activityIn = mobileDataEnabled && cb.activityIn; - state.activityOut = mobileDataEnabled && cb.activityOut; - state.expandedAccessibilityClassName = Switch.class.getName(); - if (cb.noSim) { - state.icon = ResourceIcon.get(R.drawable.ic_qs_no_sim); - } else { - state.icon = ResourceIcon.get(R.drawable.ic_swap_vert); - } - - if (cb.noSim) { - state.state = Tile.STATE_UNAVAILABLE; - state.secondaryLabel = r.getString(R.string.keyguard_missing_sim_message_short); - } else if (cb.airplaneModeEnabled) { - state.state = Tile.STATE_UNAVAILABLE; - state.secondaryLabel = r.getString(R.string.status_bar_airplane); - } else if (mobileDataEnabled) { - state.state = Tile.STATE_ACTIVE; - state.secondaryLabel = appendMobileDataType( - // Only show carrier name if there are more than 1 subscription - cb.multipleSubs ? cb.dataSubscriptionName : "", - getMobileDataContentName(cb)); - } else { - state.state = Tile.STATE_INACTIVE; - state.secondaryLabel = r.getString(R.string.cell_data_off); - } - - state.contentDescription = state.label; - if (state.state == Tile.STATE_INACTIVE) { - // This information is appended later by converting the Tile.STATE_INACTIVE state. - state.stateDescription = ""; - } else { - state.stateDescription = state.secondaryLabel; - } - } - - private CharSequence appendMobileDataType(CharSequence current, CharSequence dataType) { - if (TextUtils.isEmpty(dataType)) { - return Html.fromHtml(current.toString(), 0); - } - if (TextUtils.isEmpty(current)) { - return Html.fromHtml(dataType.toString(), 0); - } - String concat = mContext.getString(R.string.mobile_carrier_text_format, current, dataType); - return Html.fromHtml(concat, 0); - } - - private CharSequence getMobileDataContentName(CallbackInfo cb) { - if (cb.roaming && !TextUtils.isEmpty(cb.dataContentDescription)) { - String roaming = mContext.getString(R.string.data_connection_roaming); - String dataDescription = cb.dataContentDescription.toString(); - return mContext.getString(R.string.mobile_data_text_format, roaming, dataDescription); - } - if (cb.roaming) { - return mContext.getString(R.string.data_connection_roaming); - } - return cb.dataContentDescription; - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.QS_CELLULAR; - } - - @Override - public boolean isAvailable() { - return mController.hasMobileDataFeature() - && mHost.getUserContext().getUserId() == UserHandle.USER_SYSTEM; - } - - private static final class CallbackInfo { - boolean airplaneModeEnabled; - @Nullable - CharSequence dataSubscriptionName; - @Nullable - CharSequence dataContentDescription; - boolean activityIn; - boolean activityOut; - boolean noSim; - boolean roaming; - boolean multipleSubs; - } - - private final class CellSignalCallback implements SignalCallback { - private final CallbackInfo mInfo = new CallbackInfo(); - - @Override - public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) { - if (indicators.qsIcon == null) { - // Not data sim, don't display. - return; - } - mInfo.dataSubscriptionName = mController.getMobileDataNetworkName(); - mInfo.dataContentDescription = indicators.qsDescription != null - ? indicators.typeContentDescriptionHtml : null; - mInfo.activityIn = indicators.activityIn; - mInfo.activityOut = indicators.activityOut; - mInfo.roaming = indicators.roaming; - mInfo.multipleSubs = mController.getNumberSubscriptions() > 1; - refreshState(mInfo); - } - - @Override - public void setNoSims(boolean show, boolean simDetected) { - mInfo.noSim = show; - refreshState(mInfo); - } - - @Override - public void setIsAirplaneMode(@NonNull IconState icon) { - mInfo.airplaneModeEnabled = icon.visible; - refreshState(mInfo); - } - } - - static Intent getCellularSettingIntent() { - Intent intent = new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS); - int dataSub = SubscriptionManager.getDefaultDataSubscriptionId(); - if (dataSub != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - intent.putExtra(Settings.EXTRA_SUB_ID, - SubscriptionManager.getDefaultDataSubscriptionId()); - } - return intent; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java deleted file mode 100644 index b2be56cca51e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (C) 2014 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.qs.tiles; - -import android.annotation.NonNull; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; -import android.service.quicksettings.Tile; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.widget.Switch; - -import androidx.annotation.Nullable; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.qs.QSIconView; -import com.android.systemui.plugins.qs.QSTile; -import com.android.systemui.plugins.qs.QSTile.SignalState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.qs.AlphaControlledSignalTileView; -import com.android.systemui.qs.QSHost; -import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.qs.tileimpl.QSIconViewImpl; -import com.android.systemui.qs.tileimpl.QSTileImpl; -import com.android.systemui.statusbar.connectivity.AccessPointController; -import com.android.systemui.statusbar.connectivity.NetworkController; -import com.android.systemui.statusbar.connectivity.SignalCallback; -import com.android.systemui.statusbar.connectivity.WifiIcons; -import com.android.systemui.statusbar.connectivity.WifiIndicators; - -import javax.inject.Inject; - -/** Quick settings tile: Wifi **/ -public class WifiTile extends QSTileImpl<SignalState> { - private static final Intent WIFI_SETTINGS = new Intent(Settings.ACTION_WIFI_SETTINGS); - - protected final NetworkController mController; - private final AccessPointController mWifiController; - private final QSTile.SignalState mStateBeforeClick = newTileState(); - - protected final WifiSignalCallback mSignalCallback = new WifiSignalCallback(); - private boolean mExpectDisabled; - - @Inject - public WifiTile( - QSHost host, - @Background Looper backgroundLooper, - @Main Handler mainHandler, - FalsingManager falsingManager, - MetricsLogger metricsLogger, - StatusBarStateController statusBarStateController, - ActivityStarter activityStarter, - QSLogger qsLogger, - NetworkController networkController, - AccessPointController accessPointController - ) { - super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, - statusBarStateController, activityStarter, qsLogger); - mController = networkController; - mWifiController = accessPointController; - mController.observe(getLifecycle(), mSignalCallback); - mStateBeforeClick.spec = "wifi"; - } - - @Override - public SignalState newTileState() { - return new SignalState(); - } - - @Override - public QSIconView createTileView(Context context) { - return new AlphaControlledSignalTileView(context); - } - - @Override - public Intent getLongClickIntent() { - return WIFI_SETTINGS; - } - - @Override - protected void handleClick(@Nullable View view) { - // Secondary clicks are header clicks, just toggle. - mState.copyTo(mStateBeforeClick); - boolean wifiEnabled = mState.value; - // Immediately enter transient state when turning on wifi. - refreshState(wifiEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING); - mController.setWifiEnabled(!wifiEnabled); - mExpectDisabled = wifiEnabled; - if (mExpectDisabled) { - mHandler.postDelayed(() -> { - if (mExpectDisabled) { - mExpectDisabled = false; - refreshState(); - } - }, QSIconViewImpl.QS_ANIM_LENGTH); - } - } - - @Override - protected void handleSecondaryClick(@Nullable View view) { - if (!mWifiController.canConfigWifi()) { - mActivityStarter.postStartActivityDismissingKeyguard( - new Intent(Settings.ACTION_WIFI_SETTINGS), 0); - return; - } - if (!mState.value) { - mController.setWifiEnabled(true); - } - } - - @Override - public CharSequence getTileLabel() { - return mContext.getString(R.string.quick_settings_wifi_label); - } - - @Override - protected void handleUpdateState(SignalState state, Object arg) { - if (DEBUG) Log.d(TAG, "handleUpdateState arg=" + arg); - final CallbackInfo cb = mSignalCallback.mInfo; - if (mExpectDisabled) { - if (cb.enabled) { - return; // Ignore updates until disabled event occurs. - } else { - mExpectDisabled = false; - } - } - boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING; - boolean wifiConnected = cb.enabled && (cb.wifiSignalIconId > 0) - && (cb.ssid != null || cb.wifiSignalIconId != WifiIcons.QS_WIFI_NO_NETWORK); - boolean wifiNotConnected = (cb.ssid == null) - && (cb.wifiSignalIconId == WifiIcons.QS_WIFI_NO_NETWORK); - if (state.slash == null) { - state.slash = new SlashState(); - state.slash.rotation = 6; - } - state.slash.isSlashed = false; - boolean isTransient = transientEnabling || cb.isTransient; - state.secondaryLabel = getSecondaryLabel(isTransient, cb.statusLabel); - state.state = Tile.STATE_ACTIVE; - state.dualTarget = true; - state.value = transientEnabling || cb.enabled; - state.activityIn = cb.enabled && cb.activityIn; - state.activityOut = cb.enabled && cb.activityOut; - final StringBuffer minimalContentDescription = new StringBuffer(); - final StringBuffer minimalStateDescription = new StringBuffer(); - final Resources r = mContext.getResources(); - if (isTransient) { - state.icon = ResourceIcon.get( - com.android.internal.R.drawable.ic_signal_wifi_transient_animation); - state.label = r.getString(R.string.quick_settings_wifi_label); - } else if (!state.value) { - state.slash.isSlashed = true; - state.state = Tile.STATE_INACTIVE; - state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_DISABLED); - state.label = r.getString(R.string.quick_settings_wifi_label); - } else if (wifiConnected) { - state.icon = ResourceIcon.get(cb.wifiSignalIconId); - state.label = cb.ssid != null ? removeDoubleQuotes(cb.ssid) : getTileLabel(); - } else if (wifiNotConnected) { - state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_NO_NETWORK); - state.label = r.getString(R.string.quick_settings_wifi_label); - } else { - state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_NO_NETWORK); - state.label = r.getString(R.string.quick_settings_wifi_label); - } - minimalContentDescription.append( - mContext.getString(R.string.quick_settings_wifi_label)).append(","); - if (state.value) { - if (wifiConnected) { - minimalStateDescription.append(cb.wifiSignalContentDescription); - minimalContentDescription.append(removeDoubleQuotes(cb.ssid)); - if (!TextUtils.isEmpty(state.secondaryLabel)) { - minimalContentDescription.append(",").append(state.secondaryLabel); - } - } - } - state.stateDescription = minimalStateDescription.toString(); - state.contentDescription = minimalContentDescription.toString(); - state.dualLabelContentDescription = r.getString( - R.string.accessibility_quick_settings_open_settings, getTileLabel()); - state.expandedAccessibilityClassName = Switch.class.getName(); - } - - private CharSequence getSecondaryLabel(boolean isTransient, String statusLabel) { - return isTransient - ? mContext.getString(R.string.quick_settings_wifi_secondary_label_transient) - : statusLabel; - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.QS_WIFI; - } - - @Override - public boolean isAvailable() { - return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI); - } - - @Nullable - private static String removeDoubleQuotes(String string) { - if (string == null) return null; - final int length = string.length(); - if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) { - return string.substring(1, length - 1); - } - return string; - } - - protected static final class CallbackInfo { - boolean enabled; - boolean connected; - int wifiSignalIconId; - @Nullable - String ssid; - boolean activityIn; - boolean activityOut; - @Nullable - String wifiSignalContentDescription; - boolean isTransient; - @Nullable - public String statusLabel; - - @Override - public String toString() { - return new StringBuilder("CallbackInfo[") - .append("enabled=").append(enabled) - .append(",connected=").append(connected) - .append(",wifiSignalIconId=").append(wifiSignalIconId) - .append(",ssid=").append(ssid) - .append(",activityIn=").append(activityIn) - .append(",activityOut=").append(activityOut) - .append(",wifiSignalContentDescription=").append(wifiSignalContentDescription) - .append(",isTransient=").append(isTransient) - .append(']').toString(); - } - } - - protected final class WifiSignalCallback implements SignalCallback { - final CallbackInfo mInfo = new CallbackInfo(); - - @Override - public void setWifiIndicators(@NonNull WifiIndicators indicators) { - if (DEBUG) Log.d(TAG, "onWifiSignalChanged enabled=" + indicators.enabled); - if (indicators.qsIcon == null) { - return; - } - mInfo.enabled = indicators.enabled; - mInfo.connected = indicators.qsIcon.visible; - mInfo.wifiSignalIconId = indicators.qsIcon.icon; - mInfo.ssid = indicators.description; - mInfo.activityIn = indicators.activityIn; - mInfo.activityOut = indicators.activityOut; - mInfo.wifiSignalContentDescription = indicators.qsIcon.contentDescription; - mInfo.isTransient = indicators.isTransient; - mInfo.statusLabel = indicators.statusLabel; - refreshState(); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java index a6c7781d891c..72c6bfe371ce 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java @@ -101,7 +101,6 @@ public class WorkModeTile extends QSTileImpl<BooleanState> implements @MainThread public void onManagedProfileRemoved() { mHost.removeTile(getTileSpec()); - mHost.unmarkTileAsAutoAdded(getTileSpec()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 91ebf79344b6..b21a4857c736 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -687,8 +687,8 @@ public class ScreenshotController { } }); if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { - mScreenshotView.badgeScreenshot( - mContext.getPackageManager().getUserBadgeForDensity(owner, 0)); + mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon( + mContext.getDrawable(R.drawable.overlay_badge_background), owner)); } mScreenshotView.setScreenshot(mScreenBitmap, screenInsets); if (DEBUG_WINDOW) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index be40813e4622..7c013a8a9f59 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -1097,7 +1097,7 @@ public class ScreenshotView extends FrameLayout implements mScreenshotBadge.setVisibility(View.GONE); mScreenshotBadge.setImageDrawable(null); mPendingSharedTransition = false; - mActionsContainerBackground.setVisibility(View.GONE); + mActionsContainerBackground.setVisibility(View.INVISIBLE); mActionsContainer.setVisibility(View.GONE); mDismissButton.setVisibility(View.GONE); mScrollingScrim.setVisibility(View.GONE); diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 200288bb8faf..4dbe0998fc03 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -111,7 +111,7 @@ open class UserTrackerImpl internal constructor( // These get called when a managed profile goes in or out of quiet mode. addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) - + addAction(Intent.ACTION_MANAGED_PROFILE_ADDED) addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED) addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED) } @@ -128,6 +128,7 @@ open class UserTrackerImpl internal constructor( Intent.ACTION_USER_INFO_CHANGED, Intent.ACTION_MANAGED_PROFILE_AVAILABLE, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE, + Intent.ACTION_MANAGED_PROFILE_ADDED, Intent.ACTION_MANAGED_PROFILE_REMOVED, Intent.ACTION_MANAGED_PROFILE_UNLOCKED -> { handleProfilesChanged() diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt index 7fc0a5f6d4bf..e406be1ea0a3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt @@ -175,9 +175,10 @@ class LargeScreenShadeHeaderController @Inject constructor( */ var shadeExpandedFraction = -1f set(value) { - if (visible && field != value) { + if (field != value) { header.alpha = ShadeInterpolation.getContentAlpha(value) field = value + updateVisibility() } } @@ -331,6 +332,9 @@ class LargeScreenShadeHeaderController @Inject constructor( .setDuration(duration) .alpha(if (show) 0f else 1f) .setInterpolator(if (show) Interpolators.ALPHA_OUT else Interpolators.ALPHA_IN) + .setUpdateListener { + updateVisibility() + } .start() } @@ -414,7 +418,7 @@ class LargeScreenShadeHeaderController @Inject constructor( private fun updateVisibility() { val visibility = if (!largeScreenActive && !combinedHeaders || qsDisabled) { View.GONE - } else if (qsVisible) { + } else if (qsVisible && header.alpha > 0f) { View.VISIBLE } else { View.INVISIBLE diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index b48fd9846798..93e815174b50 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -2340,7 +2340,7 @@ public final class NotificationPanelViewController implements Dumpable { // When false, down but not synthesized motion event. mLastEventSynthesizedDown = mExpectingSynthesizedDown; mLastDownEvents.insert( - mSystemClock.currentTimeMillis(), + event.getEventTime(), mDownX, mDownY, mQsTouchAboveFalsingThreshold, diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index 5fedbeb556c2..11617be40f53 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -36,16 +36,9 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { buffer.log(TAG, LogLevel.DEBUG, msg) } - private inline fun log( - logLevel: LogLevel, - initializer: LogMessage.() -> Unit, - noinline printer: LogMessage.() -> String - ) { - buffer.log(TAG, logLevel, initializer, printer) - } - fun onQsInterceptMoveQsTrackingEnabled(h: Float) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { double1 = h.toDouble() }, { "onQsIntercept: move action, QS tracking enabled. h = $double1" } @@ -62,7 +55,8 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { keyguardShowing: Boolean, qsExpansionEnabled: Boolean ) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { int1 = initialTouchY.toInt() @@ -82,7 +76,8 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { } fun logMotionEvent(event: MotionEvent, message: String) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { str1 = message @@ -99,7 +94,8 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { } fun logMotionEventStatusBarState(event: MotionEvent, statusBarState: Int, message: String) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { str1 = message @@ -128,25 +124,33 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { tracking: Boolean, dragDownPxAmount: Float, ) { - log(LogLevel.VERBOSE, { - str1 = message - double1 = fraction.toDouble() - bool1 = expanded - bool2 = tracking - long1 = dragDownPxAmount.toLong() - }, { - "$str1 fraction=$double1,expanded=$bool1," + + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = message + double1 = fraction.toDouble() + bool1 = expanded + bool2 = tracking + long1 = dragDownPxAmount.toLong() + }, + { + "$str1 fraction=$double1,expanded=$bool1," + "tracking=$bool2," + "dragDownPxAmount=$dragDownPxAmount" - }) + } + ) } fun logHasVibrated(hasVibratedOnOpen: Boolean, fraction: Float) { - log(LogLevel.VERBOSE, { - bool1 = hasVibratedOnOpen - double1 = fraction.toDouble() - }, { - "hasVibratedOnOpen=$bool1, expansionFraction=$double1" - }) + buffer.log( + TAG, + LogLevel.VERBOSE, + { + bool1 = hasVibratedOnOpen + double1 = fraction.toDouble() + }, + { "hasVibratedOnOpen=$bool1, expansionFraction=$double1" } + ) } fun logQsExpansionChanged( @@ -159,42 +163,56 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { qsAnimatorExpand: Boolean, animatingQs: Boolean ) { - log(LogLevel.VERBOSE, { - str1 = message - bool1 = qsExpanded - int1 = qsMinExpansionHeight - int2 = qsMaxExpansionHeight - bool2 = stackScrollerOverscrolling - bool3 = dozing - bool4 = qsAnimatorExpand - // 0 = false, 1 = true - long1 = animatingQs.compareTo(false).toLong() - }, { - "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," + + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = message + bool1 = qsExpanded + int1 = qsMinExpansionHeight + int2 = qsMaxExpansionHeight + bool2 = stackScrollerOverscrolling + bool3 = dozing + bool4 = qsAnimatorExpand + // 0 = false, 1 = true + long1 = animatingQs.compareTo(false).toLong() + }, + { + "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," + "stackScrollerOverscrolling=$bool2,dozing=$bool3,qsAnimatorExpand=$bool4," + "animatingQs=$long1" - }) + } + ) } fun logSingleTapUp(isDozing: Boolean, singleTapEnabled: Boolean, isNotDocked: Boolean) { - log(LogLevel.DEBUG, { - bool1 = isDozing - bool2 = singleTapEnabled - bool3 = isNotDocked - }, { - "PulsingGestureListener#onSingleTapUp all of this must true for single " + - "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3" + buffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = isDozing + bool2 = singleTapEnabled + bool3 = isNotDocked + }, + { + "PulsingGestureListener#onSingleTapUp all of this must true for single " + + "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3" }) } fun logSingleTapUpFalsingState(proximityIsNotNear: Boolean, isNotFalseTap: Boolean) { - log(LogLevel.DEBUG, { - bool1 = proximityIsNotNear - bool2 = isNotFalseTap - }, { - "PulsingGestureListener#onSingleTapUp all of this must true for single " + + buffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = proximityIsNotNear + bool2 = isNotFalseTap + }, + { + "PulsingGestureListener#onSingleTapUp all of this must true for single " + "tap to be detected: proximityIsNotNear: $bool1, isNotFalseTap: $bool2" - }) + } + ) } fun logNotInterceptingTouchInstantExpanding( @@ -202,13 +220,18 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { notificationsDragEnabled: Boolean, touchDisabled: Boolean ) { - log(LogLevel.VERBOSE, { - bool1 = instantExpanding - bool2 = notificationsDragEnabled - bool3 = touchDisabled - }, { - "NPVC not intercepting touch, instantExpanding: $bool1, " + + buffer.log( + TAG, + LogLevel.VERBOSE, + { + bool1 = instantExpanding + bool2 = notificationsDragEnabled + bool3 = touchDisabled + }, + { + "NPVC not intercepting touch, instantExpanding: $bool1, " + "!notificationsDragEnabled: $bool2, touchDisabled: $bool3" - }) + } + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt index c6a6e875b82d..9851625b6152 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt @@ -32,11 +32,21 @@ class ShadeWindowLogger @Inject constructor(@ShadeWindowLog private val buffer: ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) { fun logApplyingWindowLayoutParams(lp: WindowManager.LayoutParams) { - log(DEBUG, { str1 = lp.toString() }, { "Applying new window layout params: $str1" }) + buffer.log( + TAG, + DEBUG, + { str1 = lp.toString() }, + { "Applying new window layout params: $str1" } + ) } fun logNewState(state: Any) { - log(DEBUG, { str1 = state.toString() }, { "Applying new state: $str1" }) + buffer.log( + TAG, + DEBUG, + { str1 = state.toString() }, + { "Applying new state: $str1" } + ) } private inline fun log( @@ -48,11 +58,16 @@ class ShadeWindowLogger @Inject constructor(@ShadeWindowLog private val buffer: } fun logApplyVisibility(visible: Boolean) { - log(DEBUG, { bool1 = visible }, { "Updating visibility, should be visible : $bool1" }) + buffer.log( + TAG, + DEBUG, + { bool1 = visible }, + { "Updating visibility, should be visible : $bool1" }) } fun logShadeVisibleAndFocusable(visible: Boolean) { - log( + buffer.log( + TAG, DEBUG, { bool1 = visible }, { "Updating shade, should be visible and focusable: $bool1" } @@ -60,6 +75,11 @@ class ShadeWindowLogger @Inject constructor(@ShadeWindowLog private val buffer: } fun logShadeFocusable(focusable: Boolean) { - log(DEBUG, { bool1 = focusable }, { "Updating shade, should be focusable : $bool1" }) + buffer.log( + TAG, + DEBUG, + { bool1 = focusable }, + { "Updating shade, should be focusable : $bool1" } + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 96a61695ded1..0f5213373cb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -41,6 +41,7 @@ import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewCont import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_USER_LOCKED; import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON; import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY; +import static com.android.systemui.plugins.log.LogLevel.ERROR; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; @@ -1044,7 +1045,7 @@ public class KeyguardIndicationController { mChargingTimeRemaining = mPowerPluggedIn ? mBatteryInfo.computeChargeTimeRemaining() : -1; } catch (RemoteException e) { - mKeyguardLogger.logException(e, "Error calling IBatteryStats"); + mKeyguardLogger.log(TAG, ERROR, "Error calling IBatteryStats", e); mChargingTimeRemaining = -1; } updateDeviceEntryIndication(!wasPluggedIn && mPowerPluggedInWired); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 8f9365cd4dc4..99081e98c4a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -65,8 +65,6 @@ import com.android.systemui.statusbar.policy.RemoteInputView; import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.ListenerSet; -import dagger.Lazy; - import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -74,6 +72,8 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; +import dagger.Lazy; + /** * Class for handling remote input state over a set of notifications. This class handles things * like keeping notifications temporarily that were cancelled as a response to a remote input @@ -465,8 +465,7 @@ public class NotificationRemoteInputManager implements Dumpable { riv.getController().setRemoteInput(input); riv.getController().setRemoteInputs(inputs); riv.getController().setEditedSuggestionInfo(editedSuggestionInfo); - ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null; - riv.focusAnimated(parent); + riv.focusAnimated(); if (userMessageContent != null) { riv.setEditTextContent(userMessageContent); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java index c4961029dc33..b084a765956d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java @@ -109,7 +109,7 @@ public class ActivatableNotificationViewController return true; } if (ev.getAction() == MotionEvent.ACTION_UP) { - mView.setLastActionUpTime(SystemClock.uptimeMillis()); + mView.setLastActionUpTime(ev.getEventTime()); } // With a11y, just do nothing. if (mAccessibilityManager.isTouchExplorationEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 8d48d738f0f2..9b93d7b9e1d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -1431,6 +1431,22 @@ public class NotificationChildrenContainer extends ViewGroup @Override public void applyRoundnessAndInvalidate() { boolean last = true; + if (mUseRoundnessSourceTypes) { + if (mNotificationHeaderWrapper != null) { + mNotificationHeaderWrapper.requestTopRoundness( + /* value = */ getTopRoundness(), + /* sourceType = */ FROM_PARENT, + /* animate = */ false + ); + } + if (mNotificationHeaderWrapperLowPriority != null) { + mNotificationHeaderWrapperLowPriority.requestTopRoundness( + /* value = */ getTopRoundness(), + /* sourceType = */ FROM_PARENT, + /* animate = */ false + ); + } + } for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == View.GONE) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index ca1e397f930a..356ddfa2d482 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -1811,9 +1811,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override @ShadeViewRefactor(RefactorComponent.COORDINATOR) public WindowInsets onApplyWindowInsets(WindowInsets insets) { - mBottomInset = insets.getSystemWindowInsetBottom() - + insets.getInsets(WindowInsets.Type.ime()).bottom; - + mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom; mWaterfallTopInset = 0; final DisplayCutout cutout = insets.getDisplayCutout(); if (cutout != null) { @@ -2262,7 +2260,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.COORDINATOR) private int getImeInset() { - return Math.max(0, mBottomInset - (getRootView().getHeight() - getHeight())); + // The NotificationStackScrollLayout does not extend all the way to the bottom of the + // display. Therefore, subtract that space from the mBottomInset, in order to only include + // the portion of the bottom inset that actually overlaps the NotificationStackScrollLayout. + return Math.max(0, mBottomInset + - (getRootView().getHeight() - getHeight() - getLocationOnScreen()[1])); } /** @@ -2970,12 +2972,19 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable childInGroup = (ExpandableNotificationRow) requestedView; requestedView = requestedRow = childInGroup.getNotificationParent(); } - int position = 0; + final float scrimTopPadding = mAmbientState.isOnKeyguard() ? 0 : mMinimumPaddings; + int position = (int) scrimTopPadding; + int visibleIndex = -1; + ExpandableView lastVisibleChild = null; for (int i = 0; i < getChildCount(); i++) { ExpandableView child = getChildAtIndex(i); boolean notGone = child.getVisibility() != View.GONE; + if (notGone) visibleIndex++; if (notGone && !child.hasNoContentHeight()) { - if (position != 0) { + if (position != scrimTopPadding) { + if (lastVisibleChild != null) { + position += calculateGapHeight(lastVisibleChild, child, visibleIndex); + } position += mPaddingBetweenElements; } } @@ -2987,6 +2996,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } if (notGone) { position += getIntrinsicHeight(child); + lastVisibleChild = child; } } return 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java index 9070eadd9944..149ec545dfa7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java @@ -154,9 +154,7 @@ public class AutoTileManager implements UserAwareController { if (!mAutoTracker.isAdded(SAVER)) { mDataSaverController.addCallback(mDataSaverListener); } - if (!mAutoTracker.isAdded(WORK)) { - mManagedProfileController.addCallback(mProfileCallback); - } + mManagedProfileController.addCallback(mProfileCallback); if (!mAutoTracker.isAdded(NIGHT) && ColorDisplayManager.isNightDisplayAvailable(mContext)) { mNightDisplayListener.setCallback(mNightDisplayCallback); @@ -275,18 +273,18 @@ public class AutoTileManager implements UserAwareController { return mCurrentUser.getIdentifier(); } - public void unmarkTileAsAutoAdded(String tabSpec) { - mAutoTracker.setTileRemoved(tabSpec); - } - private final ManagedProfileController.Callback mProfileCallback = new ManagedProfileController.Callback() { @Override public void onManagedProfileChanged() { - if (mAutoTracker.isAdded(WORK)) return; if (mManagedProfileController.hasActiveProfile()) { + if (mAutoTracker.isAdded(WORK)) return; mHost.addTile(WORK); mAutoTracker.setTileAdded(WORK); + } else { + if (!mAutoTracker.isAdded(WORK)) return; + mHost.removeTile(WORK); + mAutoTracker.setTileRemoved(WORK); } } @@ -429,7 +427,7 @@ public class AutoTileManager implements UserAwareController { initSafetyTile(); } else if (!isSafetyCenterEnabled && mAutoTracker.isAdded(mSafetySpec)) { mHost.removeTile(mSafetySpec); - mHost.unmarkTileAsAutoAdded(mSafetySpec); + mAutoTracker.setTileRemoved(mSafetySpec); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 9a8c5d709f3a..9f3836105a95 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.phone; import static android.app.StatusBarManager.SESSION_KEYGUARD; +import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME; + import android.annotation.IntDef; import android.content.res.Resources; import android.hardware.biometrics.BiometricFaceConstants; @@ -27,7 +29,6 @@ import android.hardware.fingerprint.FingerprintManager; import android.metrics.LogMaker; import android.os.Handler; import android.os.PowerManager; -import android.os.SystemClock; import android.os.Trace; import androidx.annotation.Nullable; @@ -59,6 +60,7 @@ import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.time.SystemClock; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -75,6 +77,7 @@ import javax.inject.Inject; */ @SysUISingleton public class BiometricUnlockController extends KeyguardUpdateMonitorCallback implements Dumpable { + private static final long RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS = 400L; private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000; private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); @@ -165,9 +168,11 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private final MetricsLogger mMetricsLogger; private final AuthController mAuthController; private final StatusBarStateController mStatusBarStateController; + private final WakefulnessLifecycle mWakefulnessLifecycle; private final LatencyTracker mLatencyTracker; private final VibratorHelper mVibratorHelper; private final BiometricUnlockLogger mLogger; + private final SystemClock mSystemClock; private long mLastFpFailureUptimeMillis; private int mNumConsecutiveFpFailures; @@ -272,13 +277,16 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp SessionTracker sessionTracker, LatencyTracker latencyTracker, ScreenOffAnimationController screenOffAnimationController, - VibratorHelper vibrator) { + VibratorHelper vibrator, + SystemClock systemClock + ) { mPowerManager = powerManager; mUpdateMonitor = keyguardUpdateMonitor; mUpdateMonitor.registerCallback(this); mMediaManager = notificationMediaManager; mLatencyTracker = latencyTracker; - wakefulnessLifecycle.addObserver(mWakefulnessObserver); + mWakefulnessLifecycle = wakefulnessLifecycle; + mWakefulnessLifecycle.addObserver(mWakefulnessObserver); screenLifecycle.addObserver(mScreenObserver); mNotificationShadeWindowController = notificationShadeWindowController; @@ -297,6 +305,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mScreenOffAnimationController = screenOffAnimationController; mVibratorHelper = vibrator; mLogger = biometricUnlockLogger; + mSystemClock = systemClock; dumpManager.registerDumpable(getClass().getName(), this); } @@ -420,8 +429,11 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp Runnable wakeUp = ()-> { if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) { mLogger.i("bio wakelock: Authenticated, waking up..."); - mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC, - "android.policy:BIOMETRIC"); + mPowerManager.wakeUp( + mSystemClock.uptimeMillis(), + PowerManager.WAKE_REASON_BIOMETRIC, + "android.policy:BIOMETRIC" + ); } Trace.beginSection("release wake-and-unlock"); releaseBiometricWakeLock(); @@ -652,7 +664,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp startWakeAndUnlock(MODE_ONLY_WAKE); } else if (biometricSourceType == BiometricSourceType.FINGERPRINT && mUpdateMonitor.isUdfpsSupported()) { - long currUptimeMillis = SystemClock.uptimeMillis(); + long currUptimeMillis = mSystemClock.uptimeMillis(); if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) { mNumConsecutiveFpFailures += 1; } else { @@ -700,12 +712,26 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp cleanup(); } - //these haptics are for device-entry only + // these haptics are for device-entry only private void vibrateSuccess(BiometricSourceType type) { + if (mAuthController.isSfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()) + && lastWakeupFromPowerButtonWithinHapticThreshold()) { + mLogger.d("Skip auth success haptic. Power button was recently pressed."); + return; + } mVibratorHelper.vibrateAuthSuccess( getClass().getSimpleName() + ", type =" + type + "device-entry::success"); } + private boolean lastWakeupFromPowerButtonWithinHapticThreshold() { + final boolean lastWakeupFromPowerButton = mWakefulnessLifecycle.getLastWakeReason() + == PowerManager.WAKE_REASON_POWER_BUTTON; + return lastWakeupFromPowerButton + && mWakefulnessLifecycle.getLastWakeTime() != UNKNOWN_LAST_WAKE_TIME + && mSystemClock.uptimeMillis() - mWakefulnessLifecycle.getLastWakeTime() + < RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS; + } + private void vibrateError(BiometricSourceType type) { mVibratorHelper.vibrateAuthError( getClass().getSimpleName() + ", type =" + type + "device-entry::error"); @@ -798,7 +824,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp if (mUpdateMonitor.isUdfpsSupported()) { pw.print(" mNumConsecutiveFpFailures="); pw.println(mNumConsecutiveFpFailures); pw.print(" time since last failure="); - pw.println(SystemClock.uptimeMillis() - mLastFpFailureUptimeMillis); + pw.println(mSystemClock.uptimeMillis() - mLastFpFailureUptimeMillis); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 22ebcab777aa..4b56594e082d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -4247,8 +4247,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void onDozeAmountChanged(float linear, float eased) { - if (mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) - && !mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION) + if (!mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION) && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) { mLightRevealScrim.setRevealAmount(1f - linear); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index de7b152adaab..0446cefb10dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -44,10 +44,9 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.tuner.TunerService; @@ -82,7 +81,6 @@ public class DozeParameters implements private final AlwaysOnDisplayPolicy mAlwaysOnPolicy; private final Resources mResources; private final BatteryController mBatteryController; - private final FeatureFlags mFeatureFlags; private final ScreenOffAnimationController mScreenOffAnimationController; private final FoldAodAnimationController mFoldAodAnimationController; private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; @@ -125,7 +123,6 @@ public class DozeParameters implements BatteryController batteryController, TunerService tunerService, DumpManager dumpManager, - FeatureFlags featureFlags, ScreenOffAnimationController screenOffAnimationController, Optional<SysUIUnfoldComponent> sysUiUnfoldComponent, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, @@ -141,7 +138,6 @@ public class DozeParameters implements mControlScreenOffAnimation = !getDisplayNeedsBlanking(); mPowerManager = powerManager; mPowerManager.setDozeAfterScreenOff(!mControlScreenOffAnimation); - mFeatureFlags = featureFlags; mScreenOffAnimationController = screenOffAnimationController; mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; @@ -162,6 +158,13 @@ public class DozeParameters implements SettingsObserver quickPickupSettingsObserver = new SettingsObserver(context, handler); quickPickupSettingsObserver.observe(); + + batteryController.addCallback(new BatteryStateChangeCallback() { + @Override + public void onPowerSaveChanged(boolean isPowerSave) { + dispatchAlwaysOnEvent(); + } + }); } private void updateQuickPickupEnabled() { @@ -300,13 +303,10 @@ public class DozeParameters implements /** * Whether we're capable of controlling the screen off animation if we want to. This isn't - * possible if AOD isn't even enabled or if the flag is disabled, or if the display needs - * blanking. + * possible if AOD isn't even enabled or if the display needs blanking. */ public boolean canControlUnlockedScreenOff() { - return getAlwaysOn() - && mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) - && !getDisplayNeedsBlanking(); + return getAlwaysOn() && !getDisplayNeedsBlanking(); } /** @@ -424,9 +424,7 @@ public class DozeParameters implements updateControlScreenOff(); } - for (Callback callback : mCallbacks) { - callback.onAlwaysOnChange(); - } + dispatchAlwaysOnEvent(); mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn()); } @@ -463,6 +461,12 @@ public class DozeParameters implements pw.print("isQuickPickupEnabled(): "); pw.println(isQuickPickupEnabled()); } + private void dispatchAlwaysOnEvent() { + for (Callback callback : mCallbacks) { + callback.onAlwaysOnChange(); + } + } + private boolean getPostureSpecificBool( int[] postureMapping, boolean defaultSensorBool, @@ -477,7 +481,8 @@ public class DozeParameters implements return bool; } - interface Callback { + /** Callbacks for doze parameter related information */ + public interface Callback { /** * Invoked when the value of getAlwaysOn may have changed. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 348357445223..4ad319969eaf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -45,6 +45,7 @@ import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.battery.BatteryMeterViewController; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.CommandQueue; @@ -76,6 +77,7 @@ import javax.inject.Inject; /** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */ public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> { + private static final String TAG = "KeyguardStatusBarViewController"; private static final AnimationProperties KEYGUARD_HUN_PROPERTIES = new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); @@ -422,7 +424,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar in. */ public void animateKeyguardStatusBarIn() { - mLogger.d("animating status bar in"); + mLogger.log(TAG, LogLevel.DEBUG, "animating status bar in"); if (mDisableStateTracker.isDisabled()) { // If our view is disabled, don't allow us to animate in. return; @@ -438,7 +440,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar out. */ public void animateKeyguardStatusBarOut(long startDelay, long duration) { - mLogger.d("animating status bar out"); + mLogger.log(TAG, LogLevel.DEBUG, "animating status bar out"); ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f); anim.addUpdateListener(mAnimatorUpdateListener); anim.setStartDelay(startDelay); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt index 08599c27f4b8..fbe374c32952 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone +import android.view.InsetsFlags +import android.view.ViewDebug import android.view.WindowInsets.Type.InsetsType import android.view.WindowInsetsController.Appearance import android.view.WindowInsetsController.Behavior @@ -148,4 +150,20 @@ private data class SystemBarAttributesParams( ) { val letterboxesArray = letterboxes.toTypedArray() val appearanceRegionsArray = appearanceRegions.toTypedArray() + override fun toString(): String { + val appearanceToString = + ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance) + return """SystemBarAttributesParams( + displayId=$displayId, + appearance=$appearanceToString, + appearanceRegions=$appearanceRegions, + navbarColorManagedByIme=$navbarColorManagedByIme, + behavior=$behavior, + requestedVisibleTypes=$requestedVisibleTypes, + packageName='$packageName', + letterboxes=$letterboxes, + letterboxesArray=${letterboxesArray.contentToString()}, + appearanceRegionsArray=${appearanceRegionsArray.contentToString()} + )""".trimMargin() + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt index 0e164e7ee859..8ac12379e59e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -149,7 +149,7 @@ constructor( } private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository { - val tableLogBuffer = logFactory.create("DemoMobileConnectionLog [$subId]", 100) + val tableLogBuffer = logFactory.getOrCreate("DemoMobileConnectionLog [$subId]", 100) return DemoMobileConnectionRepository( subId, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 0fa0fea0bebf..4e42f9b31e5c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -308,7 +308,7 @@ class MobileConnectionRepositoryImpl( networkNameSeparator: String, globalMobileDataSettingChangedEvent: Flow<Unit>, ): MobileConnectionRepository { - val mobileLogger = logFactory.create(tableBufferLogName(subId), 100) + val mobileLogger = logFactory.getOrCreate(tableBufferLogName(subId), 100) return MobileConnectionRepositoryImpl( context, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index c9ed0cb4155d..f8c17e8c8379 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -109,6 +109,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33; private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83; private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f; + private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120; + private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180; public final Object mToken = new Object(); @@ -421,7 +423,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } @VisibleForTesting - void onDefocus(boolean animate, boolean logClose) { + void onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus) { mController.removeRemoteInput(mEntry, mToken); mEntry.remoteInputText = mEditText.getText(); @@ -431,18 +433,20 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene ViewGroup parent = (ViewGroup) getParent(); if (animate && parent != null && mIsFocusAnimationFlagActive) { - ViewGroup grandParent = (ViewGroup) parent.getParent(); ViewGroupOverlay overlay = parent.getOverlay(); + View actionsContainer = getActionsContainerLayout(); + int actionsContainerHeight = + actionsContainer != null ? actionsContainer.getHeight() : 0; // After adding this RemoteInputView to the overlay of the parent (and thus removing // it from the parent itself), the parent will shrink in height. This causes the // overlay to be moved. To correct the position of the overlay we need to offset it. - int overlayOffsetY = getMaxSiblingHeight() - getHeight(); + int overlayOffsetY = actionsContainerHeight - getHeight(); overlay.add(this); if (grandParent != null) grandParent.setClipChildren(false); - Animator animator = getDefocusAnimator(overlayOffsetY); + Animator animator = getDefocusAnimator(actionsContainer, overlayOffsetY); View self = this; animator.addListener(new AnimatorListenerAdapter() { @Override @@ -454,8 +458,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene if (mWrapper != null) { mWrapper.setRemoteInputVisible(false); } + if (doAfterDefocus != null) { + doAfterDefocus.run(); + } } }); + if (actionsContainer != null) actionsContainer.setAlpha(0f); animator.start(); } else if (animate && mRevealParams != null && mRevealParams.radius > 0) { @@ -474,6 +482,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene reveal.start(); } else { setVisibility(GONE); + if (doAfterDefocus != null) doAfterDefocus.run(); if (mWrapper != null) { mWrapper.setRemoteInputVisible(false); } @@ -596,10 +605,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene /** * Focuses the RemoteInputView and animates its appearance - * - * @param crossFadeView view that will be crossfaded during the appearance animation */ - public void focusAnimated(View crossFadeView) { + public void focusAnimated() { if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE && mRevealParams != null) { android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this); @@ -609,7 +616,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) { mIsAnimatingAppearance = true; setAlpha(0f); - Animator focusAnimator = getFocusAnimator(crossFadeView); + Animator focusAnimator = getFocusAnimator(getActionsContainerLayout()); focusAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation, boolean isReverse) { @@ -661,6 +668,23 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void reset() { + if (mIsFocusAnimationFlagActive) { + mProgressBar.setVisibility(INVISIBLE); + mResetting = true; + mSending = false; + onDefocus(true /* animate */, false /* logClose */, () -> { + mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); + mEditText.getText().clear(); + mEditText.setEnabled(isAggregatedVisible()); + mSendButton.setVisibility(VISIBLE); + mController.removeSpinning(mEntry.getKey(), mToken); + updateSendButton(); + setAttachment(null); + mResetting = false; + }); + return; + } + mResetting = true; mSending = false; mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); @@ -671,7 +695,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mProgressBar.setVisibility(INVISIBLE); mController.removeSpinning(mEntry.getKey(), mToken); updateSendButton(); - onDefocus(false /* animate */, false /* logClose */); + onDefocus(false /* animate */, false /* logClose */, null /* doAfterDefocus */); setAttachment(null); mResetting = false; @@ -825,23 +849,22 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } /** - * @return max sibling height (0 in case of no siblings) + * @return action button container view (i.e. ViewGroup containing Reply button etc.) */ - public int getMaxSiblingHeight() { + public View getActionsContainerLayout() { ViewGroup parentView = (ViewGroup) getParent(); - int maxHeight = 0; - if (parentView == null) return 0; - for (int i = 0; i < parentView.getChildCount(); i++) { - View siblingView = parentView.getChildAt(i); - if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight()); - } - return maxHeight; + if (parentView == null) return null; + return parentView.findViewById(com.android.internal.R.id.actions_container_layout); } /** * Creates an animator for the focus animation. + * + * @param fadeOutView View that will be faded out during the focus animation. */ - private Animator getFocusAnimator(View crossFadeView) { + private Animator getFocusAnimator(@Nullable View fadeOutView) { + final AnimatorSet animatorSet = new AnimatorSet(); + final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f); alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY); alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); @@ -854,30 +877,36 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); - final Animator crossFadeViewAlphaAnimator = - ObjectAnimator.ofFloat(crossFadeView, View.ALPHA, 1f, 0f); - crossFadeViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); - crossFadeViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); - alphaAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation, boolean isReverse) { - crossFadeView.setAlpha(1f); - } - }); - - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(alphaAnimator, scaleAnimator, crossFadeViewAlphaAnimator); + if (fadeOutView == null) { + animatorSet.playTogether(alphaAnimator, scaleAnimator); + } else { + final Animator fadeOutViewAlphaAnimator = + ObjectAnimator.ofFloat(fadeOutView, View.ALPHA, 1f, 0f); + fadeOutViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); + fadeOutViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation, boolean isReverse) { + fadeOutView.setAlpha(1f); + } + }); + animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeOutViewAlphaAnimator); + } return animatorSet; } /** * Creates an animator for the defocus animation. * - * @param offsetY The RemoteInputView will be offset by offsetY during the animation + * @param fadeInView View that will be faded in during the defocus animation. + * @param offsetY The RemoteInputView will be offset by offsetY during the animation */ - private Animator getDefocusAnimator(int offsetY) { + private Animator getDefocusAnimator(@Nullable View fadeInView, int offsetY) { + final AnimatorSet animatorSet = new AnimatorSet(); + final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f); - alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); + alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); + alphaAnimator.setStartDelay(DEFOCUS_ANIMATION_FADE_OUT_DELAY); alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE); @@ -893,8 +922,17 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } }); - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(alphaAnimator, scaleAnimator); + if (fadeInView == null) { + animatorSet.playTogether(alphaAnimator, scaleAnimator); + } else { + fadeInView.forceHasOverlappingRendering(false); + Animator fadeInViewAlphaAnimator = + ObjectAnimator.ofFloat(fadeInView, View.ALPHA, 0f, 1f); + fadeInViewAlphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); + fadeInViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); + fadeInViewAlphaAnimator.setStartDelay(DEFOCUS_ANIMATION_CROSSFADE_DELAY); + animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeInViewAlphaAnimator); + } return animatorSet; } @@ -1011,7 +1049,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene if (isFocusable() && isEnabled()) { setInnerFocusable(false); if (mRemoteInputView != null) { - mRemoteInputView.onDefocus(animate, true /* logClose */); + mRemoteInputView + .onDefocus(animate, true /* logClose */, null /* doAfterDefocus */); } mShowImeOnInputConnection = false; } diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt deleted file mode 100644 index 154c6e2e3158..000000000000 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt +++ /dev/null @@ -1,136 +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.stylus - -import android.content.Context -import android.hardware.BatteryState -import android.hardware.input.InputManager -import android.os.Handler -import android.util.Log -import android.view.InputDevice -import androidx.annotation.VisibleForTesting -import com.android.systemui.CoreStartable -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import java.util.concurrent.Executor -import javax.inject.Inject - -/** - * A listener that detects when a stylus has first been used, by detecting 1) the presence of an - * internal SOURCE_STYLUS with a battery, or 2) any added SOURCE_STYLUS device with a bluetooth - * address. - */ -@SysUISingleton -class StylusFirstUsageListener -@Inject -constructor( - private val context: Context, - private val inputManager: InputManager, - private val stylusManager: StylusManager, - private val featureFlags: FeatureFlags, - @Background private val executor: Executor, - @Background private val handler: Handler, -) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener { - - // Set must be only accessed from the background handler, which is the same handler that - // runs the StylusManager callbacks. - private val internalStylusDeviceIds: MutableSet<Int> = mutableSetOf() - @VisibleForTesting var hasStarted = false - - override fun start() { - if (true) return // TODO(b/261826950): remove on main - if (hasStarted) return - if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return - if (inputManager.isStylusEverUsed(context)) return - if (!hostDeviceSupportsStylusInput()) return - - hasStarted = true - inputManager.inputDeviceIds.forEach(this::onStylusAdded) - stylusManager.registerCallback(this) - stylusManager.startListener() - } - - override fun onStylusAdded(deviceId: Int) { - if (!hasStarted) return - - val device = inputManager.getInputDevice(deviceId) ?: return - if (device.isExternal || !device.supportsSource(InputDevice.SOURCE_STYLUS)) return - - try { - inputManager.addInputDeviceBatteryListener(deviceId, executor, this) - internalStylusDeviceIds += deviceId - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to register battery listener for $deviceId ${device.name}.") - } - } - - override fun onStylusRemoved(deviceId: Int) { - if (!hasStarted) return - - if (!internalStylusDeviceIds.contains(deviceId)) return - try { - inputManager.removeInputDeviceBatteryListener(deviceId, this) - internalStylusDeviceIds.remove(deviceId) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.") - } - } - - override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) { - if (!hasStarted) return - - onRemoteDeviceFound() - } - - override fun onBatteryStateChanged( - deviceId: Int, - eventTimeMillis: Long, - batteryState: BatteryState - ) { - if (!hasStarted) return - - if (batteryState.isPresent) { - onRemoteDeviceFound() - } - } - - private fun onRemoteDeviceFound() { - inputManager.setStylusEverUsed(context, true) - cleanupListeners() - } - - private fun cleanupListeners() { - stylusManager.unregisterCallback(this) - handler.post { - internalStylusDeviceIds.forEach { - inputManager.removeInputDeviceBatteryListener(it, this) - } - } - } - - private fun hostDeviceSupportsStylusInput(): Boolean { - return inputManager.inputDeviceIds - .asSequence() - .mapNotNull { inputManager.getInputDevice(it) } - .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal } - } - - companion object { - private val TAG = StylusFirstUsageListener::class.simpleName.orEmpty() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index 302d6a9ca1b7..235495cfa50d 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -18,6 +18,8 @@ package com.android.systemui.stylus import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.content.Context +import android.hardware.BatteryState import android.hardware.input.InputManager import android.os.Handler import android.util.ArrayMap @@ -25,6 +27,8 @@ import android.util.Log import android.view.InputDevice import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.Executor import javax.inject.Inject @@ -37,25 +41,37 @@ import javax.inject.Inject class StylusManager @Inject constructor( + private val context: Context, private val inputManager: InputManager, private val bluetoothAdapter: BluetoothAdapter?, @Background private val handler: Handler, @Background private val executor: Executor, -) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener { + private val featureFlags: FeatureFlags, +) : + InputManager.InputDeviceListener, + InputManager.InputDeviceBatteryListener, + BluetoothAdapter.OnMetadataChangedListener { private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList() private val stylusBatteryCallbacks: CopyOnWriteArrayList<StylusBatteryCallback> = CopyOnWriteArrayList() // This map should only be accessed on the handler private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap() + // This variable should only be accessed on the handler + private var hasStarted: Boolean = false /** * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot * at time of starting. */ fun startListener() { - addExistingStylusToMap() - inputManager.registerInputDeviceListener(this, handler) + handler.post { + if (hasStarted) return@post + hasStarted = true + addExistingStylusToMap() + + inputManager.registerInputDeviceListener(this, handler) + } } /** Registers a StylusCallback to listen to stylus events. */ @@ -77,21 +93,30 @@ constructor( } override fun onInputDeviceAdded(deviceId: Int) { + if (!hasStarted) return + val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return + if (!device.isExternal) { + registerBatteryListener(deviceId) + } + // TODO(b/257936830): get address once input api available val btAddress: String? = null inputDeviceAddressMap[deviceId] = btAddress executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) } if (btAddress != null) { + onStylusUsed() onStylusBluetoothConnected(btAddress) executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) } } } override fun onInputDeviceChanged(deviceId: Int) { + if (!hasStarted) return + val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return @@ -112,7 +137,10 @@ constructor( } override fun onInputDeviceRemoved(deviceId: Int) { + if (!hasStarted) return + if (!inputDeviceAddressMap.contains(deviceId)) return + unregisterBatteryListener(deviceId) val btAddress: String? = inputDeviceAddressMap[deviceId] inputDeviceAddressMap.remove(deviceId) @@ -124,13 +152,14 @@ constructor( } override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) { - handler.post executeMetadataChanged@{ - if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) - return@executeMetadataChanged + handler.post { + if (!hasStarted) return@post + + if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) return@post val inputDeviceId: Int = inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull() - ?: return@executeMetadataChanged + ?: return@post val isCharging = String(value) == "true" @@ -140,6 +169,24 @@ constructor( } } + override fun onBatteryStateChanged( + deviceId: Int, + eventTimeMillis: Long, + batteryState: BatteryState + ) { + handler.post { + if (!hasStarted) return@post + + if (batteryState.isPresent) { + onStylusUsed() + } + + executeStylusBatteryCallbacks { cb -> + cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState) + } + } + } + private fun onStylusBluetoothConnected(btAddress: String) { val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return try { @@ -158,6 +205,21 @@ constructor( } } + /** + * An InputDevice that supports [InputDevice.SOURCE_STYLUS] may still be present even when a + * physical stylus device has never been used. This method is run when 1) a USI stylus battery + * event happens, or 2) a bluetooth stylus is connected, as they are both indicators that a + * physical stylus device has actually been used. + */ + private fun onStylusUsed() { + if (true) return // TODO(b/261826950): remove on main + if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return + if (inputManager.isStylusEverUsed(context)) return + + inputManager.setStylusEverUsed(context, true) + executeStylusCallbacks { cb -> cb.onStylusFirstUsed() } + } + private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) { stylusCallbacks.forEach(run) } @@ -166,31 +228,69 @@ constructor( stylusBatteryCallbacks.forEach(run) } + private fun registerBatteryListener(deviceId: Int) { + try { + inputManager.addInputDeviceBatteryListener(deviceId, executor, this) + } catch (e: SecurityException) { + Log.e(TAG, "$e: Failed to register battery listener for $deviceId.") + } + } + + private fun unregisterBatteryListener(deviceId: Int) { + // If deviceId wasn't registered, the result is a no-op, so an "is registered" + // check is not needed. + try { + inputManager.removeInputDeviceBatteryListener(deviceId, this) + } catch (e: SecurityException) { + Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.") + } + } + private fun addExistingStylusToMap() { for (deviceId: Int in inputManager.inputDeviceIds) { val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue if (device.supportsSource(InputDevice.SOURCE_STYLUS)) { // TODO(b/257936830): get address once input api available inputDeviceAddressMap[deviceId] = null + + if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available + // For most devices, an active (non-bluetooth) stylus is represented by an + // internal InputDevice. This InputDevice will be present in InputManager + // before CoreStartables run, and will not be removed. + // In many cases, it reports the battery level of the stylus. + registerBatteryListener(deviceId) + } } } } - /** Callback interface to receive events from the StylusManager. */ + /** + * Callback interface to receive events from the StylusManager. All callbacks are run on the + * same background handler. + */ interface StylusCallback { fun onStylusAdded(deviceId: Int) {} fun onStylusRemoved(deviceId: Int) {} fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {} fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {} + fun onStylusFirstUsed() {} } - /** Callback interface to receive stylus battery events from the StylusManager. */ + /** + * Callback interface to receive stylus battery events from the StylusManager. All callbacks are + * runs on the same background handler. + */ interface StylusBatteryCallback { fun onStylusBluetoothChargingStateChanged( inputDeviceId: Int, btDevice: BluetoothDevice, isCharging: Boolean ) {} + fun onStylusUsiBatteryStateChanged( + deviceId: Int, + eventTimeMillis: Long, + batteryState: BatteryState, + ) {} } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt index 11233dda165c..14a9161ac291 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt @@ -18,14 +18,11 @@ package com.android.systemui.stylus import android.hardware.BatteryState import android.hardware.input.InputManager -import android.util.Log import android.view.InputDevice import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import java.util.concurrent.Executor import javax.inject.Inject /** @@ -40,16 +37,7 @@ constructor( private val inputManager: InputManager, private val stylusUsiPowerUi: StylusUsiPowerUI, private val featureFlags: FeatureFlags, - @Background private val executor: Executor, -) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener { - - override fun onStylusAdded(deviceId: Int) { - val device = inputManager.getInputDevice(deviceId) ?: return - - if (!device.isExternal) { - registerBatteryListener(deviceId) - } - } +) : CoreStartable, StylusManager.StylusCallback, StylusManager.StylusBatteryCallback { override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) { stylusUsiPowerUi.refresh() @@ -59,15 +47,7 @@ constructor( stylusUsiPowerUi.refresh() } - override fun onStylusRemoved(deviceId: Int) { - val device = inputManager.getInputDevice(deviceId) ?: return - - if (!device.isExternal) { - unregisterBatteryListener(deviceId) - } - } - - override fun onBatteryStateChanged( + override fun onStylusUsiBatteryStateChanged( deviceId: Int, eventTimeMillis: Long, batteryState: BatteryState @@ -77,39 +57,19 @@ constructor( } } - private fun registerBatteryListener(deviceId: Int) { - try { - inputManager.addInputDeviceBatteryListener(deviceId, executor, this) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to register battery listener for $deviceId.") - } - } - - private fun unregisterBatteryListener(deviceId: Int) { - try { - inputManager.removeInputDeviceBatteryListener(deviceId, this) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to unregister battery listener for $deviceId.") - } - } - override fun start() { if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return - addBatteryListenerForInternalStyluses() + if (!hostDeviceSupportsStylusInput()) return stylusManager.registerCallback(this) stylusManager.startListener() } - private fun addBatteryListenerForInternalStyluses() { - // For most devices, an active stylus is represented by an internal InputDevice. - // This InputDevice will be present in InputManager before CoreStartables run, - // and will not be removed. In many cases, it reports the battery level of the stylus. - inputManager.inputDeviceIds + private fun hostDeviceSupportsStylusInput(): Boolean { + return inputManager.inputDeviceIds .asSequence() .mapNotNull { inputManager.getInputDevice(it) } - .filter { it.supportsSource(InputDevice.SOURCE_STYLUS) } - .forEach { onStylusAdded(it.id) } + .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt index 70a5b366263e..e8216576811a 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt @@ -123,13 +123,13 @@ constructor( .setSmallIcon(R.drawable.ic_power_low) .setDeleteIntent(getPendingBroadcast(ACTION_DISMISSED_LOW_BATTERY)) .setContentIntent(getPendingBroadcast(ACTION_CLICKED_LOW_BATTERY)) - .setContentTitle(context.getString(R.string.stylus_battery_low)) - .setContentText( + .setContentTitle( context.getString( - R.string.battery_low_percent_format, + R.string.stylus_battery_low_percentage, NumberFormat.getPercentInstance().format(batteryCapacity) ) ) + .setContentText(context.getString(R.string.stylus_battery_low_subtitle)) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setLocalOnly(true) .setAutoCancel(true) @@ -177,7 +177,7 @@ constructor( // https://source.chromium.org/chromium/chromium/src/+/main:ash/system/power/peripheral_battery_notifier.cc;l=41 private const val LOW_BATTERY_THRESHOLD = 0.16f - private val USI_NOTIFICATION_ID = R.string.stylus_battery_low + private val USI_NOTIFICATION_ID = R.string.stylus_battery_low_percentage private const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss" private const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click" diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt index 59ad24a3e7bb..2709da38a7d8 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt @@ -17,6 +17,9 @@ package com.android.systemui.unfold import android.content.Context +import android.hardware.devicestate.DeviceStateManager +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.LifecycleScreenStatusProvider import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.system.SystemUnfoldSharedModule @@ -32,6 +35,7 @@ import dagger.Lazy import dagger.Module import dagger.Provides import java.util.Optional +import java.util.concurrent.Executor import javax.inject.Named import javax.inject.Singleton @@ -40,6 +44,20 @@ class UnfoldTransitionModule { @Provides @UnfoldTransitionATracePrefix fun tracingTagPrefix() = "systemui" + /** A globally available FoldStateListener that allows one to query the fold state. */ + @Provides + @Singleton + fun providesFoldStateListener( + deviceStateManager: DeviceStateManager, + @Application context: Context, + @Main executor: Executor + ): DeviceStateManager.FoldStateListener { + val listener = DeviceStateManager.FoldStateListener(context) + deviceStateManager.registerCallback(executor, listener) + + return listener + } + @Provides @Singleton fun providesFoldStateLoggingProvider( diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index b4baa44985bb..c76b127a161c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -84,7 +84,8 @@ class ClockEventControllerTest : SysuiTestCase() { @Mock private lateinit var transitionRepository: KeyguardTransitionRepository @Mock private lateinit var commandQueue: CommandQueue private lateinit var repository: FakeKeyguardRepository - @Mock private lateinit var logBuffer: LogBuffer + @Mock private lateinit var smallLogBuffer: LogBuffer + @Mock private lateinit var largeLogBuffer: LogBuffer private lateinit var underTest: ClockEventController @Before @@ -111,7 +112,8 @@ class ClockEventControllerTest : SysuiTestCase() { context, mainExecutor, bgExecutor, - logBuffer, + smallLogBuffer, + largeLogBuffer, featureFlags ) underTest.clock = clock diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index c8e753844c64..9a9acf3dd986 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -48,6 +48,7 @@ import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; import com.android.systemui.plugins.ClockEvents; import com.android.systemui.plugins.ClockFaceController; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.AnimatableClockView; import com.android.systemui.shared.clocks.ClockRegistry; @@ -115,6 +116,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private FrameLayout mLargeClockFrame; @Mock private SecureSettings mSecureSettings; + @Mock + private LogBuffer mLogBuffer; private final View mFakeSmartspaceView = new View(mContext); @@ -156,7 +159,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mSecureSettings, mExecutor, mDumpManager, - mClockEventController + mClockEventController, + mLogBuffer ); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java index 254f9531ef83..8dc1e8fba600 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java @@ -16,6 +16,7 @@ package com.android.keyguard; +import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; @@ -189,6 +190,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -198,6 +200,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -212,6 +215,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -223,6 +227,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index ac22de9f9c52..87dd6a4cfa5e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -33,6 +33,9 @@ import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_CANCELL import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.HAL_POWER_PRESS_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import static com.google.common.truth.Truth.assertThat; @@ -93,6 +96,7 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.service.dreams.IDreamManager; import android.service.trust.TrustAgentService; import android.telephony.ServiceState; @@ -125,6 +129,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; +import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.settings.SecureSettings; @@ -194,6 +199,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private DevicePolicyManager mDevicePolicyManager; @Mock + private DevicePostureController mDevicePostureController; + @Mock private IDreamManager mDreamManager; @Mock private KeyguardBypassController mKeyguardBypassController; @@ -300,6 +307,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .thenReturn(new ServiceState()); when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings); when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false); + when(mDevicePostureController.getDevicePosture()).thenReturn(DEVICE_POSTURE_UNKNOWN); mMockitoSession = ExtendedMockito.mockitoSession() .spyStatic(SubscriptionManager.class) @@ -311,6 +319,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mUserTracker.getUserId()).thenReturn(mCurrentUserId); ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService); + mContext.getOrCreateTestableResources().addOverride( + com.android.systemui.R.integer.config_face_auth_supported_posture, + DEVICE_POSTURE_UNKNOWN); mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig( mContext.getResources(), mGlobalSettings, @@ -1254,7 +1265,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void testStartsListeningForSfps_whenKeyguardIsVisible_ifRequireScreenOnToAuthEnabled() + public void startsListeningForSfps_whenKeyguardIsVisible_ifRequireInteractiveToAuthEnabled() throws RemoteException { // SFPS supported and enrolled final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); @@ -1262,12 +1273,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mAuthController.getSfpsProps()).thenReturn(props); when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - // WHEN require screen on to auth is disabled, and keyguard is not awake + // WHEN require interactive to auth is disabled, and keyguard is not awake when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false); - mContext.getOrCreateTestableResources().addOverride( - com.android.internal.R.bool.config_requireScreenOnToAuthEnabled, true); - // Preconditions for sfps auth to run keyguardNotGoingAway(); currentUserIsPrimary(); @@ -1282,8 +1290,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // THEN we should listen for sfps when screen off, because require screen on is disabled assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); - // WHEN require screen on to auth is enabled, and keyguard is not awake - when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false); + // WHEN require interactive to auth is enabled, and keyguard is not awake + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true); // THEN we shouldn't listen for sfps when screen off, because require screen on is enabled assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); @@ -1297,6 +1305,62 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); } + @Test + public void notListeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthEnabled() + throws RemoteException { + // GIVEN SFPS supported and enrolled + final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); + props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + + // GIVEN Preconditions for sfps auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + statusBarShadeIsLocked(); + + // WHEN require interactive to auth is enabled & keyguard is going to sleep + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true); + deviceGoingToSleep(); + + mTestableLooper.processAllMessages(); + + // THEN we should NOT listen for sfps because device is going to sleep + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); + } + + @Test + public void listeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthDisabled() + throws RemoteException { + // GIVEN SFPS supported and enrolled + final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); + props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + + // GIVEN Preconditions for sfps auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + statusBarShadeIsLocked(); + + // WHEN require interactive to auth is disabled & keyguard is going to sleep + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false); + deviceGoingToSleep(); + + mTestableLooper.processAllMessages(); + + // THEN we should listen for sfps because screen on to auth is disabled + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); + } + private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal( @FingerprintSensorProperties.SensorType int sensorType) { return new FingerprintSensorPropertiesInternal( @@ -2188,6 +2252,54 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { eq(true)); } + @Test + public void testShouldListenForFace_withAuthSupportPostureConfig_returnsTrue() + throws RemoteException { + mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_CLOSED; + keyguardNotGoingAway(); + bouncerFullyVisibleAndNotGoingToSleep(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + supportsFaceDetection(); + + deviceInPostureStateOpened(); + mTestableLooper.processAllMessages(); + // Should not listen for face when posture state in DEVICE_POSTURE_OPENED + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); + + deviceInPostureStateClosed(); + mTestableLooper.processAllMessages(); + // Should listen for face when posture state in DEVICE_POSTURE_CLOSED + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + } + + @Test + public void testShouldListenForFace_withoutAuthSupportPostureConfig_returnsTrue() + throws RemoteException { + mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_UNKNOWN; + keyguardNotGoingAway(); + bouncerFullyVisibleAndNotGoingToSleep(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + supportsFaceDetection(); + + deviceInPostureStateClosed(); + mTestableLooper.processAllMessages(); + // Whether device in any posture state, always listen for face + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + + deviceInPostureStateOpened(); + mTestableLooper.processAllMessages(); + // Whether device in any posture state, always listen for face + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + } + private void userDeviceLockDown() { when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); when(mStrongAuthTracker.getStrongAuthForUser(mCurrentUserId)) @@ -2267,6 +2379,14 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .onAuthenticationAcquired(FINGERPRINT_ACQUIRED_START); } + private void deviceInPostureStateOpened() { + mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_OPENED); + } + + private void deviceInPostureStateClosed() { + mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_CLOSED); + } + private void successfulFingerprintAuth() { mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback .onAuthenticationSucceeded( @@ -2408,7 +2528,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mPowerManager, mTrustManager, mSubscriptionManager, mUserManager, mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager, mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager, - mFaceWakeUpTriggersConfig, Optional.of(mInteractiveToAuthProvider)); + mFaceWakeUpTriggersConfig, mDevicePostureController, + Optional.of(mInteractiveToAuthProvider)); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt new file mode 100644 index 000000000000..af46d9b97abf --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.udfps + +import android.graphics.Point +import android.graphics.Rect +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters +import org.mockito.Mockito.spy +import org.mockito.Mockito.`when` as whenEver + +@SmallTest +@RunWith(Parameterized::class) +class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { + val underTest = spy(EllipseOverlapDetector(neededPoints = 1)) + + @Before + fun setUp() { + // Use one single center point for testing, required or total number of points may change + whenEver(underTest.calculateSensorPoints(SENSOR)) + .thenReturn(listOf(Point(SENSOR.centerX(), SENSOR.centerY()))) + } + + @Test + fun isGoodOverlap() { + val touchData = + TOUCH_DATA.copy( + x = testCase.x.toFloat(), + y = testCase.y.toFloat(), + minor = testCase.minor, + major = testCase.major + ) + val actual = underTest.isGoodOverlap(touchData, SENSOR) + + assertThat(actual).isEqualTo(testCase.expected) + } + + data class TestCase( + val x: Int, + val y: Int, + val minor: Float, + val major: Float, + val expected: Boolean + ) + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun data(): List<TestCase> = + listOf( + genTestCases( + innerXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + innerYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()), + outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1), + outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + minor = 300f, + major = 300f, + expected = true + ), + genTestCases( + innerXs = listOf(SENSOR.left, SENSOR.right), + innerYs = listOf(SENSOR.top, SENSOR.bottom), + outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1), + outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + minor = 100f, + major = 100f, + expected = false + ) + ) + .flatten() + } +} + +/* Placeholder touch parameters. */ +private const val POINTER_ID = 42 +private const val NATIVE_MINOR = 2.71828f +private const val NATIVE_MAJOR = 3.14f +private const val ORIENTATION = 0f // used for perfect circles +private const val TIME = 12345699L +private const val GESTURE_START = 12345600L + +/* Template [NormalizedTouchData]. */ +private val TOUCH_DATA = + NormalizedTouchData( + POINTER_ID, + x = 0f, + y = 0f, + NATIVE_MINOR, + NATIVE_MAJOR, + ORIENTATION, + TIME, + GESTURE_START + ) + +private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 400 /* bottom */) + +private fun genTestCases( + innerXs: List<Int>, + innerYs: List<Int>, + outerXs: List<Int>, + outerYs: List<Int>, + minor: Float, + major: Float, + expected: Boolean +): List<EllipseOverlapDetectorTest.TestCase> { + return (innerXs + outerXs).flatMap { x -> + (innerYs + outerYs).map { y -> + EllipseOverlapDetectorTest.TestCase(x, y, minor, major, expected) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt index 95c53b408056..56043e306c16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt @@ -221,6 +221,14 @@ class SinglePointerTouchProcessorTest(val testCase: TestCase) : SysuiTestCase() private const val ROTATION_0_NATIVE_DISPLAY_WIDTH = 400 private const val ROTATION_0_NATIVE_DISPLAY_HEIGHT = 600 +/* Placeholder touch parameters. */ +private const val POINTER_ID = 42 +private const val NATIVE_MINOR = 2.71828f +private const val NATIVE_MAJOR = 3.14f +private const val ORIENTATION = 1.2345f +private const val TIME = 12345699L +private const val GESTURE_START = 12345600L + /* * ROTATION_0 map: * _ _ _ _ @@ -244,6 +252,7 @@ private val ROTATION_0_NATIVE_SENSOR_BOUNDS = private val ROTATION_0_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_0, + nativeOrientation = ORIENTATION, nativeXWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 250f, @@ -271,6 +280,7 @@ private val ROTATION_90_NATIVE_SENSOR_BOUNDS = private val ROTATION_90_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_90, + nativeOrientation = (ORIENTATION - Math.PI.toFloat() / 2), nativeXWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 150f, @@ -304,20 +314,13 @@ private val ROTATION_270_NATIVE_SENSOR_BOUNDS = private val ROTATION_270_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_270, + nativeOrientation = (ORIENTATION + Math.PI.toFloat() / 2), nativeXWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 450f, nativeYOutsideSensor = 250f, ) -/* Placeholder touch parameters. */ -private const val POINTER_ID = 42 -private const val NATIVE_MINOR = 2.71828f -private const val NATIVE_MAJOR = 3.14f -private const val ORIENTATION = 1.23f -private const val TIME = 12345699L -private const val GESTURE_START = 12345600L - /* Template [MotionEvent]. */ private val MOTION_EVENT = obtainMotionEvent( @@ -352,6 +355,7 @@ private val NORMALIZED_TOUCH_DATA = */ private data class OrientationBasedInputs( @Rotation val rotation: Int, + val nativeOrientation: Float, val nativeXWithinSensor: Float, val nativeYWithinSensor: Float, val nativeXOutsideSensor: Float, @@ -404,6 +408,7 @@ private fun genPositiveTestCases( y = nativeY * scaleFactor, minor = NATIVE_MINOR * scaleFactor, major = NATIVE_MAJOR * scaleFactor, + orientation = orientation.nativeOrientation ) val expectedTouchData = NORMALIZED_TOUCH_DATA.copy( diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java index 0fadc138637a..e4df754ec96a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java @@ -106,6 +106,7 @@ public class BrightLineClassifierTest extends SysuiTestCase { mClassifiers.add(mClassifierB); when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList); when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mFalsingDataProvider.isFolded()).thenReturn(true); mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, mMetricsLogger, mClassifiers, mSingleTapClassfier, mLongTapClassifier, mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController, @@ -121,6 +122,7 @@ public class BrightLineClassifierTest extends SysuiTestCase { mGestureFinalizedListener = gestureCompleteListenerCaptor.getValue(); mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true); mFakeFeatureFlags.set(Flags.MEDIA_FALSING_PENALTY, true); + mFakeFeatureFlags.set(Flags.FALSING_OFF_FOR_UNFOLDED, true); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java index 4281ee0f139f..ae38eb67c431 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java @@ -89,25 +89,27 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { mClassifiers.add(mClassifierA); when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList); when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mFalsingDataProvider.isFolded()).thenReturn(true); mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, mMetricsLogger, mClassifiers, mSingleTapClassifier, mLongTapClassifier, mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController, mAccessibilityManager, false, mFakeFeatureFlags); mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true); + mFakeFeatureFlags.set(Flags.FALSING_OFF_FOR_UNFOLDED, true); } @Test public void testA11yDisablesGesture() { - assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue(); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true); - assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse(); } @Test public void testA11yDisablesTap() { - assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); + assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue(); when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true); - assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse(); + assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); } @@ -179,4 +181,11 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { when(mFalsingDataProvider.isA11yAction()).thenReturn(true); assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); } + + @Test + public void testSkipUnfolded() { + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); + when(mFalsingDataProvider.isFolded()).thenReturn(false); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java index 5fa7214f07ff..94cf384267ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java @@ -16,6 +16,7 @@ package com.android.systemui.classifier; +import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.util.DisplayMetrics; import android.view.MotionEvent; @@ -38,6 +39,7 @@ public class ClassifierTest extends SysuiTestCase { private float mOffsetY = 0; @Mock private BatteryController mBatteryController; + private FoldStateListener mFoldStateListener = new FoldStateListener(mContext); private final DockManagerFake mDockManager = new DockManagerFake(); public void setup() { @@ -47,7 +49,8 @@ public class ClassifierTest extends SysuiTestCase { displayMetrics.ydpi = 100; displayMetrics.widthPixels = 1000; displayMetrics.heightPixels = 1000; - mDataProvider = new FalsingDataProvider(displayMetrics, mBatteryController, mDockManager); + mDataProvider = new FalsingDataProvider( + displayMetrics, mBatteryController, mFoldStateListener, mDockManager); } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java index d315c2da0703..c451a1e754c9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; import android.view.MotionEvent; @@ -50,6 +51,8 @@ public class FalsingDataProviderTest extends ClassifierTest { private FalsingDataProvider mDataProvider; @Mock private BatteryController mBatteryController; + @Mock + private FoldStateListener mFoldStateListener; private final DockManagerFake mDockManager = new DockManagerFake(); @Before @@ -61,7 +64,8 @@ public class FalsingDataProviderTest extends ClassifierTest { displayMetrics.ydpi = 100; displayMetrics.widthPixels = 1000; displayMetrics.heightPixels = 1000; - mDataProvider = new FalsingDataProvider(displayMetrics, mBatteryController, mDockManager); + mDataProvider = new FalsingDataProvider( + displayMetrics, mBatteryController, mFoldStateListener, mDockManager); } @After @@ -316,4 +320,16 @@ public class FalsingDataProviderTest extends ClassifierTest { mDataProvider.onA11yAction(); assertThat(mDataProvider.isA11yAction()).isTrue(); } + + @Test + public void test_FoldedState_Folded() { + when(mFoldStateListener.getFolded()).thenReturn(true); + assertThat(mDataProvider.isFolded()).isTrue(); + } + + @Test + public void test_FoldedState_Unfolded() { + when(mFoldStateListener.getFolded()).thenReturn(false); + assertThat(mDataProvider.isFolded()).isFalse(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 59e4655ce71e..7c20e3c9baff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -29,6 +29,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -485,6 +486,38 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { assertTrue(mViewMediator.isShowingAndNotOccluded()); } + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testDoKeyguardWhileInteractive_resets() { + mViewMediator.setShowingLocked(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + TestableLooper.get(this).processAllMessages(); + + when(mPowerManager.isInteractive()).thenReturn(true); + + mViewMediator.onSystemReady(); + TestableLooper.get(this).processAllMessages(); + + assertTrue(mViewMediator.isShowingAndNotOccluded()); + verify(mStatusBarKeyguardViewManager).reset(anyBoolean()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testDoKeyguardWhileNotInteractive_showsInsteadOfResetting() { + mViewMediator.setShowingLocked(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + TestableLooper.get(this).processAllMessages(); + + when(mPowerManager.isInteractive()).thenReturn(false); + + mViewMediator.onSystemReady(); + TestableLooper.get(this).processAllMessages(); + + assertTrue(mViewMediator.isShowingAndNotOccluded()); + verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean()); + } + private void createAndStartViewMediator() { mViewMediator = new KeyguardViewMediator( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java index f32d76bb601e..39a453da7f92 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java @@ -30,6 +30,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -51,7 +52,12 @@ public class WakefulnessLifecycleTest extends SysuiTestCase { public void setUp() throws Exception { mWallpaperManager = mock(IWallpaperManager.class); mWakefulness = - new WakefulnessLifecycle(mContext, mWallpaperManager, mock(DumpManager.class)); + new WakefulnessLifecycle( + mContext, + mWallpaperManager, + new FakeSystemClock(), + mock(DumpManager.class) + ); mWakefulnessObserver = mock(WakefulnessLifecycle.Observer.class); mWakefulness.addObserver(mWakefulnessObserver); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index be712f699b7b..f997d18a57a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -24,6 +24,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController import com.android.systemui.common.shared.model.Position +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback @@ -38,14 +39,17 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController +import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -68,6 +72,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var authController: AuthController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController + @Mock private lateinit var dozeParameters: DozeParameters private lateinit var underTest: KeyguardRepositoryImpl @@ -84,6 +89,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { keyguardStateController, keyguardUpdateMonitor, dozeTransitionListener, + dozeParameters, authController, dreamOverlayCallbackController, ) @@ -170,6 +176,26 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun isAodAvailable() = runTest { + val flow = underTest.isAodAvailable + var isAodAvailable = collectLastValue(flow) + runCurrent() + + val callback = + withArgCaptor<DozeParameters.Callback> { verify(dozeParameters).addCallback(capture()) } + + whenever(dozeParameters.getAlwaysOn()).thenReturn(false) + callback.onAlwaysOnChange() + assertThat(isAodAvailable()).isEqualTo(false) + + whenever(dozeParameters.getAlwaysOn()).thenReturn(true) + callback.onAlwaysOnChange() + assertThat(isAodAvailable()).isEqualTo(true) + + flow.onCompletion { verify(dozeParameters).removeCallback(callback) } + } + + @Test fun isKeyguardOccluded() = runTest(UnconfinedTestDispatcher()) { whenever(keyguardStateController.isOccluded).thenReturn(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 754adfdc48b3..b3cee2273012 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -71,6 +71,10 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor + private lateinit var fromDozingTransitionInteractor: FromDozingTransitionInteractor + private lateinit var fromOccludedTransitionInteractor: FromOccludedTransitionInteractor + private lateinit var fromGoneTransitionInteractor: FromGoneTransitionInteractor + private lateinit var fromAodTransitionInteractor: FromAodTransitionInteractor @Before fun setUp() { @@ -102,6 +106,42 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), ) fromDreamingTransitionInteractor.start() + + fromAodTransitionInteractor = + FromAodTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromAodTransitionInteractor.start() + + fromGoneTransitionInteractor = + FromGoneTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromGoneTransitionInteractor.start() + + fromDozingTransitionInteractor = + FromDozingTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromDozingTransitionInteractor.start() + + fromOccludedTransitionInteractor = + FromOccludedTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromOccludedTransitionInteractor.start() } @Test @@ -192,6 +232,289 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { coroutineContext.cancelChildren() } + @Test + fun `OCCLUDED to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `OCCLUDED to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `LOCKSCREEN to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to LOCKSCREEN + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `LOCKSCREEN to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to LOCKSCREEN + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `DOZING to LOCKSCREEN`() = + testScope.runTest { + // GIVEN a prior transition has run to DOZING + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DOZING, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to wake + keyguardRepository.setWakefulnessModel(startingToWake()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.DOZING) + assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `GONE to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to GONE + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `GONE to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to GONE + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + private fun startingToWake() = WakefulnessModel( WakefulnessState.STARTING_TO_WAKE, @@ -199,4 +522,12 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { WakeSleepReason.OTHER, WakeSleepReason.OTHER ) + + private fun startingToSleep() = + WakefulnessModel( + WakefulnessState.STARTING_TO_SLEEP, + true, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt new file mode 100644 index 000000000000..411b1bd04c52 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.table + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class TableLogBufferFactoryTest : SysuiTestCase() { + private val dumpManager: DumpManager = mock() + private val systemClock = FakeSystemClock() + private val underTest = TableLogBufferFactory(dumpManager, systemClock) + + @Test + fun `create - always creates new instance`() { + val b1 = underTest.create(NAME_1, SIZE) + val b1_copy = underTest.create(NAME_1, SIZE) + val b2 = underTest.create(NAME_2, SIZE) + val b2_copy = underTest.create(NAME_2, SIZE) + + assertThat(b1).isNotSameInstanceAs(b1_copy) + assertThat(b1).isNotSameInstanceAs(b2) + assertThat(b2).isNotSameInstanceAs(b2_copy) + } + + @Test + fun `getOrCreate - reuses instance`() { + val b1 = underTest.getOrCreate(NAME_1, SIZE) + val b1_copy = underTest.getOrCreate(NAME_1, SIZE) + val b2 = underTest.getOrCreate(NAME_2, SIZE) + val b2_copy = underTest.getOrCreate(NAME_2, SIZE) + + assertThat(b1).isSameInstanceAs(b1_copy) + assertThat(b2).isSameInstanceAs(b2_copy) + assertThat(b1).isNotSameInstanceAs(b2) + assertThat(b1_copy).isNotSameInstanceAs(b2_copy) + } + + companion object { + const val NAME_1 = "name 1" + const val NAME_2 = "name 2" + + const val SIZE = 8 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java index 4d2d0f05b76a..c0639f34484c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java @@ -79,7 +79,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { USER_ID, true, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null, MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, - InstanceId.fakeInstanceId(-1), -1); + InstanceId.fakeInstanceId(-1), -1, false); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index 52b694fac07c..c24c8c7f7cf6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -228,6 +228,7 @@ class MediaDataManagerTest : SysuiTestCase() { whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L) whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false) + whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) } @@ -300,6 +301,60 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testLoadMetadata_withExplicitIndicator() { + val metadata = + MediaMetadata.Builder().run { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + build() + } + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + whenever(controller.metadata).thenReturn(metadata) + + mediaDataManager.addListener(listener) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isTrue() + } + + @Test + fun testOnMetaDataLoaded_withoutExplicitIndicator() { + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + + mediaDataManager.addListener(listener) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isFalse() + } + + @Test fun testOnMetaDataLoaded_callsListener() { addNotificationAndLoad() verify(logger) @@ -603,6 +658,53 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testAddResumptionControls_withExplicitIndicator() { + val bundle = Bundle() + // WHEN resumption controls are added with explicit indicator + bundle.putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(bundle) + build() + } + val currentTime = clock.elapsedRealtime() + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + // THEN the media data indicates that it is for resumption + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.song).isEqualTo(SESSION_TITLE) + assertThat(data.app).isEqualTo(APP_NAME) + assertThat(data.actions).hasSize(1) + assertThat(data.semanticActions!!.playOrPause).isNotNull() + assertThat(data.lastActive).isAtLeast(currentTime) + assertThat(data.isExplicit).isTrue() + verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test fun testResumptionDisabled_dismissesResumeControls() { // WHEN there are resume controls and resumption is switched off val desc = diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt index 039dd4d92eb4..e4e95e580a7c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt @@ -20,6 +20,7 @@ import android.app.PendingIntent import android.content.res.Configuration import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.util.MathUtils.abs import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase @@ -31,14 +32,11 @@ import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.pipeline.MediaDataManager -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.PAGINATION_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.PageIndicator 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 @@ -56,6 +54,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.floatThat import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever @@ -86,6 +85,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var debugLogger: MediaCarouselControllerLogger @Mock lateinit var mediaViewController: MediaViewController @Mock lateinit var smartspaceMediaData: SmartspaceMediaData + @Mock lateinit var mediaCarousel: MediaScrollView + @Mock lateinit var pageIndicator: PageIndicator @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> @Captor lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener> @@ -647,25 +648,22 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Test fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() { val delta = 0.0001F - val paginationSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - val paginationSquishEnd = - TRANSFORM_BEZIER.getInterpolation( - (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION - ) + mediaCarouselController.mediaCarousel = mediaCarousel + mediaCarouselController.pageIndicator = pageIndicator + whenever(mediaCarousel.measuredHeight).thenReturn(100) + whenever(pageIndicator.translationY).thenReturn(80F) + whenever(pageIndicator.height).thenReturn(10) whenever(mediaHostStatesManager.mediaHostStates) .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState)) whenever(mediaHostState.visible).thenReturn(true) mediaCarouselController.currentEndLocation = LOCATION_QS - whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle) + whenever(mediaHostState.squishFraction).thenReturn(0.938F) mediaCarouselController.updatePageIndicatorAlpha() - assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta) + verify(pageIndicator).alpha = floatThat { abs(it - 0.5F) < delta } - whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd) + whenever(mediaHostState.squishFraction).thenReturn(1.0F) mediaCarouselController.updatePageIndicatorAlpha() - assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta) + verify(pageIndicator).alpha = floatThat { abs(it - 1.0F) < delta } } @Ignore("b/253229241") diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index b65f5cb51aaf..cfb19fc32bec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -54,6 +54,7 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.LiveData import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId +import com.android.internal.widget.CachingIconView import com.android.systemui.ActivityIntentHelper import com.android.systemui.R import com.android.systemui.SysuiTestCase @@ -154,6 +155,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var albumView: ImageView private lateinit var titleText: TextView private lateinit var artistText: TextView + private lateinit var explicitIndicator: CachingIconView private lateinit var seamless: ViewGroup private lateinit var seamlessButton: View @Mock private lateinit var seamlessBackground: RippleDrawable @@ -216,6 +218,7 @@ public class MediaControlPanelTest : SysuiTestCase() { this.set(Flags.UMO_SURFACE_RIPPLE, false) this.set(Flags.UMO_TURBULENCE_NOISE, false) this.set(Flags.MEDIA_FALSING_PENALTY, true) + this.set(Flags.MEDIA_EXPLICIT_INDICATOR, true) } @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -350,6 +353,7 @@ public class MediaControlPanelTest : SysuiTestCase() { appIcon = ImageView(context) titleText = TextView(context) artistText = TextView(context) + explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator } seamless = FrameLayout(context) seamless.foreground = seamlessBackground seamlessButton = View(context) @@ -396,6 +400,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(albumView.foreground).thenReturn(mock(Drawable::class.java)) whenever(viewHolder.titleText).thenReturn(titleText) whenever(viewHolder.artistText).thenReturn(artistText) + whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator) whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java)) whenever(viewHolder.seamless).thenReturn(seamless) whenever(viewHolder.seamlessButton).thenReturn(seamlessButton) @@ -1019,6 +1024,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindText() { + useRealConstraintSets() player.attachPlayer(viewHolder) player.bindPlayer(mediaData, PACKAGE) @@ -1036,6 +1042,8 @@ public class MediaControlPanelTest : SysuiTestCase() { handler.onAnimationEnd(mockAnimator) assertThat(titleText.getText()).isEqualTo(TITLE) assertThat(artistText.getText()).isEqualTo(ARTIST) + assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE) + assertThat(collapsedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE) // Rebinding should not trigger animation player.bindPlayer(mediaData, PACKAGE) @@ -1043,6 +1051,36 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + fun bindTextWithExplicitIndicator() { + useRealConstraintSets() + val mediaDataWitExp = mediaData.copy(isExplicit = true) + player.attachPlayer(viewHolder) + player.bindPlayer(mediaDataWitExp, PACKAGE) + + // Capture animation handler + val captor = argumentCaptor<Animator.AnimatorListener>() + verify(mockAnimator, times(2)).addListener(captor.capture()) + val handler = captor.value + + // Validate text views unchanged but animation started + assertThat(titleText.getText()).isEqualTo("") + assertThat(artistText.getText()).isEqualTo("") + verify(mockAnimator, times(1)).start() + + // Binding only after animator runs + handler.onAnimationEnd(mockAnimator) + assertThat(titleText.getText()).isEqualTo(TITLE) + assertThat(artistText.getText()).isEqualTo(ARTIST) + assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.VISIBLE) + assertThat(collapsedSet.getVisibility(explicitIndicator.id)) + .isEqualTo(ConstraintSet.VISIBLE) + + // Rebinding should not trigger animation + player.bindPlayer(mediaData, PACKAGE) + verify(mockAnimator, times(3)).start() + } + + @Test fun bindTextInterrupted() { val data0 = mediaData.copy(artist = "ARTIST_0") val data1 = mediaData.copy(artist = "ARTIST_1") diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 920801f95f5b..a5795184b493 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager @@ -76,6 +77,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle @Mock private lateinit var keyguardViewController: KeyguardViewController + @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController @Captor @@ -110,6 +112,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { keyguardStateController, bypassController, mediaCarouselController, + mediaDataManager, keyguardViewController, dreamOverlayStateController, configurationController, @@ -125,6 +128,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP) setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + whenever(mediaDataManager.hasActiveMedia()).thenReturn(true) whenever(mediaCarouselController.mediaCarouselScrollHandler) .thenReturn(mediaCarouselScrollHandler) val observer = wakefullnessObserver.value @@ -357,17 +361,31 @@ class MediaHierarchyManagerTest : SysuiTestCase() { } @Test - fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue() { + fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_active() { goToLockscreen() enterGuidedTransformation() whenever(lockHost.visible).thenReturn(false) whenever(qsHost.visible).thenReturn(true) whenever(qqsHost.visible).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse() } @Test + fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue_without_active() { + // To keep the appearing behavior, we need to be in a guided transition + goToLockscreen() + enterGuidedTransformation() + whenever(lockHost.visible).thenReturn(false) + whenever(qsHost.visible).thenReturn(true) + whenever(qqsHost.visible).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false) + + assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue() + } + + @Test fun testDream() { goToDream() setMediaDreamComplicationEnabled(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index 35b0eb678441..4ed6d7cf6bd0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -22,13 +22,6 @@ import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionViewState @@ -60,9 +53,10 @@ class MediaViewControllerTest : SysuiTestCase() { @Mock private lateinit var controlWidgetState: WidgetState @Mock private lateinit var bgWidgetState: WidgetState @Mock private lateinit var mediaTitleWidgetState: WidgetState + @Mock private lateinit var mediaSubTitleWidgetState: WidgetState @Mock private lateinit var mediaContainerWidgetState: WidgetState - val delta = 0.0001F + val delta = 0.1F private lateinit var mediaViewController: MediaViewController @@ -76,10 +70,11 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() { mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) - player.measureState = TransitionViewState().apply { - this.height = 100 - this.measureHeight = 100 - } + player.measureState = + TransitionViewState().apply { + this.height = 100 + this.measureHeight = 100 + } mediaHostStateHolder.expansion = 1f val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) @@ -128,29 +123,21 @@ class MediaViewControllerTest : SysuiTestCase() { R.id.header_artist to detailWidgetState ) ) - - val detailSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, detailSquishMiddle) + whenever(mockCopiedState.measureHeight).thenReturn(200) + // detail widgets occupy [90, 100] + whenever(detailWidgetState.y).thenReturn(90F) + whenever(detailWidgetState.height).thenReturn(10) + // control widgets occupy [150, 170] + whenever(controlWidgetState.y).thenReturn(150F) + whenever(controlWidgetState.height).thenReturn(20) + // in current beizer, when the progress reach 0.38, the result will be 0.5 + mediaViewController.squishViewState(mockViewState, 119F / 200F) verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val detailSquishEnd = - TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION) - mediaViewController.squishViewState(mockViewState, detailSquishEnd) + mediaViewController.squishViewState(mockViewState, 150F / 200F) verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } - - val controlSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, controlSquishMiddle) + mediaViewController.squishViewState(mockViewState, 181.4F / 200F) verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val controlSquishEnd = - TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION) - mediaViewController.squishViewState(mockViewState, controlSquishEnd) + mediaViewController.squishViewState(mockViewState, 200F / 200F) verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } } @@ -161,36 +148,33 @@ class MediaViewControllerTest : SysuiTestCase() { .thenReturn( mutableMapOf( R.id.media_title1 to mediaTitleWidgetState, + R.id.media_subtitle1 to mediaSubTitleWidgetState, R.id.media_cover1_container to mediaContainerWidgetState ) ) - - val containerSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, containerSquishMiddle) + whenever(mockCopiedState.measureHeight).thenReturn(360) + // media container widgets occupy [20, 300] + whenever(mediaContainerWidgetState.y).thenReturn(20F) + whenever(mediaContainerWidgetState.height).thenReturn(280) + // media title widgets occupy [320, 330] + whenever(mediaTitleWidgetState.y).thenReturn(320F) + whenever(mediaTitleWidgetState.height).thenReturn(10) + // media subtitle widgets occupy [340, 350] + whenever(mediaSubTitleWidgetState.y).thenReturn(340F) + whenever(mediaSubTitleWidgetState.height).thenReturn(10) + + // in current beizer, when the progress reach 0.38, the result will be 0.5 + mediaViewController.squishViewState(mockViewState, 307.6F / 360F) verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val containerSquishEnd = - TRANSFORM_BEZIER.getInterpolation( - (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, containerSquishEnd) + mediaViewController.squishViewState(mockViewState, 320F / 360F) verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } - - val titleSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, titleSquishMiddle) + // media title and media subtitle are in same widget group, should be calculate together and + // have same alpha + mediaViewController.squishViewState(mockViewState, 353.8F / 360F) verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val titleSquishEnd = - TRANSFORM_BEZIER.getInterpolation( - (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, titleSquishEnd) + verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + mediaViewController.squishViewState(mockViewState, 360F / 360F) verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index b16a39f37e39..f5432e22c57e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -102,8 +102,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class); private MediaDevice mMediaDevice1 = mock(MediaDevice.class); private MediaDevice mMediaDevice2 = mock(MediaDevice.class); - private MediaItem mMediaItem1 = mock(MediaItem.class); - private MediaItem mMediaItem2 = mock(MediaItem.class); private NearbyDevice mNearbyDevice1 = mock(NearbyDevice.class); private NearbyDevice mNearbyDevice2 = mock(NearbyDevice.class); private MediaMetadata mMediaMetadata = mock(MediaMetadata.class); @@ -127,7 +125,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { private LocalMediaManager mLocalMediaManager; private List<MediaController> mMediaControllers = new ArrayList<>(); private List<MediaDevice> mMediaDevices = new ArrayList<>(); - private List<MediaItem> mMediaItemList = new ArrayList<>(); private List<NearbyDevice> mNearbyDevices = new ArrayList<>(); private MediaDescription mMediaDescription; private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>(); @@ -149,7 +146,9 @@ public class MediaOutputControllerTest extends SysuiTestCase { Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, mKeyguardManager, mFlags); when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(false); + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(false); mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; MediaDescription.Builder builder = new MediaDescription.Builder(); builder.setTitle(TEST_SONG); @@ -160,16 +159,12 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID); mMediaDevices.add(mMediaDevice1); mMediaDevices.add(mMediaDevice2); - when(mMediaItem1.getMediaDevice()).thenReturn(Optional.of(mMediaDevice1)); - when(mMediaItem2.getMediaDevice()).thenReturn(Optional.of(mMediaDevice2)); - mMediaItemList.add(mMediaItem1); - mMediaItemList.add(mMediaItem2); when(mNearbyDevice1.getMediaRoute2Id()).thenReturn(TEST_DEVICE_1_ID); - when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE); + when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR); when(mNearbyDevice2.getMediaRoute2Id()).thenReturn(TEST_DEVICE_2_ID); - when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR); + when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE); mNearbyDevices.add(mNearbyDevice1); mNearbyDevices.add(mNearbyDevice2); } @@ -274,8 +269,20 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaOutputController.onDevicesUpdated(mNearbyDevices); mMediaOutputController.onDeviceListUpdate(mMediaDevices); - verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_CLOSE); - verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_FAR); + verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_FAR); + verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_CLOSE); + } + + @Test + public void onDeviceListUpdate_withNearbyDevices_rankByRangeInformation() + throws RemoteException { + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onDevicesUpdated(mNearbyDevices); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + + assertThat(mMediaDevices.get(0).getId()).isEqualTo(TEST_DEVICE_1_ID); } @Test @@ -292,6 +299,22 @@ public class MediaOutputControllerTest extends SysuiTestCase { } @Test + public void routeProcessSupport_onDeviceListUpdate_preferenceExist_NotUpdatesRangeInformation() + throws RemoteException { + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(true); + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onDevicesUpdated(mNearbyDevices); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + + verify(mMediaDevice1, never()).setRangeZone(anyInt()); + verify(mMediaDevice2, never()).setRangeZone(anyInt()); + } + + @Test public void advanced_onDeviceListUpdate_verifyDeviceListCallback() { when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); mMediaOutputController.start(mCb); @@ -307,6 +330,35 @@ public class MediaOutputControllerTest extends SysuiTestCase { assertThat(devices.containsAll(mMediaDevices)).isTrue(); assertThat(devices.size()).isEqualTo(mMediaDevices.size()); + assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo( + mMediaDevices.size() + 2); + verify(mCb).onDeviceListChanged(); + } + + @Test + public void advanced_categorizeMediaItems_withSuggestedDevice_verifyDeviceListSize() { + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); + when(mMediaDevice1.isSuggestedDevice()).thenReturn(true); + when(mMediaDevice2.isSuggestedDevice()).thenReturn(false); + + mMediaOutputController.start(mCb); + reset(mCb); + mMediaOutputController.getMediaItemList().clear(); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + final List<MediaDevice> devices = new ArrayList<>(); + int dividerSize = 0; + for (MediaItem item : mMediaOutputController.getMediaItemList()) { + if (item.getMediaDevice().isPresent()) { + devices.add(item.getMediaDevice().get()); + } + if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) { + dividerSize++; + } + } + + assertThat(devices.containsAll(mMediaDevices)).isTrue(); + assertThat(devices.size()).isEqualTo(mMediaDevices.size()); + assertThat(dividerSize).isEqualTo(2); verify(mCb).onDeviceListChanged(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index 4cc12c709fa7..f5b3959b322d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -206,6 +206,21 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { } @Test + fun commandQueueCallback_almostCloseToStartCast_deviceNameBlank_showsDefaultDeviceName() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, + routeInfoWithBlankDeviceName, + null, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .contains(context.getString(R.string.media_ttt_default_device_type)) + assertThat(chipbarView.getChipText()) + .isNotEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) + } + + @Test fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, @@ -248,6 +263,21 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { } @Test + fun commandQueueCallback_transferToReceiverTriggered_deviceNameBlank_showsDefaultDeviceName() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, + routeInfoWithBlankDeviceName, + null, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .contains(context.getString(R.string.media_ttt_default_device_type)) + assertThat(chipbarView.getChipText()) + .isNotEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + } + + @Test fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, @@ -934,6 +964,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { private const val APP_NAME = "Fake app name" private const val OTHER_DEVICE_NAME = "My Tablet" +private const val BLANK_DEVICE_NAME = " " private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 @@ -942,3 +973,9 @@ private val routeInfo = .addFeature("feature") .setClientPackageName(PACKAGE_NAME) .build() + +private val routeInfoWithBlankDeviceName = + MediaRoute2Info.Builder("id", BLANK_DEVICE_NAME) + .addFeature("feature") + .setClientPackageName(PACKAGE_NAME) + .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 4a9c7508b1b3..fc90c1add5e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -93,7 +93,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -102,7 +102,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) - verify(bubbles).showAppBubble(notesIntent) + verify(bubbles).showOrHideAppBubble(notesIntent) verify(context, never()).startActivity(notesIntent) } @@ -113,7 +113,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = true) verify(context).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -123,7 +123,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -133,7 +133,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -143,7 +143,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -153,7 +153,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -161,7 +161,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController(isEnabled = false).showNoteTask() verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -171,7 +171,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } // endregion diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt index ca3182affcc1..3281fa9bd8a4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt @@ -28,7 +28,6 @@ import com.android.systemui.qs.tiles.BatterySaverTile import com.android.systemui.qs.tiles.BluetoothTile import com.android.systemui.qs.tiles.CameraToggleTile import com.android.systemui.qs.tiles.CastTile -import com.android.systemui.qs.tiles.CellularTile import com.android.systemui.qs.tiles.ColorCorrectionTile import com.android.systemui.qs.tiles.ColorInversionTile import com.android.systemui.qs.tiles.DataSaverTile @@ -49,7 +48,6 @@ import com.android.systemui.qs.tiles.ReduceBrightColorsTile import com.android.systemui.qs.tiles.RotationLockTile import com.android.systemui.qs.tiles.ScreenRecordTile import com.android.systemui.qs.tiles.UiModeNightTile -import com.android.systemui.qs.tiles.WifiTile import com.android.systemui.qs.tiles.WorkModeTile import com.android.systemui.util.leak.GarbageMonitor import com.google.common.truth.Truth.assertThat @@ -63,10 +61,8 @@ import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever private val specMap = mapOf( - "wifi" to WifiTile::class.java, "internet" to InternetTile::class.java, "bt" to BluetoothTile::class.java, - "cell" to CellularTile::class.java, "dnd" to DndTile::class.java, "inversion" to ColorInversionTile::class.java, "airplane" to AirplaneModeTile::class.java, @@ -102,10 +98,8 @@ class QSFactoryImplTest : SysuiTestCase() { @Mock(answer = Answers.RETURNS_SELF) private lateinit var customTileBuilder: CustomTile.Builder @Mock private lateinit var customTile: CustomTile - @Mock private lateinit var wifiTile: WifiTile @Mock private lateinit var internetTile: InternetTile @Mock private lateinit var bluetoothTile: BluetoothTile - @Mock private lateinit var cellularTile: CellularTile @Mock private lateinit var dndTile: DndTile @Mock private lateinit var colorInversionTile: ColorInversionTile @Mock private lateinit var airplaneTile: AirplaneModeTile @@ -146,10 +140,8 @@ class QSFactoryImplTest : SysuiTestCase() { factory = QSFactoryImpl( { qsHost }, { customTileBuilder }, - { wifiTile }, { internetTile }, { bluetoothTile }, - { cellularTile }, { dndTile }, { colorInversionTile }, { airplaneTile }, diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt new file mode 100644 index 000000000000..3710281499b3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt @@ -0,0 +1,100 @@ +package com.android.systemui.settings + +import android.content.Context +import android.content.Intent +import android.content.pm.UserInfo +import android.os.Handler +import android.os.UserHandle +import android.os.UserManager +import androidx.concurrent.futures.DirectExecutor +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(Parameterized::class) +class UserTrackerImplReceiveTest : SysuiTestCase() { + + companion object { + + @JvmStatic + @Parameterized.Parameters + fun data(): Iterable<String> = + listOf( + Intent.ACTION_USER_INFO_CHANGED, + Intent.ACTION_MANAGED_PROFILE_AVAILABLE, + Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE, + Intent.ACTION_MANAGED_PROFILE_ADDED, + Intent.ACTION_MANAGED_PROFILE_REMOVED, + Intent.ACTION_MANAGED_PROFILE_UNLOCKED + ) + } + + private val executor: Executor = DirectExecutor.INSTANCE + + @Mock private lateinit var context: Context + @Mock private lateinit var userManager: UserManager + @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager + @Mock(stubOnly = true) private lateinit var handler: Handler + + @Parameterized.Parameter lateinit var intentAction: String + @Mock private lateinit var callback: UserTracker.Callback + @Captor private lateinit var captor: ArgumentCaptor<List<UserInfo>> + + private lateinit var tracker: UserTrackerImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + `when`(context.user).thenReturn(UserHandle.SYSTEM) + `when`(context.createContextAsUser(ArgumentMatchers.any(), anyInt())).thenReturn(context) + + tracker = UserTrackerImpl(context, userManager, dumpManager, handler) + } + + @Test + fun `calls callback and updates profiles when an intent received`() { + tracker.initialize(0) + tracker.addCallback(callback, executor) + val profileID = tracker.userId + 10 + + `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = + UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) + } + + tracker.onReceive(context, Intent(intentAction)) + + verify(callback, times(0)).onUserChanged(anyInt(), any()) + verify(callback, times(1)).onProfilesChanged(capture(captor)) + assertThat(captor.value.map { it.id }).containsExactly(0, profileID) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt index 52462c7186d4..e65bbb1bea08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt @@ -124,6 +124,16 @@ class UserTrackerImplTest : SysuiTestCase() { verify(context).registerReceiverForAllUsers( eq(tracker), capture(captor), isNull(), eq(handler)) + with(captor.value) { + assertThat(countActions()).isEqualTo(7) + assertThat(hasAction(Intent.ACTION_USER_SWITCHED)).isTrue() + assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue() + } } @Test @@ -280,37 +290,6 @@ class UserTrackerImplTest : SysuiTestCase() { } @Test - fun testCallbackCalledOnProfileChanged() { - tracker.initialize(0) - val callback = TestCallback() - tracker.addCallback(callback, executor) - val profileID = tracker.userId + 10 - - `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) - } - - val intent = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - - tracker.onReceive(context, intent) - - assertThat(callback.calledOnUserChanged).isEqualTo(0) - assertThat(callback.calledOnProfilesChanged).isEqualTo(1) - assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID) - } - - @Test fun testCallbackCalledOnUserInfoChanged() { tracker.initialize(0) val callback = TestCallback() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt index 88651c1292c3..f802a5e09228 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade import android.testing.AndroidTestingRunner +import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START @@ -92,12 +93,12 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f) assertThat(getConstraint(R.id.date).layout.startToStart).isEqualTo(PARENT_ID) - assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0f) + assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0.5f) assertThat(getConstraint(R.id.batteryRemainingIcon).layout.endToEnd) .isEqualTo(PARENT_ID) assertThat(getConstraint(R.id.batteryRemainingIcon).layout.horizontalBias) - .isEqualTo(1f) + .isEqualTo(0.5f) assertThat(getConstraint(R.id.privacy_container).layout.endToEnd) .isEqualTo(R.id.end_guide) @@ -331,10 +332,8 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { val views = mapOf( R.id.clock to "clock", R.id.date to "date", - R.id.statusIcons to "icons", R.id.privacy_container to "privacy", R.id.carrier_group to "carriers", - R.id.batteryRemainingIcon to "battery", ) views.forEach { (id, name) -> assertWithMessage("$name has 0 height in qqs") @@ -352,11 +351,8 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { fun testCheckViewsDontChangeSizeBetweenAnimationConstraints() { val views = mapOf( R.id.clock to "clock", - R.id.date to "date", - R.id.statusIcons to "icons", R.id.privacy_container to "privacy", R.id.carrier_group to "carriers", - R.id.batteryRemainingIcon to "battery", ) views.forEach { (id, name) -> expect.withMessage("$name changes height") @@ -369,8 +365,8 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { } private fun Int.fromConstraint() = when (this) { - -1 -> "MATCH_PARENT" - -2 -> "WRAP_CONTENT" + ViewGroup.LayoutParams.MATCH_PARENT -> "MATCH_PARENT" + ViewGroup.LayoutParams.WRAP_CONTENT -> "WRAP_CONTENT" else -> toString() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt index 1d30ad9293a0..f580f5e00f67 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt @@ -182,6 +182,7 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() { null } whenever(view.visibility).thenAnswer { _ -> viewVisibility } + whenever(view.alpha).thenReturn(1f) whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt index b4c8f981b760..b568122d3fed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.shade +import android.animation.ValueAnimator import android.app.StatusBarManager import android.content.Context import android.testing.AndroidTestingRunner @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.policy.VariableDateViewController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before @@ -37,6 +39,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Answers +import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.mock @@ -75,6 +78,7 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { @JvmField @Rule val mockitoRule = MockitoJUnit.rule() var viewVisibility = View.GONE + var viewAlpha = 1f private lateinit var mLargeScreenShadeHeaderController: LargeScreenShadeHeaderController private lateinit var carrierIconSlots: List<String> @@ -101,6 +105,13 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { null } whenever(view.visibility).thenAnswer { _ -> viewVisibility } + + whenever(view.setAlpha(anyFloat())).then { + viewAlpha = it.arguments[0] as Float + null + } + whenever(view.alpha).thenAnswer { _ -> viewAlpha } + whenever(variableDateViewControllerFactory.create(any())) .thenReturn(variableDateViewController) whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager) @@ -155,6 +166,16 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { } @Test + fun alphaChangesUpdateVisibility() { + makeShadeVisible() + mLargeScreenShadeHeaderController.shadeExpandedFraction = 0f + assertThat(viewVisibility).isEqualTo(View.INVISIBLE) + + mLargeScreenShadeHeaderController.shadeExpandedFraction = 1f + assertThat(viewVisibility).isEqualTo(View.VISIBLE) + } + + @Test fun singleCarrier_enablesCarrierIconsInStatusIcons() { whenever(qsCarrierGroupController.isSingleCarrier).thenReturn(true) @@ -239,6 +260,39 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { } @Test + fun testShadeExpanded_true_alpha_zero_invisible() { + view.alpha = 0f + mLargeScreenShadeHeaderController.largeScreenActive = true + mLargeScreenShadeHeaderController.qsVisible = true + + assertThat(viewVisibility).isEqualTo(View.INVISIBLE) + } + + @Test + fun animatorCallsUpdateVisibilityOnUpdate() { + val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF) + whenever(view.animate()).thenReturn(animator) + + mLargeScreenShadeHeaderController.startCustomizingAnimation(show = false, 0L) + + val updateCaptor = argumentCaptor<ValueAnimator.AnimatorUpdateListener>() + verify(animator).setUpdateListener(capture(updateCaptor)) + + mLargeScreenShadeHeaderController.largeScreenActive = true + mLargeScreenShadeHeaderController.qsVisible = true + + view.alpha = 1f + updateCaptor.value.onAnimationUpdate(mock()) + + assertThat(viewVisibility).isEqualTo(View.VISIBLE) + + view.alpha = 0f + updateCaptor.value.onAnimationUpdate(mock()) + + assertThat(viewVisibility).isEqualTo(View.INVISIBLE) + } + + @Test fun demoMode_attachDemoMode() { val cb = argumentCaptor<DemoMode>() verify(demoModeController).addCallback(capture(cb)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index ca99e24fc105..e41929f7d578 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; import org.junit.Assert; import org.junit.Before; @@ -216,4 +217,29 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { Assert.assertEquals(1f, mChildrenContainer.getBottomRoundness(), 0.001f); Assert.assertEquals(1f, notificationRow.getBottomRoundness(), 0.001f); } + + @Test + public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_header() { + mChildrenContainer.useRoundnessSourceTypes(true); + + NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper(); + Assert.assertEquals(0f, header.getTopRoundness(), 0.001f); + + mChildrenContainer.requestTopRoundness(1f, SourceType.from(""), false); + + Assert.assertEquals(1f, header.getTopRoundness(), 0.001f); + } + + @Test + public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_headerLowPriority() { + mChildrenContainer.useRoundnessSourceTypes(true); + mChildrenContainer.setIsLowPriority(true); + + NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper(); + Assert.assertEquals(0f, header.getTopRoundness(), 0.001f); + + mChildrenContainer.requestTopRoundness(1f, SourceType.from(""), false); + + Assert.assertEquals(1f, header.getTopRoundness(), 0.001f); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java index 4ccbc6d45e63..091bb5455d93 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doReturn; @@ -74,6 +75,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import org.mockito.stubbing.Answer; import java.util.Collections; import java.util.List; @@ -115,8 +117,10 @@ public class AutoTileManagerTest extends SysuiTestCase { @Spy private PackageManager mPackageManager; private final boolean mIsReduceBrightColorsAvailable = true; - private AutoTileManager mAutoTileManager; + private AutoTileManager mAutoTileManager; // under test + private SecureSettings mSecureSettings; + private ManagedProfileController.Callback mManagedProfileCallback; @Before public void setUp() throws Exception { @@ -303,7 +307,7 @@ public class AutoTileManagerTest extends SysuiTestCase { InOrder inOrderManagedProfile = inOrder(mManagedProfileController); inOrderManagedProfile.verify(mManagedProfileController).removeCallback(any()); - inOrderManagedProfile.verify(mManagedProfileController, never()).addCallback(any()); + inOrderManagedProfile.verify(mManagedProfileController).addCallback(any()); if (ColorDisplayManager.isNightDisplayAvailable(mContext)) { InOrder inOrderNightDisplay = inOrder(mNightDisplayListener); @@ -504,6 +508,40 @@ public class AutoTileManagerTest extends SysuiTestCase { } @Test + public void managedProfileAdded_tileAdded() { + when(mAutoAddTracker.isAdded(eq("work"))).thenReturn(false); + mAutoTileManager = createAutoTileManager(mContext); + Mockito.doAnswer((Answer<Object>) invocation -> { + mManagedProfileCallback = invocation.getArgument(0); + return null; + }).when(mManagedProfileController).addCallback(any()); + mAutoTileManager.init(); + when(mManagedProfileController.hasActiveProfile()).thenReturn(true); + + mManagedProfileCallback.onManagedProfileChanged(); + + verify(mQsTileHost, times(1)).addTile(eq("work")); + verify(mAutoAddTracker, times(1)).setTileAdded(eq("work")); + } + + @Test + public void managedProfileRemoved_tileRemoved() { + when(mAutoAddTracker.isAdded(eq("work"))).thenReturn(true); + mAutoTileManager = createAutoTileManager(mContext); + Mockito.doAnswer((Answer<Object>) invocation -> { + mManagedProfileCallback = invocation.getArgument(0); + return null; + }).when(mManagedProfileController).addCallback(any()); + mAutoTileManager.init(); + when(mManagedProfileController.hasActiveProfile()).thenReturn(false); + + mManagedProfileCallback.onManagedProfileChanged(); + + verify(mQsTileHost, times(1)).removeTile(eq("work")); + verify(mAutoAddTracker, times(1)).setTileRemoved(eq("work")); + } + + @Test public void testEmptyArray_doesNotCrash() { mContext.getOrCreateTestableResources().addOverride( R.array.config_quickSettingsAutoAdd, new String[0]); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 9695000fec57..ec294b12a11a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -54,6 +56,7 @@ import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -115,6 +118,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { private VibratorHelper mVibratorHelper; @Mock private BiometricUnlockLogger mLogger; + private final FakeSystemClock mSystemClock = new FakeSystemClock(); private BiometricUnlockController mBiometricUnlockController; @Before @@ -137,7 +141,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mMetricsLogger, mDumpManager, mPowerManager, mLogger, mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle, mAuthController, mStatusBarStateController, - mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper); + mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper, + mSystemClock + ); mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); mBiometricUnlockController.addBiometricModeListener(mBiometricModeListener); when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker); @@ -200,7 +206,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { verify(mKeyguardViewMediator).onWakeAndUnlocking(); assertThat(mBiometricUnlockController.getMode()) - .isEqualTo(BiometricUnlockController.MODE_WAKE_AND_UNLOCK); + .isEqualTo(MODE_WAKE_AND_UNLOCK); } @Test @@ -437,4 +443,83 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { // THEN wakeup the device verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString()); } + + @Test + public void onSideFingerprintSuccess_recentPowerButtonPress_noHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + + // GIVEN last wake time just occurred + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN DO NOT vibrate the device + verify(mVibratorHelper, never()).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + + // GIVEN last wake time was 500ms ago + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + mSystemClock.advanceTime(500); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN vibrate the device + verify(mVibratorHelper).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() { + // GIVEN side fingerprint enrolled, wakeup just happened + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // GIVEN last wake reason was from a gesture + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_GESTURE); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN vibrate the device + verify(mVibratorHelper).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintFail_alwaysPlaysHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was recent power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // WHEN biometric fingerprint fails + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + + // THEN always vibrate the device + verify(mVibratorHelper).vibrateAuthError(anyString()); + } + + private void givenFingerprintModeUnlockCollapsing() { + when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 3a1f9b748e49..c8157ccc8a9a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -178,8 +178,6 @@ import com.android.systemui.volume.VolumeComponent; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.startingsurface.StartingSurface; -import dagger.Lazy; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -192,6 +190,8 @@ import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.util.Optional; +import dagger.Lazy; + @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -380,7 +380,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any()); mWakefulnessLifecycle = - new WakefulnessLifecycle(mContext, mIWallpaperManager, mDumpManager); + new WakefulnessLifecycle(mContext, mIWallpaperManager, mFakeSystemClock, + mDumpManager); mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN); mWakefulnessLifecycle.dispatchFinishedWakingUp(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java index 077b41a0aa90..c8438501b3e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java @@ -23,6 +23,10 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.res.Resources; @@ -39,10 +43,9 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; import com.android.systemui.unfold.FoldAodAnimationController; @@ -52,6 +55,8 @@ import org.junit.Assert; 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; @@ -69,7 +74,6 @@ public class DozeParametersTest extends SysuiTestCase { @Mock private PowerManager mPowerManager; @Mock private TunerService mTunerService; @Mock private BatteryController mBatteryController; - @Mock private FeatureFlags mFeatureFlags; @Mock private DumpManager mDumpManager; @Mock private ScreenOffAnimationController mScreenOffAnimationController; @Mock private FoldAodAnimationController mFoldAodAnimationController; @@ -78,6 +82,7 @@ public class DozeParametersTest extends SysuiTestCase { @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private StatusBarStateController mStatusBarStateController; @Mock private ConfigurationController mConfigurationController; + @Captor private ArgumentCaptor<BatteryStateChangeCallback> mBatteryStateChangeCallback; /** * The current value of PowerManager's dozeAfterScreenOff property. @@ -113,7 +118,6 @@ public class DozeParametersTest extends SysuiTestCase { mBatteryController, mTunerService, mDumpManager, - mFeatureFlags, mScreenOffAnimationController, Optional.of(mSysUIUnfoldComponent), mUnlockedScreenOffAnimationController, @@ -122,7 +126,8 @@ public class DozeParametersTest extends SysuiTestCase { mStatusBarStateController ); - when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(true); + verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture()); + setAodEnabledForTest(true); setShouldControlUnlockedScreenOffForTest(true); setDisplayNeedsBlankingForTest(false); @@ -173,6 +178,29 @@ public class DozeParametersTest extends SysuiTestCase { assertThat(mDozeParameters.getAlwaysOn()).isFalse(); } + @Test + public void testGetAlwaysOn_whenBatterySaverCallback() { + DozeParameters.Callback callback = mock(DozeParameters.Callback.class); + mDozeParameters.addCallback(callback); + + when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mBatteryController.isAodPowerSave()).thenReturn(true); + + // Both lines should trigger an event + mDozeParameters.onTuningChanged(Settings.Secure.DOZE_ALWAYS_ON, "1"); + mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); + + verify(callback, times(2)).onAlwaysOnChange(); + assertThat(mDozeParameters.getAlwaysOn()).isFalse(); + + reset(callback); + when(mBatteryController.isAodPowerSave()).thenReturn(false); + mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); + + verify(callback).onAlwaysOnChange(); + assertThat(mDozeParameters.getAlwaysOn()).isTrue(); + } + /** * PowerManager.setDozeAfterScreenOff(true) means we are not controlling screen off, and calling * it with false means we are. Confusing, but sure - make sure that we call PowerManager with @@ -196,17 +224,6 @@ public class DozeParametersTest extends SysuiTestCase { } @Test - public void testControlUnlockedScreenOffAnimationDisabled_dozeAfterScreenOff() { - when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(false); - - assertFalse(mDozeParameters.shouldControlUnlockedScreenOff()); - - // Trigger the setter for the current value. - mDozeParameters.setControlScreenOffAnimation(mDozeParameters.shouldControlScreenOff()); - assertFalse(mDozeParameters.shouldControlScreenOff()); - } - - @Test public void propagatesAnimateScreenOff_noAlwaysOn() { setAodEnabledForTest(false); setDisplayNeedsBlankingForTest(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 0da15e239932..09589707b331 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -37,6 +37,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger @@ -46,6 +47,7 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -94,7 +96,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } } - whenever(logBufferFactory.create(anyString(), anyInt())).thenAnswer { _ -> + whenever(logBufferFactory.getOrCreate(anyString(), anyInt())).thenAnswer { _ -> mock<TableLogBuffer>() } @@ -292,13 +294,13 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // Get repos to trigger creation underTest.getRepoForSubId(SUB_1_ID) verify(logBufferFactory) - .create( + .getOrCreate( eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)), anyInt(), ) underTest.getRepoForSubId(SUB_2_ID) verify(logBufferFactory) - .create( + .getOrCreate( eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)), anyInt(), ) @@ -307,6 +309,46 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `connection repository factory - reuses log buffers for same connection`() = + runBlocking(IMMEDIATE) { + val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock()) + + connectionFactory = + MobileConnectionRepositoryImpl.Factory( + fakeBroadcastDispatcher, + context = context, + telephonyManager = telephonyManager, + bgDispatcher = IMMEDIATE, + globalSettings = globalSettings, + logger = logger, + mobileMappingsProxy = mobileMappings, + scope = scope, + logFactory = realLoggerFactory, + ) + + // Create two connections for the same subId. Similar to if the connection appeared + // and disappeared from the connectionFactory's perspective + val connection1 = + connectionFactory.build( + 1, + NetworkNameModel.Default("default_name"), + "-", + underTest.globalMobileDataSettingChangedEvent, + ) + + val connection1_repeat = + connectionFactory.build( + 1, + NetworkNameModel.Default("default_name"), + "-", + underTest.globalMobileDataSettingChangedEvent, + ) + + assertThat(connection1.tableLogBuffer) + .isSameInstanceAs(connection1_repeat.tableLogBuffer) + } + + @Test fun mobileConnectivity_default() { assertThat(underTest.defaultMobileNetworkConnectivity.value) .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 4b32ee262cdc..0cca7b2aa38c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -390,19 +390,27 @@ public class RemoteInputViewTest extends SysuiTestCase { bindController(view, row.getEntry()); view.setVisibility(View.GONE); - View crossFadeView = new View(mContext); + View fadeOutView = new View(mContext); + fadeOutView.setId(com.android.internal.R.id.actions_container_layout); - // Start focus animation - view.focusAnimated(crossFadeView); + FrameLayout parent = new FrameLayout(mContext); + parent.addView(view); + parent.addView(fadeOutView); + // Start focus animation + view.focusAnimated(); assertTrue(view.isAnimatingAppearance()); + // fast forward to 1 ms before end of animation and verify fadeOutView has alpha set to 0f + mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD - 1); + assertEquals(0f, fadeOutView.getAlpha()); + // fast forward to end of animation - mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD); + mAnimatorTestRule.advanceTimeBy(1); - // assert that crossFadeView's alpha is reset to 1f after the animation (hidden behind + // assert that fadeOutView's alpha is reset to 1f after the animation (hidden behind // RemoteInputView) - assertEquals(1f, crossFadeView.getAlpha()); + assertEquals(1f, fadeOutView.getAlpha()); assertFalse(view.isAnimatingAppearance()); assertEquals(View.VISIBLE, view.getVisibility()); assertEquals(1f, view.getAlpha()); @@ -415,20 +423,27 @@ public class RemoteInputViewTest extends SysuiTestCase { mDependency, TestableLooper.get(this)); ExpandableNotificationRow row = helper.createRow(); - FrameLayout remoteInputViewParent = new FrameLayout(mContext); RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); - remoteInputViewParent.addView(view); bindController(view, row.getEntry()); + View fadeInView = new View(mContext); + fadeInView.setId(com.android.internal.R.id.actions_container_layout); + + FrameLayout parent = new FrameLayout(mContext); + parent.addView(view); + parent.addView(fadeInView); + // Start defocus animation - view.onDefocus(true, false); + view.onDefocus(true /* animate */, false /* logClose */, null /* doAfterDefocus */); assertEquals(View.VISIBLE, view.getVisibility()); + assertEquals(0f, fadeInView.getAlpha()); // fast forward to end of animation mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD); // assert that RemoteInputView is no longer visible assertEquals(View.GONE, view.getVisibility()); + assertEquals(1f, fadeInView.getAlpha()); } // NOTE: because we're refactoring the RemoteInputView and moving logic into the diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt deleted file mode 100644 index 8dd088f5760c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt +++ /dev/null @@ -1,289 +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.stylus - -import android.content.Context -import android.hardware.BatteryState -import android.hardware.input.InputManager -import android.os.Handler -import android.testing.AndroidTestingRunner -import android.view.InputDevice -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.time.FakeSystemClock -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@SmallTest -@Ignore("TODO(b/20579491): unignore on main") -class StylusFirstUsageListenerTest : SysuiTestCase() { - @Mock lateinit var context: Context - @Mock lateinit var inputManager: InputManager - @Mock lateinit var stylusManager: StylusManager - @Mock lateinit var featureFlags: FeatureFlags - @Mock lateinit var internalStylusDevice: InputDevice - @Mock lateinit var otherDevice: InputDevice - @Mock lateinit var externalStylusDevice: InputDevice - @Mock lateinit var batteryState: BatteryState - @Mock lateinit var handler: Handler - - private lateinit var stylusListener: StylusFirstUsageListener - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true) - whenever(inputManager.isStylusEverUsed(context)).thenReturn(false) - - stylusListener = - StylusFirstUsageListener( - context, - inputManager, - stylusManager, - featureFlags, - EXECUTOR, - handler - ) - stylusListener.hasStarted = false - - whenever(handler.post(any())).thenAnswer { - (it.arguments[0] as Runnable).run() - true - } - - whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false) - whenever(internalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) - whenever(internalStylusDevice.isExternal).thenReturn(false) - whenever(externalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) - whenever(externalStylusDevice.isExternal).thenReturn(true) - - whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf()) - whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice) - whenever(inputManager.getInputDevice(INTERNAL_STYLUS_DEVICE_ID)) - .thenReturn(internalStylusDevice) - whenever(inputManager.getInputDevice(EXTERNAL_STYLUS_DEVICE_ID)) - .thenReturn(externalStylusDevice) - } - - @Test - fun start_flagDisabled_doesNotRegister() { - whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(false) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_toggleHasStarted() { - stylusListener.start() - - assert(stylusListener.hasStarted) - } - - @Test - fun start_hasStarted_doesNotRegister() { - stylusListener.hasStarted = true - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - } - - @Test - fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() { - whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(OTHER_DEVICE_ID)) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_stylusEverUsed_doesNotRegister() { - whenever(inputManager.inputDeviceIds) - .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID)) - whenever(inputManager.isStylusEverUsed(context)).thenReturn(true) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_hostDeviceSupportsStylus_registersListener() { - whenever(inputManager.inputDeviceIds) - .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID)) - - stylusListener.start() - - verify(stylusManager).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun onStylusAdded_hasNotStarted_doesNotRegisterListener() { - stylusListener.hasStarted = false - - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - verifyZeroInteractions(inputManager) - } - - @Test - fun onStylusAdded_internalStylus_registersListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, EXECUTOR, stylusListener) - } - - @Test - fun onStylusAdded_externalStylus_doesNotRegisterListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusAdded(EXTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any()) - } - - @Test - fun onStylusAdded_otherDevice_doesNotRegisterListener() { - stylusListener.onStylusAdded(OTHER_DEVICE_ID) - - verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any()) - } - - @Test - fun onStylusRemoved_registeredDevice_unregistersListener() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - } - - @Test - fun onStylusRemoved_hasNotStarted_doesNotUnregisterListener() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verifyZeroInteractions(inputManager) - } - - @Test - fun onStylusRemoved_unregisteredDevice_doesNotUnregisterListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verifyNoMoreInteractions(inputManager) - } - - @Test - fun onStylusBluetoothConnected_updateStylusFlagAndUnregisters() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY") - - verify(inputManager).setStylusEverUsed(context, true) - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - verify(stylusManager).unregisterCallback(stylusListener) - } - - @Test - fun onStylusBluetoothConnected_hasNotStarted_doesNoting() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY") - - verifyZeroInteractions(inputManager) - verifyZeroInteractions(stylusManager) - } - - @Test - fun onBatteryStateChanged_batteryPresent_updateStylusFlagAndUnregisters() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(true) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verify(inputManager).setStylusEverUsed(context, true) - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - verify(stylusManager).unregisterCallback(stylusListener) - } - - @Test - fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateFlagOrUnregister() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(false) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verifyZeroInteractions(stylusManager) - verify(inputManager, never()) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - } - - @Test - fun onBatteryStateChanged_hasNotStarted_doesNothing() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(false) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verifyZeroInteractions(inputManager) - verifyZeroInteractions(stylusManager) - } - - companion object { - private const val OTHER_DEVICE_ID = 0 - private const val INTERNAL_STYLUS_DEVICE_ID = 1 - private const val EXTERNAL_STYLUS_DEVICE_ID = 2 - private val EXECUTOR = FakeExecutor(FakeSystemClock()) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt index 984de5b67bf5..6d6e40a90fa6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt @@ -17,12 +17,15 @@ package com.android.systemui.stylus import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.hardware.BatteryState import android.hardware.input.InputManager import android.os.Handler import android.testing.AndroidTestingRunner import android.view.InputDevice import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import java.util.concurrent.Executor @@ -31,30 +34,27 @@ import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.inOrder import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest -@Ignore("b/257936830 until bt APIs") class StylusManagerTest : SysuiTestCase() { @Mock lateinit var inputManager: InputManager - @Mock lateinit var stylusDevice: InputDevice - @Mock lateinit var btStylusDevice: InputDevice - @Mock lateinit var otherDevice: InputDevice - + @Mock lateinit var batteryState: BatteryState @Mock lateinit var bluetoothAdapter: BluetoothAdapter - @Mock lateinit var bluetoothDevice: BluetoothDevice - @Mock lateinit var handler: Handler + @Mock lateinit var featureFlags: FeatureFlags @Mock lateinit var stylusCallback: StylusManager.StylusCallback @@ -75,11 +75,8 @@ class StylusManagerTest : SysuiTestCase() { true } - stylusManager = StylusManager(inputManager, bluetoothAdapter, handler, EXECUTOR) - - stylusManager.registerCallback(stylusCallback) - - stylusManager.registerBatteryCallback(stylusBatteryCallback) + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false) whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) @@ -92,19 +89,47 @@ class StylusManagerTest : SysuiTestCase() { whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice) whenever(inputManager.getInputDevice(BT_STYLUS_DEVICE_ID)).thenReturn(btStylusDevice) whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(STYLUS_DEVICE_ID)) + whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(false) whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(bluetoothDevice) whenever(bluetoothDevice.address).thenReturn(STYLUS_BT_ADDRESS) + + whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true) + + stylusManager.startListener() + stylusManager.registerCallback(stylusCallback) + stylusManager.registerBatteryCallback(stylusBatteryCallback) + clearInvocations(inputManager) } @Test - fun startListener_registersInputDeviceListener() { + fun startListener_hasNotStarted_registersInputDeviceListener() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + stylusManager.startListener() verify(inputManager, times(1)).registerInputDeviceListener(any(), any()) } @Test + fun startListener_hasStarted_doesNothing() { + stylusManager.startListener() + + verifyZeroInteractions(inputManager) + } + + @Test + fun onInputDeviceAdded_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test fun onInputDeviceAdded_multipleRegisteredCallbacks_callsAll() { stylusManager.registerCallback(otherStylusCallback) @@ -117,6 +142,26 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceAdded_internalStylus_registersBatteryListener() { + whenever(stylusDevice.isExternal).thenReturn(false) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verify(inputManager, times(1)) + .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager) + } + + @Test + fun onInputDeviceAdded_externalStylus_doesNotRegisterbatteryListener() { + whenever(stylusDevice.isExternal).thenReturn(true) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verify(inputManager, never()) + .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager) + } + + @Test fun onInputDeviceAdded_stylus_callsCallbacksOnStylusAdded() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) @@ -125,6 +170,23 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") + fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() { + stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) + + verify(stylusCallback, times(1)).onStylusFirstUsed() + } + + @Test + @Ignore("b/257936830 until bt APIs") + fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() { + stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) + + verify(inputManager, times(1)).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -143,6 +205,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceChanged_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + + stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_multipleRegisteredCallbacks_callsAll() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS) @@ -157,6 +230,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_stylusNewBtConnection_callsCallbacks() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS) @@ -168,6 +242,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_stylusLostBtConnection_callsCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) // whenever(btStylusDevice.bluetoothAddress).thenReturn(null) @@ -179,6 +254,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -189,6 +265,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) @@ -198,6 +275,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceRemoved_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test fun onInputDeviceRemoved_multipleRegisteredCallbacks_callsAll() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) stylusManager.registerCallback(otherStylusCallback) @@ -219,6 +307,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceRemoved_unregistersBatteryListener() { + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID) + + verify(inputManager, times(1)) + .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceRemoved_btStylus_callsCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -232,6 +331,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothConnected_registersMetadataListener() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -239,6 +339,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() { whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null) @@ -248,6 +349,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothDisconnected_unregistersMetadataListener() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -257,6 +359,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) stylusManager.registerBatteryCallback(otherStylusBatteryCallback) @@ -274,6 +377,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -288,6 +392,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -302,6 +407,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() { stylusManager.onMetadataChanged( bluetoothDevice, @@ -313,6 +419,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -326,6 +433,63 @@ class StylusManagerTest : SysuiTestCase() { .onStylusBluetoothChargingStateChanged(any(), any(), any()) } + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() { + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() { + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(stylusCallback, times(1)).onStylusFirstUsed() + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() { + whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true) + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager, never()).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateEverUsedFlag() { + whenever(batteryState.isPresent).thenReturn(false) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager, never()) + .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager) + } + + @Test + fun onBatteryStateChanged_hasNotStarted_doesNothing() { + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verifyZeroInteractions(inputManager) + } + + @Test + fun onBatteryStateChanged_executesBatteryCallbacks() { + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(stylusBatteryCallback, times(1)) + .onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + } + companion object { private val EXECUTOR = Executor { r -> r.run() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt index ff382a3ec19f..117e00dd9b2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt @@ -25,17 +25,15 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.whenever -import java.util.concurrent.Executor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock -import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -60,7 +58,6 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { inputManager, stylusUsiPowerUi, featureFlags, - DIRECT_EXECUTOR, ) whenever(featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)).thenReturn(true) @@ -79,40 +76,12 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @Test - fun start_addsBatteryListenerForInternalStylus() { - startable.start() - - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) - } - - @Test - fun onStylusAdded_internalStylus_addsBatteryListener() { - startable.onStylusAdded(STYLUS_DEVICE_ID) - - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) - } + fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() { + whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(EXTERNAL_DEVICE_ID)) - @Test - fun onStylusAdded_externalStylus_doesNotAddBatteryListener() { - startable.onStylusAdded(EXTERNAL_DEVICE_ID) - - verify(inputManager, never()) - .addInputDeviceBatteryListener(EXTERNAL_DEVICE_ID, DIRECT_EXECUTOR, startable) - } + startable.start() - @Test - fun onStylusRemoved_registeredStylus_removesBatteryListener() { - startable.onStylusAdded(STYLUS_DEVICE_ID) - startable.onStylusRemoved(STYLUS_DEVICE_ID) - - inOrder(inputManager).let { - it.verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) - it.verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, startable) - } + verifyZeroInteractions(stylusManager) } @Test @@ -130,28 +99,26 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @Test - fun onBatteryStateChanged_batteryPresent_refreshesNotification() { + fun onStylusUsiBatteryStateChanged_batteryPresent_refreshesNotification() { val batteryState = mock(BatteryState::class.java) whenever(batteryState.isPresent).thenReturn(true) - startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) verify(stylusUsiPowerUi, times(1)).updateBatteryState(batteryState) } @Test - fun onBatteryStateChanged_batteryNotPresent_noop() { + fun onStylusUsiBatteryStateChanged_batteryNotPresent_noop() { val batteryState = mock(BatteryState::class.java) whenever(batteryState.isPresent).thenReturn(false) - startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) verifyNoMoreInteractions(stylusUsiPowerUi) } companion object { - private val DIRECT_EXECUTOR = Executor { r -> r.run() } - private const val EXTERNAL_DEVICE_ID = 0 private const val STYLUS_DEVICE_ID = 1 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt index 59875507341d..a7951f4fa068 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.stylus +import android.app.Notification import android.hardware.BatteryState import android.hardware.input.InputManager import android.os.Handler @@ -28,10 +29,13 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever +import junit.framework.Assert.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.inOrder import org.mockito.Mockito.times @@ -46,6 +50,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Mock lateinit var inputManager: InputManager @Mock lateinit var handler: Handler @Mock lateinit var btStylusDevice: InputDevice + @Captor lateinit var notificationCaptor: ArgumentCaptor<Notification> private lateinit var stylusUsiPowerUi: StylusUsiPowerUI @@ -70,7 +75,8 @@ class StylusUsiPowerUiTest : SysuiTestCase() { fun updateBatteryState_capacityBelowThreshold_notifies() { stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) + verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) verifyNoMoreInteractions(notificationManager) } @@ -78,7 +84,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() { stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f)) - verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) verifyNoMoreInteractions(notificationManager) } @@ -88,8 +94,9 @@ class StylusUsiPowerUiTest : SysuiTestCase() { stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f)) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) it.verifyNoMoreInteractions() } } @@ -99,7 +106,16 @@ class StylusUsiPowerUiTest : SysuiTestCase() { stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.15f)) - verify(notificationManager, times(2)).notify(eq(R.string.stylus_battery_low), any()) + verify(notificationManager, times(2)) + .notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture()) + assertEquals( + notificationCaptor.value.extras.getString(Notification.EXTRA_TITLE), + context.getString(R.string.stylus_battery_low_percentage, "15%") + ) + assertEquals( + notificationCaptor.value.extras.getString(Notification.EXTRA_TEXT), + context.getString(R.string.stylus_battery_low_subtitle) + ) verifyNoMoreInteractions(notificationManager) } @@ -110,9 +126,11 @@ class StylusUsiPowerUiTest : SysuiTestCase() { stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) it.verifyNoMoreInteractions() } } @@ -121,7 +139,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { fun updateSuppression_noExistingNotification_cancelsNotification() { stylusUsiPowerUi.updateSuppression(true) - verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) verifyNoMoreInteractions(notificationManager) } @@ -132,8 +150,9 @@ class StylusUsiPowerUiTest : SysuiTestCase() { stylusUsiPowerUi.updateSuppression(true) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) it.verifyNoMoreInteractions() } } @@ -156,7 +175,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { stylusUsiPowerUi.refresh() - verify(notificationManager).cancel(R.string.stylus_battery_low) + verify(notificationManager).cancel(R.string.stylus_battery_low_percentage) } class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 5b424a39bb1e..a537848903bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -24,6 +24,7 @@ import static android.service.notification.NotificationListenerService.REASON_AP import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; import static com.google.common.truth.Truth.assertThat; @@ -228,6 +229,8 @@ public class BubblesTest extends SysuiTestCase { private BubbleEntry mBubbleEntryUser11; private BubbleEntry mBubbleEntry2User11; + private Intent mAppBubbleIntent; + @Mock private ShellInit mShellInit; @Mock @@ -323,6 +326,9 @@ public class BubblesTest extends SysuiTestCase { mBubbleEntry2User11 = BubblesManager.notifToBubbleEntry( mNotificationTestHelper.createBubble(handle)); + mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class); + mAppBubbleIntent.setPackage(mContext.getPackageName()); + mZenModeConfig.suppressedVisualEffects = 0; when(mZenModeController.getConfig()).thenReturn(mZenModeConfig); @@ -1630,6 +1636,62 @@ public class BubblesTest extends SysuiTestCase { any(Bubble.class), anyBoolean(), anyBoolean()); } + @Test + public void testShowOrHideAppBubble_addsAndExpand() { + assertThat(mBubbleController.isStackExpanded()).isFalse(); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull(); + + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + verify(mBubbleController).inflateAndAdd(any(Bubble.class), /* suppressFlyout= */ eq(true), + /* showInShade= */ eq(false)); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + } + + @Test + public void testShowOrHideAppBubble_expandIfCollapsed() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + mBubbleController.updateBubble(mBubbleEntry); + mBubbleController.collapseStack(); + assertThat(mBubbleController.isStackExpanded()).isFalse(); + + // Calling this while collapsed will expand the app bubble + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); + } + + @Test + public void testShowOrHideAppBubble_collapseIfSelected() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + + // Calling this while the app bubble is expanded should collapse the stack + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isFalse(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); + } + + @Test + public void testShowOrHideAppBubble_selectIfNotSelected() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + mBubbleController.updateBubble(mBubbleEntry); + mBubbleController.expandStackAndSelectBubble(mBubbleEntry); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(mBubbleEntry.getKey()); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); + } + /** Creates a bubble using the userId and package. */ private Bubble createBubble(int userId, String pkg) { final UserHandle userHandle = new UserHandle(userId); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java index 3767fbe98dc1..342855357fd2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java @@ -40,24 +40,49 @@ import java.io.IOException; public class MemoryTrackingTestCase extends SysuiTestCase { private static File sFilesDir = null; private static String sLatestTestClassName = null; + private static int sHeapCount = 0; + private static File sLatestBaselineHeapFile = null; - @Before public void grabFilesDir() { + // Ideally, we would do this in @BeforeClass just once, but we need mContext to get the files + // dir, and that does not exist until @Before on each test method. + @Before + public void grabFilesDir() throws IOException { + // This should happen only once per suite if (sFilesDir == null) { sFilesDir = mContext.getFilesDir(); } - sLatestTestClassName = getClass().getName(); + + // This will happen before the first test method in each class + if (sLatestTestClassName == null) { + sLatestTestClassName = getClass().getName(); + sLatestBaselineHeapFile = dump("baseline" + (++sHeapCount), "before-test"); + } } @AfterClass public static void dumpHeap() throws IOException { + File afterTestHeap = dump(sLatestTestClassName, "after-test"); + if (sLatestBaselineHeapFile != null && afterTestHeap != null) { + Log.w("MEMORY", "To compare heap to baseline (use go/ahat):"); + Log.w("MEMORY", " adb pull " + sLatestBaselineHeapFile); + Log.w("MEMORY", " adb pull " + afterTestHeap); + Log.w("MEMORY", + " java -jar ahat.jar --baseline " + sLatestBaselineHeapFile.getName() + " " + + afterTestHeap.getName()); + } + sLatestTestClassName = null; + } + + private static File dump(String basename, String heapKind) throws IOException { if (sFilesDir == null) { Log.e("MEMORY", "Somehow no test cases??"); - return; + return null; } mockitoTearDown(); - Log.w("MEMORY", "about to dump heap"); - File path = new File(sFilesDir, sLatestTestClassName + ".ahprof"); + Log.w("MEMORY", "about to dump " + heapKind + " heap"); + File path = new File(sFilesDir, basename + ".ahprof"); Debug.dumpHprofData(path.getPath()); - Log.w("MEMORY", "did it! Location: " + path); + Log.w("MEMORY", "Success! Location: " + path); + return path; } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 39d2ecaef51a..15b473640de7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -52,6 +52,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _isDozing = MutableStateFlow(false) override val isDozing: Flow<Boolean> = _isDozing + private val _isAodAvailable = MutableStateFlow(false) + override val isAodAvailable: Flow<Boolean> = _isAodAvailable + private val _isDreaming = MutableStateFlow(false) override val isDreaming: Flow<Boolean> = _isDreaming @@ -126,6 +129,10 @@ class FakeKeyguardRepository : KeyguardRepository { _isDozing.value = isDozing } + fun setAodAvailable(isAodAvailable: Boolean) { + _isAodAvailable.value = isAodAvailable + } + fun setDreamingWithOverlay(isDreaming: Boolean) { _isDreamingWithOverlay.value = isDreaming } diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 05e305c437ec..f4c6cc3de0b1 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -132,7 +132,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ private static final String TRACE_WM = "WindowManagerInternal"; private static final int WAIT_WINDOWS_TIMEOUT_MILLIS = 5000; - /** Display type for displays associated with the default user of th device. */ + /** Display type for displays associated with the default user of the device. */ public static final int DISPLAY_TYPE_DEFAULT = 1 << 0; /** Display type for displays associated with an AccessibilityDisplayProxy user. */ public static final int DISPLAY_TYPE_PROXY = 1 << 1; @@ -1993,17 +1993,29 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ private int resolveAccessibilityWindowIdLocked(int accessibilityWindowId) { if (accessibilityWindowId == AccessibilityWindowInfo.ACTIVE_WINDOW_ID) { - return mA11yWindowManager.getActiveWindowId(mSystemSupport.getCurrentUserIdLocked()); + final int focusedWindowId = + mA11yWindowManager.getActiveWindowId(mSystemSupport.getCurrentUserIdLocked()); + if (!mA11yWindowManager.windowIdBelongsToDisplayType(focusedWindowId, mDisplayTypes)) { + return AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; + } + return focusedWindowId; } return accessibilityWindowId; } private int resolveAccessibilityWindowIdForFindFocusLocked(int windowId, int focusType) { - if (windowId == AccessibilityWindowInfo.ACTIVE_WINDOW_ID) { - return mA11yWindowManager.getActiveWindowId(mSystemSupport.getCurrentUserIdLocked()); - } if (windowId == AccessibilityWindowInfo.ANY_WINDOW_ID) { - return mA11yWindowManager.getFocusedWindowId(focusType); + final int focusedWindowId = mA11yWindowManager.getFocusedWindowId(focusType); + // If the caller is a proxy and the found window doesn't belong to a proxy display + // (or vice versa), then return null. This doesn't work if there are multiple active + // proxys, but in the future this code shouldn't be needed if input and a11y focus are + // properly split. (so we will deal with the issues if we see them). + //TODO(254545943): Remove this when there is user and proxy separation of input and a11y + // focus + if (!mA11yWindowManager.windowIdBelongsToDisplayType(focusedWindowId, mDisplayTypes)) { + return AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; + } + return focusedWindowId; } return windowId; } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java index c050449e01d9..f0c6c4f6cf2c 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java @@ -156,6 +156,30 @@ public class AccessibilityWindowManager { } /** + * Returns {@code true} if the window belongs to a display of {@code displayTypes}. + */ + public boolean windowIdBelongsToDisplayType(int focusedWindowId, int displayTypes) { + // UIAutomation wants focus from any display type. + final int displayTypeMask = DISPLAY_TYPE_PROXY | DISPLAY_TYPE_DEFAULT; + if ((displayTypes & displayTypeMask) == displayTypeMask) { + return true; + } + synchronized (mLock) { + final int count = mDisplayWindowsObservers.size(); + for (int i = 0; i < count; i++) { + final DisplayWindowsObserver observer = mDisplayWindowsObservers.valueAt(i); + if (observer != null + && observer.findA11yWindowInfoByIdLocked(focusedWindowId) != null) { + return observer.mIsProxy + ? ((displayTypes & DISPLAY_TYPE_PROXY) != 0) + : (displayTypes & DISPLAY_TYPE_DEFAULT) != 0; + } + } + } + return false; + } + + /** * This class implements {@link WindowManagerInternal.WindowsForAccessibilityCallback} to * receive {@link WindowInfo}s from window manager when there's an accessibility change in * window and holds window lists information per display. @@ -430,6 +454,7 @@ public class AccessibilityWindowManager { return; } windowInfo.title = attributes.getWindowTitle(); + windowInfo.locales = attributes.getLocales(); } private boolean shouldUpdateWindowsLocked(boolean forceSend, @@ -756,6 +781,7 @@ public class AccessibilityWindowManager { reportedWindow.setPictureInPicture(window.inPictureInPicture); reportedWindow.setDisplayId(window.displayId); reportedWindow.setTaskId(window.taskId); + reportedWindow.setLocales(window.locales); final int parentId = findWindowIdLocked(userId, window.parentToken); if (parentId >= 0) { diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java index bb286e61815d..02e810f0e671 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java @@ -410,9 +410,6 @@ public class MagnificationController implements WindowMagnificationManager.Callb public void onRequestMagnificationSpec(int displayId, int serviceId) { final WindowMagnificationManager windowMagnificationManager; synchronized (mLock) { - if (serviceId == MAGNIFICATION_GESTURE_HANDLER_ID) { - return; - } updateMagnificationButton(displayId, ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); windowMagnificationManager = mWindowMagnificationMgr; } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index bce8812d2e9f..7df48994655d 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -826,7 +826,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (host != null) { host.callbacks = null; pruneHostLocked(host); - mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false); + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(), + false); } } } @@ -897,12 +898,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku Host host = lookupHostLocked(id); if (host != null) { - try { - mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false); - } catch (NullPointerException e) { - Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e); - throw e; - } + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(), + false); } } } @@ -4370,14 +4367,15 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku PendingHostUpdate.appWidgetRemoved(appWidgetId)); } - public SparseArray<String> getWidgetUids() { + public SparseArray<String> getWidgetUidsIfBound() { final SparseArray<String> uids = new SparseArray<>(); for (int i = widgets.size() - 1; i >= 0; i--) { final Widget widget = widgets.get(i); if (widget.provider == null) { if (DEBUG) { - Slog.e(TAG, "Widget with no provider " + widget.toString()); + Slog.d(TAG, "Widget with no provider " + widget.toString()); } + continue; } final ProviderId providerId = widget.provider.id; uids.put(providerId.uid, providerId.componentName.getPackageName()); diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 677871f6c85f..8c2c964e2d2c 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -357,6 +357,7 @@ final class SaveUi { params.width = WindowManager.LayoutParams.MATCH_PARENT; params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title); params.windowAnimations = R.style.AutofillSaveAnimation; + params.setTrustedOverlay(); show(); } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 758345f716c3..b0f2464ff52a 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -139,15 +139,6 @@ public class VirtualDeviceManagerService extends SystemService { mActivityInterceptorCallback); } - @GuardedBy("mVirtualDeviceManagerLock") - private boolean isValidVirtualDeviceLocked(IVirtualDevice virtualDevice) { - try { - return mVirtualDevices.contains(virtualDevice.getDeviceId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - void onCameraAccessBlocked(int appUid) { synchronized (mVirtualDeviceManagerLock) { for (int i = 0; i < mVirtualDevices.size(); i++) { @@ -347,6 +338,14 @@ public class VirtualDeviceManagerService extends SystemService { return VirtualDeviceManager.DEVICE_ID_DEFAULT; } + // Binder call + @Override + public boolean isValidVirtualDeviceId(int deviceId) { + synchronized (mVirtualDeviceManagerLock) { + return mVirtualDevices.contains(deviceId); + } + } + @Override // Binder call public int getAudioPlaybackSessionId(int deviceId) { synchronized (mVirtualDeviceManagerLock) { @@ -445,13 +444,6 @@ public class VirtualDeviceManagerService extends SystemService { private final ArraySet<Integer> mAllUidsOnVirtualDevice = new ArraySet<>(); @Override - public boolean isValidVirtualDevice(IVirtualDevice virtualDevice) { - synchronized (mVirtualDeviceManagerLock) { - return isValidVirtualDeviceLocked(virtualDevice); - } - } - - @Override public int getDeviceOwnerUid(int deviceId) { synchronized (mVirtualDeviceManagerLock) { VirtualDeviceImpl virtualDevice = mVirtualDevices.get(deviceId); diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java index 71a4c73a4dac..b41664f34c06 100644 --- a/services/core/java/com/android/server/BatteryService.java +++ b/services/core/java/com/android/server/BatteryService.java @@ -189,15 +189,20 @@ public final class BatteryService extends SystemService { private long mLastBatteryLevelChangedSentMs; private Bundle mBatteryChangedOptions = BroadcastOptions.makeRemovingMatchingFilter( - new IntentFilter(Intent.ACTION_BATTERY_CHANGED)).toBundle(); + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)).setDeferUntilActive(true) + .toBundle(); private Bundle mPowerConnectedOptions = BroadcastOptions.makeRemovingMatchingFilter( - new IntentFilter(Intent.ACTION_POWER_DISCONNECTED)).toBundle(); + new IntentFilter(Intent.ACTION_POWER_DISCONNECTED)).setDeferUntilActive(true) + .toBundle(); private Bundle mPowerDisconnectedOptions = BroadcastOptions.makeRemovingMatchingFilter( - new IntentFilter(Intent.ACTION_POWER_CONNECTED)).toBundle(); + new IntentFilter(Intent.ACTION_POWER_CONNECTED)).setDeferUntilActive(true) + .toBundle(); private Bundle mBatteryLowOptions = BroadcastOptions.makeRemovingMatchingFilter( - new IntentFilter(Intent.ACTION_BATTERY_OKAY)).toBundle(); + new IntentFilter(Intent.ACTION_BATTERY_OKAY)).setDeferUntilActive(true) + .toBundle(); private Bundle mBatteryOkayOptions = BroadcastOptions.makeRemovingMatchingFilter( - new IntentFilter(Intent.ACTION_BATTERY_LOW)).toBundle(); + new IntentFilter(Intent.ACTION_BATTERY_LOW)).setDeferUntilActive(true) + .toBundle(); private MetricsLogger mMetricsLogger; diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 819c948fd0e3..68722d207ae3 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -531,27 +531,30 @@ public class BinaryTransparencyService extends SystemService { pw.println("|--> Pre-installed package install location: " + origPackageFilepath); - if (useSha256) { - String sha256Digest = PackageUtils.computeSha256DigestForLargeFile( - origPackageFilepath, PackageUtils.createLargeFileBuffer()); - pw.println("|--> Pre-installed package SHA-256 digest: " - + sha256Digest); - } - + if (!origPackageFilepath.equals(APEX_PRELOAD_LOCATION_ERROR)) { + if (useSha256) { + String sha256Digest = PackageUtils.computeSha256DigestForLargeFile( + origPackageFilepath, PackageUtils.createLargeFileBuffer()); + pw.println("|--> Pre-installed package SHA-256 digest: " + + sha256Digest); + } - Map<Integer, byte[]> contentDigests = computeApkContentDigest( - origPackageFilepath); - if (contentDigests == null) { - pw.println("ERROR: Failed to compute package content digest for " - + origPackageFilepath); - } else { - for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) { - Integer algorithmId = entry.getKey(); - byte[] contentDigest = entry.getValue(); - pw.println("|--> Pre-installed package content digest: " - + HexEncoding.encodeToString(contentDigest, false)); - pw.println("|--> Pre-installed package content digest algorithm: " - + translateContentDigestAlgorithmIdToString(algorithmId)); + Map<Integer, byte[]> contentDigests = computeApkContentDigest( + origPackageFilepath); + if (contentDigests == null) { + pw.println("|--> ERROR: Failed to compute package content digest " + + "for " + origPackageFilepath); + } else { + for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) { + Integer algorithmId = entry.getKey(); + byte[] contentDigest = entry.getValue(); + pw.println("|--> Pre-installed package content digest: " + + HexEncoding.encodeToString(contentDigest, false)); + pw.println("|--> Pre-installed package content digest " + + "algorithm: " + + translateContentDigestAlgorithmIdToString( + algorithmId)); + } } } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index fc6d30bf58c9..1bc312e08575 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -159,6 +159,8 @@ import android.Manifest; import android.Manifest.permission; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.PermissionMethod; +import android.annotation.PermissionName; import android.annotation.UserIdInt; import android.app.Activity; import android.app.ActivityClient; @@ -251,8 +253,6 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.PermissionInfo; -import android.content.pm.PermissionMethod; -import android.content.pm.PermissionName; import android.content.pm.ProcessInfo; import android.content.pm.ProviderInfo; import android.content.pm.ProviderInfoList; @@ -399,7 +399,6 @@ import com.android.server.IoThread; import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; import com.android.server.LockGuard; -import com.android.server.NetworkManagementInternal; import com.android.server.PackageWatchdog; import com.android.server.ServiceThread; import com.android.server.SystemConfig; @@ -417,6 +416,7 @@ import com.android.server.criticalevents.CriticalEventLog; import com.android.server.firewall.IntentFirewall; import com.android.server.graphics.fonts.FontManagerInternal; import com.android.server.job.JobSchedulerInternal; +import com.android.server.net.NetworkManagementInternal; import com.android.server.os.NativeTombstoneManager; import com.android.server.pm.Computer; import com.android.server.pm.Installer; @@ -14108,8 +14108,16 @@ public class ActivityManagerService extends IActivityManager.Stub if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST, (sticky ? "Broadcast sticky: ": "Broadcast: ") + intent + " ordered=" + ordered + " userid=" + userId); - if ((resultTo != null) && !ordered && !mEnableModernQueue) { - Slog.w(TAG, "Broadcast " + intent + " not ordered but result callback requested!"); + if ((resultTo != null) && !ordered) { + if (!mEnableModernQueue) { + Slog.w(TAG, "Broadcast " + intent + " not ordered but result callback requested!"); + } + if (!UserHandle.isCore(callingUid)) { + String msg = "Unauthorized unordered resultTo broadcast " + + intent + " sent from uid " + callingUid; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } } userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true, @@ -14190,6 +14198,18 @@ public class ActivityManagerService extends IActivityManager.Stub } } + // resultTo broadcasts are always infinitely deferrable. + if ((resultTo != null) && !ordered && mEnableModernQueue) { + if (brOptions == null) { + brOptions = BroadcastOptions.makeBasic(); + } + brOptions.setDeferUntilActive(true); + } + + if (ordered && brOptions != null && brOptions.isDeferUntilActive()) { + throw new IllegalArgumentException("Ordered broadcasts can't be deferred until active"); + } + // Verify that protected broadcasts are only being sent by system code, // and that system code is only sending protected broadcasts. final boolean isProtectedBroadcast; diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 80684bf9fed3..788c81c3864c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -2153,19 +2153,24 @@ final class ActivityManagerShellCommand extends ShellCommand { boolean success; String displaySuffix; - if (displayId == Display.INVALID_DISPLAY) { - success = mInterface.startUserInBackgroundWithListener(userId, waiter); - displaySuffix = ""; - } else { - if (!UserManager.isVisibleBackgroundUsersEnabled()) { - pw.println("Not supported"); - return -1; + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "shell_runStartUser" + userId); + try { + if (displayId == Display.INVALID_DISPLAY) { + success = mInterface.startUserInBackgroundWithListener(userId, waiter); + displaySuffix = ""; + } else { + if (!UserManager.isVisibleBackgroundUsersEnabled()) { + pw.println("Not supported"); + return -1; + } + success = mInterface.startUserInBackgroundVisibleOnDisplay(userId, displayId); + displaySuffix = " on display " + displayId; } - success = mInterface.startUserInBackgroundVisibleOnDisplay(userId, displayId); - displaySuffix = " on display " + displayId; - } - if (wait && success) { - success = waiter.waitForFinish(USER_OPERATION_TIMEOUT_MS); + if (wait && success) { + success = waiter.waitForFinish(USER_OPERATION_TIMEOUT_MS); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } if (success) { diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index fa7748b9c16d..41d9a11e3eaf 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -62,6 +62,7 @@ import java.util.Objects; */ // @NotThreadSafe class BroadcastProcessQueue { + static final boolean VERBOSE = false; final @NonNull BroadcastConstants constants; final @NonNull String processName; final int uid; @@ -168,10 +169,14 @@ class BroadcastProcessQueue { /** * Count of pending broadcasts of these various flavors. */ + private int mCountEnqueued; + private int mCountDeferred; private int mCountForeground; + private int mCountForegroundDeferred; private int mCountOrdered; private int mCountAlarm; private int mCountPrioritized; + private int mCountPrioritizedDeferred; private int mCountInteractive; private int mCountResultTo; private int mCountInstrumented; @@ -226,10 +231,10 @@ class BroadcastProcessQueue { * used for ordered broadcasts and priority traunches. */ public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex, - @NonNull BroadcastConsumer replacedBroadcastConsumer) { + @NonNull BroadcastConsumer replacedBroadcastConsumer, boolean wouldBeSkipped) { if (record.isReplacePending()) { final boolean didReplace = replaceBroadcast(record, recordIndex, - replacedBroadcastConsumer); + replacedBroadcastConsumer, wouldBeSkipped); if (didReplace) { return; } @@ -240,13 +245,14 @@ class BroadcastProcessQueue { SomeArgs newBroadcastArgs = SomeArgs.obtain(); newBroadcastArgs.arg1 = record; newBroadcastArgs.argi1 = recordIndex; + newBroadcastArgs.argi2 = (wouldBeSkipped ? 1 : 0); // Cross-broadcast prioritization policy: some broadcasts might warrant being // issued ahead of others that are already pending, for example if this new // broadcast is in a different delivery class or is tied to a direct user interaction // with implicit responsiveness expectations. getQueueForBroadcast(record).addLast(newBroadcastArgs); - onBroadcastEnqueued(record, recordIndex); + onBroadcastEnqueued(record, recordIndex, wouldBeSkipped); } /** @@ -258,11 +264,12 @@ class BroadcastProcessQueue { * {@code false} otherwise. */ private boolean replaceBroadcast(@NonNull BroadcastRecord record, int recordIndex, - @NonNull BroadcastConsumer replacedBroadcastConsumer) { + @NonNull BroadcastConsumer replacedBroadcastConsumer, boolean wouldBeSkipped) { final int count = mPendingQueues.size(); for (int i = 0; i < count; ++i) { final ArrayDeque<SomeArgs> queue = mPendingQueues.get(i); - if (replaceBroadcastInQueue(queue, record, recordIndex, replacedBroadcastConsumer)) { + if (replaceBroadcastInQueue(queue, record, recordIndex, + replacedBroadcastConsumer, wouldBeSkipped)) { return true; } } @@ -279,13 +286,15 @@ class BroadcastProcessQueue { */ private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue, @NonNull BroadcastRecord record, int recordIndex, - @NonNull BroadcastConsumer replacedBroadcastConsumer) { + @NonNull BroadcastConsumer replacedBroadcastConsumer, + boolean wouldBeSkipped) { final Iterator<SomeArgs> it = queue.descendingIterator(); final Object receiver = record.receivers.get(recordIndex); while (it.hasNext()) { final SomeArgs args = it.next(); final BroadcastRecord testRecord = (BroadcastRecord) args.arg1; final int testRecordIndex = args.argi1; + final boolean testWouldBeSkipped = (args.argi2 == 1); final Object testReceiver = testRecord.receivers.get(testRecordIndex); if ((record.callingUid == testRecord.callingUid) && (record.userId == testRecord.userId) @@ -295,9 +304,10 @@ class BroadcastProcessQueue { // Exact match found; perform in-place swap args.arg1 = record; args.argi1 = recordIndex; + args.argi2 = (wouldBeSkipped ? 1 : 0); record.copyEnqueueTimeFrom(testRecord); - onBroadcastDequeued(testRecord, testRecordIndex); - onBroadcastEnqueued(record, recordIndex); + onBroadcastDequeued(testRecord, testRecordIndex, testWouldBeSkipped); + onBroadcastEnqueued(record, recordIndex, wouldBeSkipped); replacedBroadcastConsumer.accept(testRecord, testRecordIndex); return true; } @@ -352,12 +362,13 @@ class BroadcastProcessQueue { final SomeArgs args = it.next(); final BroadcastRecord record = (BroadcastRecord) args.arg1; final int recordIndex = args.argi1; + final boolean recordWouldBeSkipped = (args.argi2 == 1); if (predicate.test(record, recordIndex)) { consumer.accept(record, recordIndex); if (andRemove) { args.recycle(); it.remove(); - onBroadcastDequeued(record, recordIndex); + onBroadcastDequeued(record, recordIndex, recordWouldBeSkipped); } didSomething = true; } @@ -423,7 +434,7 @@ class BroadcastProcessQueue { } public int getPreferredSchedulingGroupLocked() { - if (mCountForeground > 0) { + if (mCountForeground > mCountForegroundDeferred) { // We have a foreground broadcast somewhere down the queue, so // boost priority until we drain them all return ProcessList.SCHED_GROUP_DEFAULT; @@ -469,10 +480,11 @@ class BroadcastProcessQueue { final SomeArgs next = removeNextBroadcast(); mActive = (BroadcastRecord) next.arg1; mActiveIndex = next.argi1; + final boolean wouldBeSkipped = (next.argi2 == 1); mActiveCountSinceIdle++; mActiveViaColdStart = false; next.recycle(); - onBroadcastDequeued(mActive, mActiveIndex); + onBroadcastDequeued(mActive, mActiveIndex, wouldBeSkipped); } /** @@ -489,8 +501,16 @@ class BroadcastProcessQueue { /** * Update summary statistics when the given record has been enqueued. */ - private void onBroadcastEnqueued(@NonNull BroadcastRecord record, int recordIndex) { + private void onBroadcastEnqueued(@NonNull BroadcastRecord record, int recordIndex, + boolean wouldBeSkipped) { + mCountEnqueued++; + if (record.deferUntilActive) { + mCountDeferred++; + } if (record.isForeground()) { + if (record.deferUntilActive) { + mCountForegroundDeferred++; + } mCountForeground++; } if (record.ordered) { @@ -500,6 +520,9 @@ class BroadcastProcessQueue { mCountAlarm++; } if (record.prioritized) { + if (record.deferUntilActive) { + mCountPrioritizedDeferred++; + } mCountPrioritized++; } if (record.interactive) { @@ -511,7 +534,8 @@ class BroadcastProcessQueue { if (record.callerInstrumented) { mCountInstrumented++; } - if (record.receivers.get(recordIndex) instanceof ResolveInfo) { + if (!wouldBeSkipped + && (record.receivers.get(recordIndex) instanceof ResolveInfo)) { mCountManifest++; } invalidateRunnableAt(); @@ -520,8 +544,16 @@ class BroadcastProcessQueue { /** * Update summary statistics when the given record has been dequeued. */ - private void onBroadcastDequeued(@NonNull BroadcastRecord record, int recordIndex) { + private void onBroadcastDequeued(@NonNull BroadcastRecord record, int recordIndex, + boolean wouldBeSkipped) { + mCountEnqueued--; + if (record.deferUntilActive) { + mCountDeferred--; + } if (record.isForeground()) { + if (record.deferUntilActive) { + mCountForegroundDeferred--; + } mCountForeground--; } if (record.ordered) { @@ -531,6 +563,9 @@ class BroadcastProcessQueue { mCountAlarm--; } if (record.prioritized) { + if (record.deferUntilActive) { + mCountPrioritizedDeferred--; + } mCountPrioritized--; } if (record.interactive) { @@ -542,7 +577,8 @@ class BroadcastProcessQueue { if (record.callerInstrumented) { mCountInstrumented--; } - if (record.receivers.get(recordIndex) instanceof ResolveInfo) { + if (!wouldBeSkipped + && (record.receivers.get(recordIndex) instanceof ResolveInfo)) { mCountManifest--; } invalidateRunnableAt(); @@ -741,6 +777,15 @@ class BroadcastProcessQueue { return mRunnableAt != Long.MAX_VALUE; } + public boolean isDeferredUntilActive() { + if (mRunnableAtInvalidated) updateRunnableAt(); + return mRunnableAtReason == BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER; + } + + public boolean hasDeferredBroadcasts() { + return (mCountDeferred > 0); + } + /** * Return time at which this process is considered runnable. This is * typically the time at which the next pending broadcast was first @@ -776,6 +821,7 @@ class BroadcastProcessQueue { static final int REASON_INSTRUMENTED = 5; static final int REASON_PERSISTENT = 6; static final int REASON_FORCE_DELAYED = 7; + static final int REASON_CACHED_INFINITE_DEFER = 8; static final int REASON_CONTAINS_FOREGROUND = 10; static final int REASON_CONTAINS_ORDERED = 11; static final int REASON_CONTAINS_ALARM = 12; @@ -794,6 +840,7 @@ class BroadcastProcessQueue { REASON_INSTRUMENTED, REASON_PERSISTENT, REASON_FORCE_DELAYED, + REASON_CACHED_INFINITE_DEFER, REASON_CONTAINS_FOREGROUND, REASON_CONTAINS_ORDERED, REASON_CONTAINS_ALARM, @@ -816,6 +863,7 @@ class BroadcastProcessQueue { case REASON_INSTRUMENTED: return "INSTRUMENTED"; case REASON_PERSISTENT: return "PERSISTENT"; case REASON_FORCE_DELAYED: return "FORCE_DELAYED"; + case REASON_CACHED_INFINITE_DEFER: return "INFINITE_DEFER"; case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND"; case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED"; case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM"; @@ -831,9 +879,16 @@ class BroadcastProcessQueue { private boolean blockedOnOrderedDispatch(BroadcastRecord r, int index) { final int blockedUntilTerminalCount = r.blockedUntilTerminalCount[index]; + int existingDeferredCount = 0; + if (r.deferUntilActive) { + for (int i = 0; i < index; i++) { + if (r.deferredUntilActive[i]) existingDeferredCount++; + } + } + // We might be blocked waiting for other receivers to finish, // typically for an ordered broadcast or priority traunches - if (r.terminalCount < blockedUntilTerminalCount + if ((r.terminalCount + existingDeferredCount) < blockedUntilTerminalCount && !isDeliveryStateTerminal(r.getDeliveryState(index))) { return true; } @@ -862,7 +917,7 @@ class BroadcastProcessQueue { if (mForcedDelayedDurationMs > 0) { mRunnableAt = runnableAt + mForcedDelayedDurationMs; mRunnableAtReason = REASON_FORCE_DELAYED; - } else if (mCountForeground > 0) { + } else if (mCountForeground > mCountForegroundDeferred) { mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS; mRunnableAtReason = REASON_CONTAINS_FOREGROUND; } else if (mCountInteractive > 0) { @@ -880,12 +935,9 @@ class BroadcastProcessQueue { } else if (mCountAlarm > 0) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_ALARM; - } else if (mCountPrioritized > 0) { + } else if (mCountPrioritized > mCountPrioritizedDeferred) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_PRIORITIZED; - } else if (mCountResultTo > 0) { - mRunnableAt = runnableAt; - mRunnableAtReason = REASON_CONTAINS_RESULT_TO; } else if (mCountManifest > 0) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_MANIFEST; @@ -893,8 +945,39 @@ class BroadcastProcessQueue { mRunnableAt = runnableAt; mRunnableAtReason = REASON_PERSISTENT; } else if (mProcessCached) { - mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS; - mRunnableAtReason = REASON_CACHED; + if (r.deferUntilActive) { + // All enqueued broadcasts are deferrable, defer + if (mCountDeferred == mCountEnqueued) { + mRunnableAt = Long.MAX_VALUE; + mRunnableAtReason = REASON_CACHED_INFINITE_DEFER; + } else { + // At least one enqueued broadcast isn't deferrable, repick time and reason + // for this record. If a later record is not deferrable and is one of these + // special cases, one of the cases above would have already caught that. + if (r.isForeground()) { + mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS; + mRunnableAtReason = REASON_CONTAINS_FOREGROUND; + } else if (r.prioritized) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_PRIORITIZED; + } else if (r.resultTo != null) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_RESULT_TO; + } else { + mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS; + mRunnableAtReason = REASON_CACHED; + } + } + } else { + // This record isn't deferrable + mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS; + mRunnableAtReason = REASON_CACHED; + } + } else if (mCountResultTo > 0) { + // All resultTo broadcasts are infinitely deferrable, so if the app + // is already cached, they'll be deferred on the line above + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_CONTAINS_RESULT_TO; } else { mRunnableAt = runnableAt + constants.DELAY_NORMAL_MILLIS; mRunnableAtReason = REASON_NORMAL; @@ -907,6 +990,15 @@ class BroadcastProcessQueue { mRunnableAt = Math.min(mRunnableAt, runnableAt); mRunnableAtReason = REASON_MAX_PENDING; } + + if (VERBOSE) { + Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, "BroadcastQueue", + ((app != null) ? app.processName : "(null)") + + ":" + r.intent.toString() + ":" + + r.deferUntilActive + + ":" + mRunnableAt + " " + reasonToString(mRunnableAtReason) + + ":" + ((app != null) ? app.isCached() : "false")); + } } else { mRunnableAt = Long.MAX_VALUE; mRunnableAtReason = REASON_EMPTY; diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index a994b1db7ca3..d819bfcc231c 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -385,6 +385,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // If app isn't running, and there's nothing in the queue, clean up if (queue.isEmpty() && !queue.isActive() && !queue.isProcessWarm()) { removeProcessQueue(queue.processName, queue.uid); + } else { + updateQueueDeferred(queue); } } @@ -637,11 +639,34 @@ class BroadcastQueueModernImpl extends BroadcastQueue { final ArraySet<BroadcastRecord> replacedBroadcasts = new ArraySet<>(); final BroadcastConsumer replacedBroadcastConsumer = (record, i) -> replacedBroadcasts.add(record); + boolean enqueuedBroadcast = false; + 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.enqueueOrReplaceBroadcast(r, i, replacedBroadcastConsumer); + + boolean wouldBeSkipped = false; + if (receiver instanceof ResolveInfo) { + // If the app is running but would not have been started if the process weren't + // running, we're going to deliver the broadcast but mark that it's not a manifest + // broadcast that would have started the app. This allows BroadcastProcessQueue to + // defer the broadcast as though it were a normal runtime receiver. + wouldBeSkipped = (mSkipPolicy.shouldSkipMessage(r, receiver) != null); + if (wouldBeSkipped && queue.app == null && !queue.getActiveViaColdStart()) { + // Skip receiver if there's no running app, the app is not being started, and + // the app wouldn't be launched for this broadcast + setDeliveryState(null, null, r, i, receiver, BroadcastRecord.DELIVERY_SKIPPED, + "skipped by policy to avoid cold start"); + continue; + } + } + enqueuedBroadcast = true; + queue.enqueueOrReplaceBroadcast(r, i, replacedBroadcastConsumer, wouldBeSkipped); + if (r.isDeferUntilActive() && queue.isDeferredUntilActive()) { + setDeliveryState(queue, null, r, i, receiver, BroadcastRecord.DELIVERY_DEFERRED, + "deferred at enqueue time"); + } updateRunnableList(queue); enqueueUpdateRunningList(); } @@ -651,7 +676,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { skipAndCancelReplacedBroadcasts(replacedBroadcasts); // If nothing to dispatch, send any pending result immediately - if (r.receivers.isEmpty()) { + if (r.receivers.isEmpty() || !enqueuedBroadcast) { scheduleResultTo(r); notifyFinishBroadcast(r); } @@ -1195,11 +1220,19 @@ class BroadcastQueueModernImpl extends BroadcastQueue { @NonNull Object receiver, @DeliveryState int newDeliveryState, String reason) { final int cookie = traceBegin("setDeliveryState"); final int oldDeliveryState = getDeliveryState(r, index); + boolean checkFinished = false; // Only apply state when we haven't already reached a terminal state; // this is how we ignore racing timeout messages if (!isDeliveryStateTerminal(oldDeliveryState)) { r.setDeliveryState(index, newDeliveryState); + if (oldDeliveryState == BroadcastRecord.DELIVERY_DEFERRED) { + r.deferredCount--; + } else if (newDeliveryState == BroadcastRecord.DELIVERY_DEFERRED) { + // If we're deferring a broadcast, maybe that's enough to unblock the final callback + r.deferredCount++; + checkFinished = true; + } } // Emit any relevant tracing results when we're changing the delivery @@ -1217,7 +1250,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // bookkeeping to update for ordered broadcasts if (!isDeliveryStateTerminal(oldDeliveryState) && isDeliveryStateTerminal(newDeliveryState)) { - if (newDeliveryState != BroadcastRecord.DELIVERY_DELIVERED) { + if (DEBUG_BROADCAST + && newDeliveryState != BroadcastRecord.DELIVERY_DELIVERED) { logw("Delivery state of " + r + " to " + receiver + " via " + app + " changed from " + deliveryStateToString(oldDeliveryState) + " to " @@ -1226,9 +1260,12 @@ class BroadcastQueueModernImpl extends BroadcastQueue { r.terminalCount++; notifyFinishReceiver(queue, r, index, receiver); - - // When entire ordered broadcast finished, deliver final result - final boolean recordFinished = (r.terminalCount == r.receivers.size()); + checkFinished = true; + } + // When entire ordered broadcast finished, deliver final result + if (checkFinished) { + final boolean recordFinished = + ((r.terminalCount + r.deferredCount) == r.receivers.size()); if (recordFinished) { scheduleResultTo(r); } @@ -1329,6 +1366,16 @@ class BroadcastQueueModernImpl extends BroadcastQueue { r.resultExtras = null; }; + private final BroadcastConsumer mBroadcastConsumerDefer = (r, i) -> { + setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_DEFERRED, + "mBroadcastConsumerDefer"); + }; + + private final BroadcastConsumer mBroadcastConsumerUndoDefer = (r, i) -> { + setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_PENDING, + "mBroadcastConsumerUndoDefer"); + }; + /** * Verify that all known {@link #mProcessQueues} are in the state tested by * the given {@link Predicate}. @@ -1392,6 +1439,21 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } } + private void updateQueueDeferred( + @NonNull BroadcastProcessQueue leaf) { + if (leaf.isDeferredUntilActive()) { + leaf.forEachMatchingBroadcast((r, i) -> { + return r.deferUntilActive && (r.getDeliveryState(i) + == BroadcastRecord.DELIVERY_PENDING); + }, mBroadcastConsumerDefer, false); + } else if (leaf.hasDeferredBroadcasts()) { + leaf.forEachMatchingBroadcast((r, i) -> { + return r.deferUntilActive && (r.getDeliveryState(i) + == BroadcastRecord.DELIVERY_DEFERRED); + }, mBroadcastConsumerUndoDefer, false); + } + } + @Override public void start(@NonNull ContentResolver resolver) { mFgConstants.startObserving(mHandler, resolver); @@ -1404,6 +1466,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { BroadcastProcessQueue leaf = mProcessQueues.get(uid); while (leaf != null) { leaf.setProcessCached(cached); + updateQueueDeferred(leaf); updateRunnableList(leaf); leaf = leaf.processNameNext; } diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 1d4d425590c1..9679fb8f2d10 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -88,6 +88,7 @@ final class BroadcastRecord extends Binder { final boolean interactive; // originated from user interaction? final boolean initialSticky; // initial broadcast from register to sticky? final boolean prioritized; // contains more than one priority tranche + final boolean deferUntilActive; // infinitely deferrable broadcast final int userId; // user id this broadcast was for final @Nullable String resolvedType; // the resolved data type final @Nullable String[] requiredPermissions; // permissions the caller has required @@ -97,6 +98,7 @@ final class BroadcastRecord extends Binder { final @Nullable BroadcastOptions options; // BroadcastOptions supplied by caller final @NonNull List<Object> receivers; // contains BroadcastFilter and ResolveInfo final @DeliveryState int[] delivery; // delivery state of each receiver + final boolean[] deferredUntilActive; // whether each receiver is infinitely deferred final int[] blockedUntilTerminalCount; // blocked until count of each receiver @Nullable ProcessRecord resultToApp; // who receives final result if non-null @Nullable IIntentReceiver resultTo; // who receives final result if non-null @@ -130,6 +132,7 @@ final class BroadcastRecord extends Binder { int manifestCount; // number of manifest receivers dispatched. int manifestSkipCount; // number of manifest receivers skipped. int terminalCount; // number of receivers in terminal state. + int deferredCount; // number of receivers in deferred state. @Nullable BroadcastQueue queue; // the outbound queue handling this broadcast // if set to true, app's process will be temporarily allowed to start activities from background @@ -168,6 +171,8 @@ final class BroadcastRecord extends Binder { static final int DELIVERY_SCHEDULED = 4; /** Terminal state: failure to dispatch */ static final int DELIVERY_FAILURE = 5; + /** Intermediate state: currently deferred while app is cached */ + static final int DELIVERY_DEFERRED = 6; @IntDef(flag = false, prefix = { "DELIVERY_" }, value = { DELIVERY_PENDING, @@ -176,6 +181,7 @@ final class BroadcastRecord extends Binder { DELIVERY_TIMEOUT, DELIVERY_SCHEDULED, DELIVERY_FAILURE, + DELIVERY_DEFERRED, }) @Retention(RetentionPolicy.SOURCE) public @interface DeliveryState {} @@ -188,6 +194,7 @@ final class BroadcastRecord extends Binder { case DELIVERY_TIMEOUT: return "TIMEOUT"; case DELIVERY_SCHEDULED: return "SCHEDULED"; case DELIVERY_FAILURE: return "FAILURE"; + case DELIVERY_DEFERRED: return "DEFERRED"; default: return Integer.toString(deliveryState); } } @@ -388,6 +395,8 @@ final class BroadcastRecord extends Binder { options = _options; receivers = (_receivers != null) ? _receivers : EMPTY_RECEIVERS; delivery = new int[_receivers != null ? _receivers.size() : 0]; + deferUntilActive = options != null ? options.isDeferUntilActive() : false; + deferredUntilActive = new boolean[deferUntilActive ? delivery.length : 0]; blockedUntilTerminalCount = calculateBlockedUntilTerminalCount(receivers, _serialized); scheduledTime = new long[delivery.length]; terminalTime = new long[delivery.length]; @@ -443,6 +452,8 @@ final class BroadcastRecord extends Binder { options = from.options; receivers = from.receivers; delivery = from.delivery; + deferUntilActive = from.deferUntilActive; + deferredUntilActive = from.deferredUntilActive; blockedUntilTerminalCount = from.blockedUntilTerminalCount; scheduledTime = from.scheduledTime; terminalTime = from.terminalTime; @@ -606,7 +617,7 @@ final class BroadcastRecord extends Binder { */ void setDeliveryState(int index, @DeliveryState int deliveryState) { delivery[index] = deliveryState; - + if (deferUntilActive) deferredUntilActive[index] = false; switch (deliveryState) { case DELIVERY_DELIVERED: case DELIVERY_SKIPPED: @@ -617,6 +628,9 @@ final class BroadcastRecord extends Binder { case DELIVERY_SCHEDULED: scheduledTime[index] = SystemClock.uptimeMillis(); break; + case DELIVERY_DEFERRED: + if (deferUntilActive) deferredUntilActive[index] = true; + break; } } @@ -647,6 +661,10 @@ final class BroadcastRecord extends Binder { return (intent.getFlags() & Intent.FLAG_RECEIVER_OFFLOAD) != 0; } + boolean isDeferUntilActive() { + return deferUntilActive; + } + /** * Core policy determination about this broadcast's delivery prioritization */ diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index 481ab17b609e..6718319c7ac6 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -229,12 +229,7 @@ public class BroadcastSkipPolicy { return "Background execution disabled: receiving " + r.intent + " to " + component.flattenToShortString(); - } else if (((r.intent.getFlags()&Intent.FLAG_RECEIVER_EXCLUDE_BACKGROUND) != 0) - || (r.intent.getComponent() == null - && r.intent.getPackage() == null - && ((r.intent.getFlags() - & Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND) == 0) - && !isSignaturePerm(r.requiredPermissions))) { + } else if (disallowBackgroundStart(r)) { mService.addBackgroundCheckViolationLocked(r.intent.getAction(), component.getPackageName()); return "Background execution not allowed: receiving " @@ -341,6 +336,18 @@ public class BroadcastSkipPolicy { } /** + * Determine if the given {@link BroadcastRecord} is eligible to launch processes. + */ + public boolean disallowBackgroundStart(@NonNull BroadcastRecord r) { + return ((r.intent.getFlags() & Intent.FLAG_RECEIVER_EXCLUDE_BACKGROUND) != 0) + || (r.intent.getComponent() == null + && r.intent.getPackage() == null + && ((r.intent.getFlags() + & Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND) == 0) + && !isSignaturePerm(r.requiredPermissions)); + } + + /** * Determine if the given {@link BroadcastRecord} is eligible to be sent to * the given {@link BroadcastFilter}. * @@ -624,7 +631,7 @@ public class BroadcastSkipPolicy { /** * Return true if all given permissions are signature-only perms. */ - private boolean isSignaturePerm(String[] perms) { + private static boolean isSignaturePerm(String[] perms) { if (perms == null) { return false; } diff --git a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java index 5c18827ebd61..7d9b272906c6 100644 --- a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java +++ b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java @@ -50,6 +50,8 @@ import com.android.server.infra.AbstractMasterSystemService; import com.android.server.infra.FrameworkResourcesServiceNameResolver; import com.android.server.pm.KnownPackages; +import com.google.android.collect.Sets; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -67,12 +69,10 @@ public class AmbientContextManagerService extends AmbientContextManagerPerUserService> { private static final String TAG = AmbientContextManagerService.class.getSimpleName(); private static final String KEY_SERVICE_ENABLED = "service_enabled"; - private static final Set<Integer> DEFAULT_EVENT_SET = new HashSet<>(){{ - add(AmbientContextEvent.EVENT_COUGH); - add(AmbientContextEvent.EVENT_SNORE); - add(AmbientContextEvent.EVENT_BACK_DOUBLE_TAP); - } - }; + private static final Set<Integer> DEFAULT_EVENT_SET = Sets.newHashSet( + AmbientContextEvent.EVENT_COUGH, + AmbientContextEvent.EVENT_SNORE, + AmbientContextEvent.EVENT_BACK_DOUBLE_TAP); /** Default value in absence of {@link DeviceConfig} override. */ private static final boolean DEFAULT_SERVICE_ENABLED = true; @@ -409,14 +409,6 @@ public class AmbientContextManagerService extends } } - private Set<Integer> intArrayToIntegerSet(int[] eventTypes) { - Set<Integer> types = new HashSet<>(); - for (Integer i : eventTypes) { - types.add(i); - } - return types; - } - private AmbientContextManagerPerUserService.ServiceType getServiceType(String serviceName) { final String wearableService = mContext.getResources() .getString(R.string.config_defaultWearableSensingService); @@ -513,6 +505,14 @@ public class AmbientContextManagerService extends return intArray; } + private Set<Integer> intArrayToIntegerSet(int[] eventTypes) { + Set<Integer> types = new HashSet<>(); + for (Integer i : eventTypes) { + types.add(i); + } + return types; + } + @NonNull private static Integer[] intArrayToIntegerArray(@NonNull int[] integerSet) { Integer[] intArray = new Integer[integerSet.length]; @@ -567,37 +567,24 @@ public class AmbientContextManagerService extends mContext.enforceCallingOrSelfPermission( Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG); assertCalledByPackageOwner(packageName); + AmbientContextManagerPerUserService service = getAmbientContextManagerPerUserServiceForEventTypes( UserHandle.getCallingUserId(), request.getEventTypes()); - if (service == null) { Slog.w(TAG, "onRegisterObserver unavailable user_id: " + UserHandle.getCallingUserId()); - } - - if (service.getServiceType() == ServiceType.DEFAULT && !mIsServiceEnabled) { - Slog.d(TAG, "Service not available."); - service.completeRegistration(observer, - AmbientContextManager.STATUS_SERVICE_UNAVAILABLE); - return; - } - if (service.getServiceType() == ServiceType.WEARABLE && !mIsWearableServiceEnabled) { - Slog.d(TAG, "Wearable Service not available."); - service.completeRegistration(observer, - AmbientContextManager.STATUS_SERVICE_UNAVAILABLE); - return; - } - if (containsMixedEvents(integerSetToIntArray(request.getEventTypes()))) { - Slog.d(TAG, "AmbientContextEventRequest contains mixed events," - + " this is not supported."); - service.completeRegistration(observer, - AmbientContextManager.STATUS_NOT_SUPPORTED); return; } - service.onRegisterObserver(request, packageName, observer); + int statusCode = checkStatusCode( + service, integerSetToIntArray(request.getEventTypes())); + if (statusCode == AmbientContextManager.STATUS_SUCCESS) { + service.onRegisterObserver(request, packageName, observer); + } else { + service.completeRegistration(observer, statusCode); + } } @Override @@ -606,10 +593,10 @@ public class AmbientContextManagerService extends Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG); assertCalledByPackageOwner(callingPackage); - AmbientContextManagerPerUserService service = null; for (ClientRequest cr : mExistingClientRequests) { if (cr.getPackageName().equals(callingPackage)) { - service = getAmbientContextManagerPerUserServiceForEventTypes( + AmbientContextManagerPerUserService service = + getAmbientContextManagerPerUserServiceForEventTypes( UserHandle.getCallingUserId(), cr.getRequest().getEventTypes()); if (service != null) { service.onUnregisterObserver(callingPackage); @@ -635,34 +622,18 @@ public class AmbientContextManagerService extends getAmbientContextManagerPerUserServiceForEventTypes( UserHandle.getCallingUserId(), intArrayToIntegerSet(eventTypes)); if (service == null) { - Slog.w(TAG, "onQueryServiceStatus unavailable user_id: " + Slog.w(TAG, "queryServiceStatus unavailable user_id: " + UserHandle.getCallingUserId()); - } - - if (service.getServiceType() == ServiceType.DEFAULT && !mIsServiceEnabled) { - Slog.d(TAG, "Service not available."); - service.sendStatusCallback(statusCallback, - AmbientContextManager.STATUS_SERVICE_UNAVAILABLE); - return; - } - if (service.getServiceType() == ServiceType.WEARABLE - && !mIsWearableServiceEnabled) { - Slog.d(TAG, "Wearable Service not available."); - service.sendStatusCallback(statusCallback, - AmbientContextManager.STATUS_SERVICE_UNAVAILABLE); return; } - if (containsMixedEvents(eventTypes)) { - Slog.d(TAG, "AmbientContextEventRequest contains mixed events," - + " this is not supported."); - service.sendStatusCallback(statusCallback, - AmbientContextManager.STATUS_NOT_SUPPORTED); - return; + int statusCode = checkStatusCode(service, eventTypes); + if (statusCode == AmbientContextManager.STATUS_SUCCESS) { + service.onQueryServiceStatus(eventTypes, callingPackage, + statusCallback); + } else { + service.sendStatusCallback(statusCallback, statusCode); } - - service.onQueryServiceStatus(eventTypes, callingPackage, - statusCallback); } } @@ -708,5 +679,22 @@ public class AmbientContextManagerService extends new AmbientContextShellCommand(AmbientContextManagerService.this).exec( this, in, out, err, args, callback, resultReceiver); } + + private int checkStatusCode(AmbientContextManagerPerUserService service, int[] eventTypes) { + if (service.getServiceType() == ServiceType.DEFAULT && !mIsServiceEnabled) { + Slog.d(TAG, "Service not enabled."); + return AmbientContextManager.STATUS_SERVICE_UNAVAILABLE; + } + if (service.getServiceType() == ServiceType.WEARABLE && !mIsWearableServiceEnabled) { + Slog.d(TAG, "Wearable Service not available."); + return AmbientContextManager.STATUS_SERVICE_UNAVAILABLE; + } + if (containsMixedEvents(eventTypes)) { + Slog.d(TAG, "AmbientContextEventRequest contains mixed events," + + " this is not supported."); + return AmbientContextManager.STATUS_NOT_SUPPORTED; + } + return AmbientContextManager.STATUS_SUCCESS; + } } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 78bff9524b10..58ddd9c6a8b8 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -3717,9 +3717,11 @@ public class AudioService extends IAudioService.Stub setRingerMode(getNewRingerMode(stream, index, flags), TAG + ".onSetStreamVolume", false /*external*/); } - // setting non-zero volume for a muted stream unmutes the stream and vice versa, + // setting non-zero volume for a muted stream unmutes the stream and vice versa + // (only when changing volume for the current device), // except for BT SCO stream where only explicit mute is allowed to comply to BT requirements - if (streamType != AudioSystem.STREAM_BLUETOOTH_SCO) { + if ((streamType != AudioSystem.STREAM_BLUETOOTH_SCO) + && (getDeviceForStream(stream) == device)) { mStreamStates[stream].mute(index == 0); } } diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index 7edd911299e5..919b85061db5 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -202,15 +202,7 @@ public class SoundDoseHelper { } } mCurrentCsd = currentCsd; - mDoseRecords.addAll(Arrays.asList(records)); - long totalDuration = 0; - for (SoundDoseRecord record : records) { - Log.i(TAG, " new record: csd=" + record.value - + " averageMel=" + record.averageMel + " timestamp=" + record.timestamp - + " duration=" + record.duration); - totalDuration += record.duration; - } - mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration)); + updateSoundDoseRecords(records, currentCsd); } }; @@ -626,6 +618,29 @@ public class SoundDoseHelper { return null; } + private void updateSoundDoseRecords(SoundDoseRecord[] newRecords, float currentCsd) { + long totalDuration = 0; + for (SoundDoseRecord record : newRecords) { + Log.i(TAG, " new record: " + record); + totalDuration += record.duration; + + if (record.value < 0) { + // Negative value means the record timestamp exceeded the CSD rolling window size + // and needs to be removed + if (!mDoseRecords.removeIf( + r -> r.value == -record.value && r.timestamp == record.timestamp + && r.averageMel == record.averageMel + && r.duration == record.duration)) { + Log.w(TAG, "Could not find cached record to remove: " + record); + } + } else { + mDoseRecords.add(record); + } + } + + mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration)); + } + // StreamVolumeCommand contains the information needed to defer the process of // setStreamVolume() in case the user has to acknowledge the safe volume warning message. private static class StreamVolumeCommand { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index cb409fef6fa2..0248010bc9f3 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -125,7 +125,7 @@ public class FaceService extends SystemService { return proto.getBytes(); } - @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC) + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public List<FaceSensorPropertiesInternal> getSensorPropertiesInternal( String opPackageName) { diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java index 39b13547cf93..c9ae735d63e2 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java @@ -86,12 +86,8 @@ final class ProgramInfoCache { } @VisibleForTesting - boolean programInfosAreExactly(RadioManager.ProgramInfo... programInfos) { - Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> expectedMap = new ArrayMap<>(); - for (int i = 0; i < programInfos.length; i++) { - expectedMap.put(programInfos[i].getSelector().getPrimaryId(), programInfos[i]); - } - return expectedMap.equals(mProgramInfoMap); + List<RadioManager.ProgramInfo> toProgramInfoList() { + return new ArrayList<>(mProgramInfoMap.values()); } @Override diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java index e3ea1a6a3de7..974c04bc45f5 100644 --- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java +++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java @@ -68,11 +68,6 @@ public abstract class VirtualDeviceManagerInternal { public abstract void onAppsOnVirtualDeviceChanged(); /** - * Validate the virtual device. - */ - public abstract boolean isValidVirtualDevice(IVirtualDevice virtualDevice); - - /** * Gets the owner uid for a deviceId. * * @param deviceId which device we're asking about diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java index dcc98e17fadf..bde14ee2000d 100644 --- a/services/core/java/com/android/server/content/SyncManager.java +++ b/services/core/java/com/android/server/content/SyncManager.java @@ -49,6 +49,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.PeriodicSync; import android.content.ServiceConnection; +import android.content.SharedPreferences; import android.content.SyncActivityTooManyDeletes; import android.content.SyncAdapterType; import android.content.SyncAdaptersCache; @@ -205,13 +206,6 @@ public class SyncManager { */ private static final long SYNC_DELAY_ON_CONFLICT = 10*1000; // 10 seconds - /** - * Generate job ids in the range [MIN_SYNC_JOB_ID, MAX_SYNC_JOB_ID) to avoid conflicts with - * other jobs scheduled by the system process. - */ - private static final int MIN_SYNC_JOB_ID = 100000; - private static final int MAX_SYNC_JOB_ID = 110000; - private static final String SYNC_WAKE_LOCK_PREFIX = "*sync*/"; private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarm"; private static final String SYNC_LOOP_WAKE_LOCK = "SyncLoopWakeLock"; @@ -229,6 +223,9 @@ public class SyncManager { private static final int SYNC_ADAPTER_CONNECTION_FLAGS = Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND | Context.BIND_ALLOW_OOM_MANAGEMENT; + private static final String PREF_KEY_SYNC_JOB_NAMESPACE_MIGRATED = + "sync_job_namespace_migrated"; + /** Singleton instance. */ @GuardedBy("SyncManager.class") private static SyncManager sInstance; @@ -242,12 +239,11 @@ public class SyncManager { volatile private PowerManager.WakeLock mSyncManagerWakeLock; volatile private boolean mDataConnectionIsConnected = false; - private volatile int mNextJobIdOffset = 0; + private volatile int mNextJobId = 0; private final NotificationManager mNotificationMgr; private final IBatteryStats mBatteryStats; private JobScheduler mJobScheduler; - private JobSchedulerInternal mJobSchedulerInternal; private SyncStorageEngine mSyncStorageEngine; @@ -281,24 +277,19 @@ public class SyncManager { } private int getUnusedJobIdH() { - final int maxNumSyncJobIds = MAX_SYNC_JOB_ID - MIN_SYNC_JOB_ID; - final List<JobInfo> pendingJobs = mJobSchedulerInternal.getSystemScheduledPendingJobs(); - for (int i = 0; i < maxNumSyncJobIds; ++i) { - int newJobId = MIN_SYNC_JOB_ID + ((mNextJobIdOffset + i) % maxNumSyncJobIds); - if (!isJobIdInUseLockedH(newJobId, pendingJobs)) { - mNextJobIdOffset = (mNextJobIdOffset + i + 1) % maxNumSyncJobIds; - return newJobId; - } - } - // We've used all 10,000 intended job IDs.... We're probably in a world of pain right now :/ - Slog.wtf(TAG, "All " + maxNumSyncJobIds + " possible sync job IDs are taken :/"); - mNextJobIdOffset = (mNextJobIdOffset + 1) % maxNumSyncJobIds; - return MIN_SYNC_JOB_ID + mNextJobIdOffset; + final List<JobInfo> pendingJobs = mJobScheduler.getAllPendingJobs(); + while (isJobIdInUseLockedH(mNextJobId, pendingJobs)) { + // SyncManager jobs are placed in their own namespace. Since there's no chance of + // conflicting with other parts of the system, we can just keep incrementing until + // we find an unused ID. + mNextJobId++; + } + return mNextJobId; } private List<SyncOperation> getAllPendingSyncs() { verifyJobScheduler(); - List<JobInfo> pendingJobs = mJobSchedulerInternal.getSystemScheduledPendingJobs(); + List<JobInfo> pendingJobs = mJobScheduler.getAllPendingJobs(); final int numJobs = pendingJobs.size(); final List<SyncOperation> pendingSyncs = new ArrayList<>(numJobs); for (int i = 0; i < numJobs; ++i) { @@ -306,6 +297,8 @@ public class SyncManager { SyncOperation op = SyncOperation.maybeCreateFromJobExtras(job.getExtras()); if (op != null) { pendingSyncs.add(op); + } else { + Slog.wtf(TAG, "Non-sync job inside of SyncManager's namespace"); } } return pendingSyncs; @@ -491,6 +484,31 @@ public class SyncManager { }); } + /** + * Migrate syncs from the default job namespace to SyncManager's namespace if they haven't been + * migrated already. + */ + private void migrateSyncJobNamespaceIfNeeded() { + final SharedPreferences prefs = mContext.getSharedPreferences( + mSyncStorageEngine.getSyncDir(), Context.MODE_PRIVATE); + if (prefs.getBoolean(PREF_KEY_SYNC_JOB_NAMESPACE_MIGRATED, false)) { + return; + } + final List<JobInfo> pendingJobs = getJobSchedulerInternal().getSystemScheduledPendingJobs(); + final JobScheduler jobSchedulerDefaultNamespace = + mContext.getSystemService(JobScheduler.class); + for (int i = pendingJobs.size() - 1; i >= 0; --i) { + final JobInfo job = pendingJobs.get(i); + final SyncOperation op = SyncOperation.maybeCreateFromJobExtras(job.getExtras()); + if (op != null) { + // This is a sync. Move it over to SyncManager's namespace. + mJobScheduler.schedule(job); + jobSchedulerDefaultNamespace.cancel(job.getId()); + } + } + prefs.edit().putBoolean(PREF_KEY_SYNC_JOB_NAMESPACE_MIGRATED, true).apply(); + } + private synchronized void verifyJobScheduler() { if (mJobScheduler != null) { return; @@ -500,10 +518,12 @@ public class SyncManager { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.d(TAG, "initializing JobScheduler object."); } - mJobScheduler = (JobScheduler) mContext.getSystemService( - Context.JOB_SCHEDULER_SERVICE); - mJobSchedulerInternal = getJobSchedulerInternal(); - // Get all persisted syncs from JobScheduler + // Use a dedicated namespace to avoid conflicts with other jobs + // scheduled by the system process. + mJobScheduler = mContext.getSystemService(JobScheduler.class) + .forNamespace("SyncManager"); + migrateSyncJobNamespaceIfNeeded(); + // Get all persisted syncs from JobScheduler in the SyncManager namespace. List<JobInfo> pendingJobs = mJobScheduler.getAllPendingJobs(); int numPersistedPeriodicSyncs = 0; @@ -519,6 +539,8 @@ public class SyncManager { // shown on the settings activity. mSyncStorageEngine.markPending(op.target, true); } + } else { + Slog.wtf(TAG, "Non-sync job inside of SyncManager namespace"); } } final String summary = "Loaded persisted syncs: " diff --git a/services/core/java/com/android/server/content/SyncStorageEngine.java b/services/core/java/com/android/server/content/SyncStorageEngine.java index 9c1cf38500f3..f7468fc97a3d 100644 --- a/services/core/java/com/android/server/content/SyncStorageEngine.java +++ b/services/core/java/com/android/server/content/SyncStorageEngine.java @@ -21,6 +21,7 @@ import static com.android.server.content.SyncLogger.logSafe; import android.accounts.Account; import android.accounts.AccountAndUser; import android.accounts.AccountManager; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.backup.BackupManager; import android.content.ComponentName; @@ -574,6 +575,11 @@ public class SyncStorageEngine { return sSyncStorageEngine; } + @NonNull + File getSyncDir() { + return mSyncDir; + } + protected void setOnSyncRequestListener(OnSyncRequestListener listener) { if (mSyncRequestListener == null) { mSyncRequestListener = listener; diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index d44e1dc121c9..06b99f82362a 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -47,6 +47,7 @@ import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.app.compat.CompatChanges; import android.companion.virtual.IVirtualDevice; +import android.companion.virtual.VirtualDeviceManager; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.BroadcastReceiver; @@ -1281,12 +1282,17 @@ public final class DisplayManagerService extends SystemService { final Surface surface = virtualDisplayConfig.getSurface(); int flags = virtualDisplayConfig.getFlags(); if (virtualDevice != null) { - final VirtualDeviceManagerInternal vdm = - getLocalService(VirtualDeviceManagerInternal.class); - if (!vdm.isValidVirtualDevice(virtualDevice)) { - throw new SecurityException("Invalid virtual device"); + final VirtualDeviceManager vdm = mContext.getSystemService(VirtualDeviceManager.class); + try { + if (!vdm.isValidVirtualDeviceId(virtualDevice.getDeviceId())) { + throw new SecurityException("Invalid virtual device"); + } + } catch (RemoteException ex) { + throw new SecurityException("Unable to validate virtual device"); } - flags |= vdm.getBaseVirtualDisplayFlags(virtualDevice); + final VirtualDeviceManagerInternal localVdm = + getLocalService(VirtualDeviceManagerInternal.class); + flags |= localVdm.getBaseVirtualDisplayFlags(virtualDevice); } if (surface != null && surface.isSingleBuffered()) { diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index c99a7a0de79c..993b4fdb2498 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -31,6 +31,7 @@ import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.InputManager; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; @@ -51,6 +52,7 @@ import java.io.PrintWriter; import java.util.Arrays; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -102,7 +104,7 @@ final class BatteryController { BatteryController(Context context, NativeInputManagerService nativeService, Looper looper) { this(context, nativeService, looper, new UEventManager() {}, - new LocalBluetoothBatteryManager(context)); + new LocalBluetoothBatteryManager(context, looper)); } @VisibleForTesting @@ -163,7 +165,7 @@ final class BatteryController { // This is the first listener that is monitoring this device. monitor = new DeviceMonitor(deviceId); mDeviceMonitors.put(deviceId, monitor); - updateBluetoothMonitoring(); + updateBluetoothBatteryMonitoring(); } if (DEBUG) { @@ -378,13 +380,26 @@ final class BatteryController { } } - private void handleBluetoothBatteryLevelChange(long eventTime, String address) { + private void handleBluetoothBatteryLevelChange(long eventTime, String address, + int batteryLevel) { synchronized (mLock) { final DeviceMonitor monitor = findIf(mDeviceMonitors, (m) -> (m.mBluetoothDevice != null && address.equals(m.mBluetoothDevice.getAddress()))); if (monitor != null) { - monitor.onBluetoothBatteryChanged(eventTime); + monitor.onBluetoothBatteryChanged(eventTime, batteryLevel); + } + } + } + + private void handleBluetoothMetadataChange(@NonNull BluetoothDevice device, int key, + @Nullable byte[] value) { + synchronized (mLock) { + final DeviceMonitor monitor = + findIf(mDeviceMonitors, (m) -> device.equals(m.mBluetoothDevice)); + if (monitor != null) { + final long eventTime = SystemClock.uptimeMillis(); + monitor.onBluetoothMetadataChanged(eventTime, key, value); } } } @@ -514,31 +529,19 @@ final class BatteryController { isPresent ? mNative.getBatteryCapacity(deviceId) / 100.f : Float.NaN); } - // Queries the battery state of an input device from Bluetooth. - private State queryBatteryStateFromBluetooth(int deviceId, long updateTime, - @NonNull BluetoothDevice bluetoothDevice) { - final int level = mBluetoothBatteryManager.getBatteryLevel(bluetoothDevice.getAddress()); - if (level == BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF - || level == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { - return new State(deviceId); - } - return new State(deviceId, updateTime, true /*isPresent*/, BatteryState.STATUS_UNKNOWN, - level / 100.f); - } - - private void updateBluetoothMonitoring() { + private void updateBluetoothBatteryMonitoring() { synchronized (mLock) { if (anyOf(mDeviceMonitors, (m) -> m.mBluetoothDevice != null)) { // At least one input device being monitored is connected over Bluetooth. if (mBluetoothBatteryListener == null) { if (DEBUG) Slog.d(TAG, "Registering bluetooth battery listener"); mBluetoothBatteryListener = this::handleBluetoothBatteryLevelChange; - mBluetoothBatteryManager.addListener(mBluetoothBatteryListener); + mBluetoothBatteryManager.addBatteryListener(mBluetoothBatteryListener); } } else if (mBluetoothBatteryListener != null) { // No Bluetooth input devices are monitored, so remove the registered listener. if (DEBUG) Slog.d(TAG, "Unregistering bluetooth battery listener"); - mBluetoothBatteryManager.removeListener(mBluetoothBatteryListener); + mBluetoothBatteryManager.removeBatteryListener(mBluetoothBatteryListener); mBluetoothBatteryListener = null; } } @@ -550,16 +553,23 @@ final class BatteryController { // Represents whether the input device has a sysfs battery node. protected boolean mHasBattery = false; - protected final State mBluetoothState; @Nullable private BluetoothDevice mBluetoothDevice; + long mBluetoothEventTime = 0; + // The battery level reported by the Bluetooth Hands-Free Profile (HPF) obtained through + // BluetoothDevice#getBatteryLevel(). + int mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + // The battery level and status reported through the Bluetooth device's metadata. + int mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + int mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + @Nullable + private BluetoothAdapter.OnMetadataChangedListener mBluetoothMetadataListener; @Nullable private UEventBatteryListener mUEventBatteryListener; DeviceMonitor(int deviceId) { mState = new State(deviceId); - mBluetoothState = new State(deviceId); // Load the initial battery state and start monitoring. final long eventTime = SystemClock.uptimeMillis(); @@ -570,7 +580,7 @@ final class BatteryController { final State oldState = getBatteryStateForReporting(); changes.accept(eventTime); final State newState = getBatteryStateForReporting(); - if (!oldState.equals(newState)) { + if (!oldState.equalsIgnoringUpdateTime(newState)) { notifyAllListenersForDevice(newState); } } @@ -594,13 +604,22 @@ final class BatteryController { final BluetoothDevice bluetoothDevice = getBluetoothDevice(deviceId); if (!Objects.equals(mBluetoothDevice, bluetoothDevice)) { if (DEBUG) { - Slog.d(TAG, "Bluetooth device " - + ((bluetoothDevice != null) ? "is" : "is not") - + " now present for deviceId " + deviceId); + Slog.d(TAG, "Bluetooth device is now " + + ((bluetoothDevice != null) ? "" : "not") + + " present for deviceId " + deviceId); } + + mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + stopBluetoothMetadataMonitoring(); + mBluetoothDevice = bluetoothDevice; - updateBluetoothMonitoring(); - updateBatteryStateFromBluetooth(eventTime); + updateBluetoothBatteryMonitoring(); + + if (mBluetoothDevice != null) { + mBluetoothBatteryLevel = mBluetoothBatteryManager.getBatteryLevel( + mBluetoothDevice.getAddress()); + startBluetoothMetadataMonitoring(eventTime); + } } } @@ -632,11 +651,39 @@ final class BatteryController { } } + private void startBluetoothMetadataMonitoring(long eventTime) { + Objects.requireNonNull(mBluetoothDevice); + + mBluetoothMetadataListener = BatteryController.this::handleBluetoothMetadataChange; + mBluetoothBatteryManager.addMetadataListener(mBluetoothDevice.getAddress(), + mBluetoothMetadataListener); + updateBluetoothMetadataState(eventTime, BluetoothDevice.METADATA_MAIN_BATTERY, + mBluetoothBatteryManager.getMetadata(mBluetoothDevice.getAddress(), + BluetoothDevice.METADATA_MAIN_BATTERY)); + updateBluetoothMetadataState(eventTime, BluetoothDevice.METADATA_MAIN_CHARGING, + mBluetoothBatteryManager.getMetadata(mBluetoothDevice.getAddress(), + BluetoothDevice.METADATA_MAIN_CHARGING)); + } + + private void stopBluetoothMetadataMonitoring() { + if (mBluetoothMetadataListener == null) { + return; + } + Objects.requireNonNull(mBluetoothDevice); + + mBluetoothBatteryManager.removeMetadataListener( + mBluetoothDevice.getAddress(), mBluetoothMetadataListener); + mBluetoothMetadataListener = null; + mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + } + // This must be called when the device is no longer being monitored. public void onMonitorDestroy() { stopNativeMonitoring(); + stopBluetoothMetadataMonitoring(); mBluetoothDevice = null; - updateBluetoothMonitoring(); + updateBluetoothBatteryMonitoring(); } protected void updateBatteryStateFromNative(long eventTime) { @@ -644,13 +691,6 @@ final class BatteryController { queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery)); } - protected void updateBatteryStateFromBluetooth(long eventTime) { - final State bluetoothState = mBluetoothDevice == null ? new State(mState.deviceId) - : queryBatteryStateFromBluetooth(mState.deviceId, eventTime, - mBluetoothDevice); - mBluetoothState.updateIfChanged(bluetoothState); - } - public void onPoll(long eventTime) { processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); } @@ -659,8 +699,51 @@ final class BatteryController { processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); } - public void onBluetoothBatteryChanged(long eventTime) { - processChangesAndNotify(eventTime, this::updateBatteryStateFromBluetooth); + public void onBluetoothBatteryChanged(long eventTime, int bluetoothBatteryLevel) { + processChangesAndNotify(eventTime, (time) -> { + mBluetoothBatteryLevel = bluetoothBatteryLevel; + mBluetoothEventTime = time; + }); + } + + public void onBluetoothMetadataChanged(long eventTime, int key, @Nullable byte[] value) { + processChangesAndNotify(eventTime, + (time) -> updateBluetoothMetadataState(time, key, value)); + } + + private void updateBluetoothMetadataState(long eventTime, int key, + @Nullable byte[] value) { + switch (key) { + case BluetoothDevice.METADATA_MAIN_BATTERY: + mBluetoothEventTime = eventTime; + mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + if (value != null) { + try { + mBluetoothMetadataBatteryLevel = Integer.parseInt( + new String(value)); + } catch (NumberFormatException e) { + Slog.wtf(TAG, + "Failed to parse bluetooth METADATA_MAIN_BATTERY with " + + "value '" + + new String(value) + "' for device " + + mBluetoothDevice); + } + } + break; + case BluetoothDevice.METADATA_MAIN_CHARGING: + mBluetoothEventTime = eventTime; + if (value != null) { + mBluetoothMetadataBatteryStatus = Boolean.parseBoolean( + new String(value)) + ? BatteryState.STATUS_CHARGING + : BatteryState.STATUS_DISCHARGING; + } else { + mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + } + break; + default: + break; + } } public boolean requiresPolling() { @@ -677,11 +760,25 @@ final class BatteryController { // Returns the current battery state that can be used to notify listeners BatteryController. public State getBatteryStateForReporting() { - // Give precedence to the Bluetooth battery state if it's present. - if (mBluetoothState.isPresent) { - return new State(mBluetoothState); + // Give precedence to the Bluetooth battery state, and fall back to the native state. + return Objects.requireNonNullElseGet(resolveBluetoothBatteryState(), + () -> new State(mState)); + } + + @Nullable + protected State resolveBluetoothBatteryState() { + final int level; + // Prefer battery level obtained from the metadata over the Bluetooth Hands-Free + // Profile (HFP). + if (mBluetoothMetadataBatteryLevel >= 0 && mBluetoothMetadataBatteryLevel <= 100) { + level = mBluetoothMetadataBatteryLevel; + } else if (mBluetoothBatteryLevel >= 0 && mBluetoothBatteryLevel <= 100) { + level = mBluetoothBatteryLevel; + } else { + return null; } - return new State(mState); + return new State(mState.deviceId, mBluetoothEventTime, true, + mBluetoothMetadataBatteryStatus, level / 100.f); } @Override @@ -690,7 +787,7 @@ final class BatteryController { + ", Name='" + getInputDeviceName(mState.deviceId) + "'" + ", NativeBattery=" + mState + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none") - + ", BluetoothBattery=" + mBluetoothState; + + ", BluetoothState=" + resolveBluetoothBatteryState(); } } @@ -775,12 +872,10 @@ final class BatteryController { @Override public State getBatteryStateForReporting() { - // Give precedence to the Bluetooth battery state if it's present. - if (mBluetoothState.isPresent) { - return new State(mBluetoothState); - } - return mValidityTimeoutCallback != null - ? new State(mState) : new State(mState.deviceId); + // Give precedence to the Bluetooth battery state, and fall back to the native state. + return Objects.requireNonNullElseGet(resolveBluetoothBatteryState(), + () -> mValidityTimeoutCallback != null + ? new State(mState) : new State(mState.deviceId)); } @Override @@ -844,15 +939,24 @@ final class BatteryController { interface BluetoothBatteryManager { @VisibleForTesting interface BluetoothBatteryListener { - void onBluetoothBatteryChanged(long eventTime, String address); + void onBluetoothBatteryChanged(long eventTime, String address, int batteryLevel); } - void addListener(BluetoothBatteryListener listener); - void removeListener(BluetoothBatteryListener listener); + // Methods used for obtaining the Bluetooth battery level through Bluetooth HFP. + void addBatteryListener(BluetoothBatteryListener listener); + void removeBatteryListener(BluetoothBatteryListener listener); int getBatteryLevel(String address); + + // Methods used for obtaining the battery level through Bluetooth metadata. + void addMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener); + void removeMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener); + byte[] getMetadata(String address, int key); } private static class LocalBluetoothBatteryManager implements BluetoothBatteryManager { private final Context mContext; + private final Executor mExecutor; @Nullable @GuardedBy("mBroadcastReceiver") private BluetoothBatteryListener mRegisteredListener; @@ -868,24 +972,25 @@ final class BatteryController { if (bluetoothDevice == null) { return; } - // We do not use the EXTRA_LEVEL value. Instead, the battery level will be queried - // from BluetoothDevice later so that we use a single source for the battery level. + final int batteryLevel = intent.getIntExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL, + BluetoothDevice.BATTERY_LEVEL_UNKNOWN); synchronized (mBroadcastReceiver) { if (mRegisteredListener != null) { final long eventTime = SystemClock.uptimeMillis(); mRegisteredListener.onBluetoothBatteryChanged( - eventTime, bluetoothDevice.getAddress()); + eventTime, bluetoothDevice.getAddress(), batteryLevel); } } } }; - LocalBluetoothBatteryManager(Context context) { + LocalBluetoothBatteryManager(Context context, Looper looper) { mContext = context; + mExecutor = new HandlerExecutor(new Handler(looper)); } @Override - public void addListener(BluetoothBatteryListener listener) { + public void addBatteryListener(BluetoothBatteryListener listener) { synchronized (mBroadcastReceiver) { if (mRegisteredListener != null) { throw new IllegalStateException( @@ -898,7 +1003,7 @@ final class BatteryController { } @Override - public void removeListener(BluetoothBatteryListener listener) { + public void removeBatteryListener(BluetoothBatteryListener listener) { synchronized (mBroadcastReceiver) { if (!listener.equals(mRegisteredListener)) { throw new IllegalStateException("Listener is not registered."); @@ -912,6 +1017,28 @@ final class BatteryController { public int getBatteryLevel(String address) { return getBluetoothDevice(mContext, address).getBatteryLevel(); } + + @Override + public void addMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener) { + Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class)) + .getAdapter().addOnMetadataChangedListener( + getBluetoothDevice(mContext, address), mExecutor, + listener); + } + + @Override + public void removeMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener) { + Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class)) + .getAdapter().removeOnMetadataChangedListener( + getBluetoothDevice(mContext, address), listener); + } + + @Override + public byte[] getMetadata(String address, int key) { + return getBluetoothDevice(mContext, address).getMetadata(key); + } } // Helper class that adds copying and printing functionality to IInputDeviceBatteryState. @@ -954,7 +1081,7 @@ final class BatteryController { this.capacity = capacity; } - private boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) { + public boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) { long updateTime = this.updateTime; this.updateTime = other.updateTime; boolean eq = this.equals(other); diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java new file mode 100644 index 000000000000..86a08579e38b --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY; + +import static com.android.server.EventLogTags.IMF_HIDE_IME; +import static com.android.server.EventLogTags.IMF_SHOW_IME; +import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; +import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME; + +import android.annotation.Nullable; +import android.os.Binder; +import android.os.IBinder; +import android.os.ResultReceiver; +import android.util.EventLog; +import android.util.Slog; +import android.view.inputmethod.ImeTracker; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.InputMethodDebug; +import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.server.LocalServices; +import com.android.server.wm.WindowManagerInternal; + +import java.util.Objects; + +/** + * The default implementation of {@link ImeVisibilityApplier} used in + * {@link InputMethodManagerService}. + */ +final class DefaultImeVisibilityApplier implements ImeVisibilityApplier { + + private static final String TAG = "DefaultImeVisibilityApplier"; + + private static final boolean DEBUG = InputMethodManagerService.DEBUG; + + private InputMethodManagerService mService; + + private final WindowManagerInternal mWindowManagerInternal; + + + DefaultImeVisibilityApplier(InputMethodManagerService service) { + mService = service; + mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); + } + + @GuardedBy("ImfLock.class") + @Override + public void performShowIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { + final IInputMethodInvoker curMethod = mService.getCurMethodLocked(); + if (curMethod != null) { + // create a placeholder token for IMS so that IMS cannot inject windows into client app. + final IBinder showInputToken = new Binder(); + mService.setRequestImeTokenToWindow(windowToken, showInputToken); + if (DEBUG) { + Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken + + ", " + showFlags + ", " + resultReceiver + ") for reason: " + + InputMethodDebug.softInputDisplayReasonToString(reason)); + } + // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. + if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) { + if (DEBUG_IME_VISIBILITY) { + EventLog.writeEvent(IMF_SHOW_IME, statsToken.getTag(), + Objects.toString(mService.mCurFocusedWindow), + InputMethodDebug.softInputDisplayReasonToString(reason), + InputMethodDebug.softInputModeToString( + mService.mCurFocusedWindowSoftInputMode)); + } + mService.onShowHideSoftInputRequested(true /* show */, windowToken, reason, + statsToken); + } + } + } + + @GuardedBy("ImfLock.class") + @Override + public void performHideIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { + final IInputMethodInvoker curMethod = mService.getCurMethodLocked(); + if (curMethod != null) { + final Binder hideInputToken = new Binder(); + mService.setRequestImeTokenToWindow(windowToken, hideInputToken); + // The IME will report its visible state again after the following message finally + // delivered to the IME process as an IPC. Hence the inconsistency between + // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in + // the final state. + if (DEBUG) { + Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken + + ", " + resultReceiver + ") for reason: " + + InputMethodDebug.softInputDisplayReasonToString(reason)); + } + // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. + if (curMethod.hideSoftInput(hideInputToken, statsToken, 0, resultReceiver)) { + if (DEBUG_IME_VISIBILITY) { + EventLog.writeEvent(IMF_HIDE_IME, statsToken.getTag(), + Objects.toString(mService.mCurFocusedWindow), + InputMethodDebug.softInputDisplayReasonToString(reason), + InputMethodDebug.softInputModeToString( + mService.mCurFocusedWindowSoftInputMode)); + } + mService.onShowHideSoftInputRequested(false /* show */, windowToken, reason, + statsToken); + } + } + } + + @GuardedBy("ImfLock.class") + @Override + public void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + @ImeVisibilityStateComputer.VisibilityState int state) { + switch (state) { + case STATE_SHOW_IME: + ImeTracker.get().onProgress(statsToken, + ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); + // Send to window manager to show IME after IME layout finishes. + mWindowManagerInternal.showImePostLayout(windowToken, statsToken); + break; + case STATE_HIDE_IME: + if (mService.mCurFocusedWindowClient != null) { + ImeTracker.get().onProgress(statsToken, + ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); + // IMMS only knows of focused window, not the actual IME target. + // e.g. it isn't aware of any window that has both + // NOT_FOCUSABLE, ALT_FOCUSABLE_IM flags set and can the IME target. + // Send it to window manager to hide IME from IME target window. + // TODO(b/139861270): send to mCurClient.client once IMMS is aware of + // actual IME target. + mWindowManagerInternal.hideIme(windowToken, + mService.mCurFocusedWindowClient.mSelfReportedDisplayId, statsToken); + } else { + ImeTracker.get().onFailed(statsToken, + ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); + } + break; + default: + throw new IllegalArgumentException("Invalid IME visibility state: " + state); + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java new file mode 100644 index 000000000000..e97ec93b23c8 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import android.annotation.Nullable; +import android.os.IBinder; +import android.os.ResultReceiver; +import android.view.inputmethod.ImeTracker; + +import com.android.internal.inputmethod.SoftInputShowHideReason; + +/** + * Interface for IME visibility operations like show/hide and update Z-ordering relative to the IME + * targeted window. + */ +interface ImeVisibilityApplier { + /** + * Performs showing IME on top of the given window. + * + * @param windowToken The token of a window that currently has focus. + * @param statsToken A token that tracks the progress of an IME request. + * @param showFlags Provides additional operating flags to show IME. + * @param resultReceiver If non-null, this will be called back to the caller when + * it has processed request to tell what it has done. + * @param reason The reason for requesting to show IME. + */ + default void performShowIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {} + + /** + * Performs hiding IME to the given window + * + * @param windowToken The token of a window that currently has focus. + * @param statsToken A token that tracks the progress of an IME request. + * @param resultReceiver If non-null, this will be called back to the caller when + * it has processed request to tell what it has done. + * @param reason The reason for requesting to hide IME. + */ + default void performHideIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {} + + /** + * Applies the IME visibility from {@link android.inputmethodservice.InputMethodService} with + * according to the given visibility state. + * + * @param windowToken The token of a window for applying the IME visibility + * @param statsToken A token that tracks the progress of an IME request. + * @param state The new IME visibility state for the applier to handle + */ + default void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + @ImeVisibilityStateComputer.VisibilityState int state) {} + + /** + * Updates the IME Z-ordering relative to the given window. + * + * This used to adjust the IME relative layer of the window during + * {@link InputMethodManagerService} is in switching IME clients. + * + * @param windowToken The token of a window to update the Z-ordering relative to the IME. + */ + default void updateImeLayeringByTarget(IBinder windowToken) { + // TODO: add a method in WindowManagerInternal to call DC#updateImeInputAndControlTarget + // here to end up updating IME layering after IMMS#attachNewInputLocked called. + } +} diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java new file mode 100644 index 000000000000..a2655f4c0f4d --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HIDDEN; +import static android.server.inputmethod.InputMethodManagerServiceProto.ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD; +import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_EXPLICITLY_REQUESTED; +import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_FORCED; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED; +import static android.view.WindowManager.LayoutParams.SoftInputModeFlags; + +import static com.android.internal.inputmethod.InputMethodDebug.softInputModeToString; +import static com.android.server.inputmethod.InputMethodManagerService.computeImeDisplayIdForTarget; + +import android.accessibilityservice.AccessibilityService; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.os.IBinder; +import android.util.PrintWriterPrinter; +import android.util.Printer; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; +import android.view.WindowManager; +import android.view.inputmethod.ImeTracker; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodManager; + +import com.android.server.LocalServices; +import com.android.server.wm.WindowManagerInternal; + +import java.io.PrintWriter; +import java.util.WeakHashMap; + +/** + * A computer used by {@link InputMethodManagerService} that computes the IME visibility state + * according the given {@link ImeTargetWindowState} from the focused window or the app requested IME + * visibility from {@link InputMethodManager}. + */ +public final class ImeVisibilityStateComputer { + + private static final String TAG = "ImeVisibilityStateComputer"; + + private static final boolean DEBUG = InputMethodManagerService.DEBUG; + + private final InputMethodManagerService mService; + private final WindowManagerInternal mWindowManagerInternal; + + final InputMethodManagerService.ImeDisplayValidator mImeDisplayValidator; + + /** + * A map used to track the requested IME target window and its state. The key represents the + * token of the window and the value is the corresponding IME window state. + */ + private final WeakHashMap<IBinder, ImeTargetWindowState> mRequestWindowStateMap = + new WeakHashMap<>(); + + /** + * Set if IME was explicitly told to show the input method. + * + * @see InputMethodManager#SHOW_IMPLICIT that we set the value is {@code false}. + * @see InputMethodManager#HIDE_IMPLICIT_ONLY that system will not hide IME when the value is + * {@code true}. + */ + boolean mRequestedShowExplicitly; + + /** + * Set if we were forced to be shown. + * + * @see InputMethodManager#SHOW_FORCED + * @see InputMethodManager#HIDE_NOT_ALWAYS + */ + boolean mShowForced; + + /** Represent the invalid IME visibility state */ + public static final int STATE_INVALID = -1; + + /** State to handle hiding the IME window requested by the app. */ + public static final int STATE_HIDE_IME = 0; + + /** State to handle showing the IME window requested by the app. */ + public static final int STATE_SHOW_IME = 1; + + /** State to handle showing the IME window with making the overlay window above it. */ + public static final int STATE_SHOW_IME_ABOVE_OVERLAY = 2; + + /** State to handle showing the IME window with making the overlay window behind it. */ + public static final int STATE_SHOW_IME_BEHIND_OVERLAY = 3; + + /** State to handle showing an IME preview surface during the app was loosing the IME focus */ + public static final int STATE_SHOW_IME_SNAPSHOT = 4; + @IntDef({ + STATE_INVALID, + STATE_HIDE_IME, + STATE_SHOW_IME, + STATE_SHOW_IME_ABOVE_OVERLAY, + STATE_SHOW_IME_BEHIND_OVERLAY, + STATE_SHOW_IME_SNAPSHOT, + }) + @interface VisibilityState {} + + /** + * The policy to configure the IME visibility. + */ + private final ImeVisibilityPolicy mPolicy; + + public ImeVisibilityStateComputer(InputMethodManagerService service) { + mService = service; + mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); + mImeDisplayValidator = mWindowManagerInternal::getDisplayImePolicy; + mPolicy = new ImeVisibilityPolicy(); + } + + /** + * Called when {@link InputMethodManagerService} is processing the show IME request. + * @param statsToken The token for tracking this show request + * @param showFlags The additional operation flags to indicate whether this show request mode is + * implicit or explicit. + * @return {@code true} when the computer has proceed this show request operation. + */ + boolean onImeShowFlags(@NonNull ImeTracker.Token statsToken, int showFlags) { + if (mPolicy.mA11yRequestingNoSoftKeyboard || mPolicy.mImeHiddenByDisplayPolicy) { + ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY); + return false; + } + ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY); + if ((showFlags & InputMethodManager.SHOW_FORCED) != 0) { + mRequestedShowExplicitly = true; + mShowForced = true; + } else if ((showFlags & InputMethodManager.SHOW_IMPLICIT) == 0) { + mRequestedShowExplicitly = true; + } + return true; + } + + /** + * Called when {@link InputMethodManagerService} is processing the hide IME request. + * @param statsToken The token for tracking this hide request + * @param hideFlags The additional operation flags to indicate whether this hide request mode is + * implicit or explicit. + * @return {@code true} when the computer has proceed this hide request operations. + */ + boolean canHideIme(@NonNull ImeTracker.Token statsToken, int hideFlags) { + if ((hideFlags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0 + && (mRequestedShowExplicitly || mShowForced)) { + if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide"); + ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT); + return false; + } + if (mShowForced && (hideFlags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) { + if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide"); + ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS); + return false; + } + ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS); + return true; + } + + int getImeShowFlags() { + int flags = 0; + if (mShowForced) { + flags |= InputMethod.SHOW_FORCED | InputMethod.SHOW_EXPLICIT; + } else if (mRequestedShowExplicitly) { + flags |= InputMethod.SHOW_EXPLICIT; + } else { + flags |= InputMethodManager.SHOW_IMPLICIT; + } + return flags; + } + + void clearImeShowFlags() { + mRequestedShowExplicitly = false; + mShowForced = false; + } + + int computeImeDisplayId(@NonNull ImeTargetWindowState state, int displayId) { + final int displayToShowIme = computeImeDisplayIdForTarget(displayId, mImeDisplayValidator); + state.setImeDisplayId(displayToShowIme); + final boolean imeHiddenByPolicy = displayToShowIme == INVALID_DISPLAY; + mPolicy.setImeHiddenByDisplayPolicy(imeHiddenByPolicy); + return displayToShowIme; + } + + /** + * Request to show/hide IME from the given window. + * + * @param windowToken The window which requests to show/hide IME. + * @param showIme {@code true} means to show IME, {@code false} otherwise. + * Note that in the computer will take this option to compute the + * visibility state, it could be {@link #STATE_SHOW_IME} or + * {@link #STATE_HIDE_IME}. + */ + void requestImeVisibility(IBinder windowToken, boolean showIme) { + final ImeTargetWindowState state = getOrCreateWindowState(windowToken); + state.setRequestedImeVisible(showIme); + setWindowState(windowToken, state); + } + + ImeTargetWindowState getOrCreateWindowState(IBinder windowToken) { + ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); + if (state == null) { + state = new ImeTargetWindowState(SOFT_INPUT_STATE_UNSPECIFIED, false, false); + } + return state; + } + + ImeTargetWindowState getWindowStateOrNull(IBinder windowToken) { + ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); + return state; + } + + void setRequestImeTokenToWindow(IBinder windowToken, IBinder token) { + ImeTargetWindowState state = getWindowStateOrNull(windowToken); + if (state != null) { + state.setRequestImeToken(token); + setWindowState(windowToken, state); + } + } + + void setWindowState(IBinder windowToken, ImeTargetWindowState newState) { + if (DEBUG) Slog.d(TAG, "setWindowState, windowToken=" + windowToken + + ", state=" + newState); + mRequestWindowStateMap.put(windowToken, newState); + } + + IBinder getWindowTokenFrom(IBinder requestImeToken) { + for (IBinder windowToken : mRequestWindowStateMap.keySet()) { + final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); + if (state.getRequestImeToken() == requestImeToken) { + return windowToken; + } + } + // Fallback to the focused window for some edge cases (e.g. relaunching the activity) + return mService.mCurFocusedWindow; + } + + IBinder getWindowTokenFrom(ImeTargetWindowState windowState) { + for (IBinder windowToken : mRequestWindowStateMap.keySet()) { + final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); + if (state == windowState) { + return windowToken; + } + } + return null; + } + + boolean shouldRestoreImeVisibility(@NonNull ImeTargetWindowState state) { + final int softInputMode = state.getSoftInputModeState(); + switch (softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE) { + case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN: + return false; + case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN: + if ((softInputMode & SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) { + return false; + } + } + return mWindowManagerInternal.shouldRestoreImeVisibility(getWindowTokenFrom(state)); + } + + void dumpDebug(ProtoOutputStream proto, long fieldId) { + proto.write(SHOW_EXPLICITLY_REQUESTED, mRequestedShowExplicitly); + proto.write(SHOW_FORCED, mShowForced); + proto.write(ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD, + mPolicy.isA11yRequestNoSoftKeyboard()); + } + + void dump(PrintWriter pw) { + final Printer p = new PrintWriterPrinter(pw); + p.println(" mRequestedShowExplicitly=" + mRequestedShowExplicitly + + " mShowForced=" + mShowForced); + p.println(" mImeHiddenByDisplayPolicy=" + mPolicy.isImeHiddenByDisplayPolicy()); + } + + /** + * A settings class to manage all IME related visibility policies or settings. + * + * This is used for the visibility computer to manage and tell + * {@link InputMethodManagerService} if the requested IME visibility is valid from + * application call or the focus window. + */ + static class ImeVisibilityPolicy { + /** + * {@code true} if the Ime policy has been set to + * {@link WindowManager#DISPLAY_IME_POLICY_HIDE}. + * + * This prevents the IME from showing when it otherwise may have shown. + */ + private boolean mImeHiddenByDisplayPolicy; + + /** + * Set when the accessibility service requests to hide IME by + * {@link AccessibilityService.SoftKeyboardController#setShowMode} + */ + private boolean mA11yRequestingNoSoftKeyboard; + + void setImeHiddenByDisplayPolicy(boolean hideIme) { + mImeHiddenByDisplayPolicy = hideIme; + } + + boolean isImeHiddenByDisplayPolicy() { + return mImeHiddenByDisplayPolicy; + } + + void setA11yRequestNoSoftKeyboard(int keyboardShowMode) { + mA11yRequestingNoSoftKeyboard = + (keyboardShowMode & AccessibilityService.SHOW_MODE_MASK) == SHOW_MODE_HIDDEN; + } + + boolean isA11yRequestNoSoftKeyboard() { + return mA11yRequestingNoSoftKeyboard; + } + } + + ImeVisibilityPolicy getImePolicy() { + return mPolicy; + } + + /** + * A class that represents the current state of the IME target window. + */ + static class ImeTargetWindowState { + ImeTargetWindowState(@SoftInputModeFlags int softInputModeState, boolean imeFocusChanged, + boolean hasFocusedEditor) { + mSoftInputModeState = softInputModeState; + mImeFocusChanged = imeFocusChanged; + mHasFocusedEditor = hasFocusedEditor; + } + + /** + * Visibility state for this window. By default no state has been specified. + */ + private final @SoftInputModeFlags int mSoftInputModeState; + + /** + * {@code true} means the IME focus changed from the previous window, {@code false} + * otherwise. + */ + private final boolean mImeFocusChanged; + + /** + * {@code true} when the window has focused an editor, {@code false} otherwise. + */ + private final boolean mHasFocusedEditor; + + /** + * Set if the client has asked for the input method to be shown. + */ + private boolean mRequestedImeVisible; + + /** + * A identifier for knowing the requester of {@link InputMethodManager#showSoftInput} or + * {@link InputMethodManager#hideSoftInputFromWindow}. + */ + private IBinder mRequestImeToken; + + /** + * The IME target display id for which the latest startInput was called. + */ + private int mImeDisplayId = DEFAULT_DISPLAY; + + boolean hasImeFocusChanged() { + return mImeFocusChanged; + } + + boolean hasEdiorFocused() { + return mHasFocusedEditor; + } + + int getSoftInputModeState() { + return mSoftInputModeState; + } + + private void setImeDisplayId(int imeDisplayId) { + mImeDisplayId = imeDisplayId; + } + + int getImeDisplayId() { + return mImeDisplayId; + } + + private void setRequestedImeVisible(boolean requestedImeVisible) { + mRequestedImeVisible = requestedImeVisible; + } + + boolean isRequestedImeVisible() { + return mRequestedImeVisible; + } + + void setRequestImeToken(IBinder token) { + mRequestImeToken = token; + } + + IBinder getRequestImeToken() { + return mRequestImeToken; + } + + @Override + public String toString() { + return "ImeTargetWindowState{ imeToken " + mRequestImeToken + + " imeFocusChanged " + mImeFocusChanged + + " hasEditorFocused " + mHasFocusedEditor + + " requestedImeVisible " + mRequestedImeVisible + + " imeDisplayId " + mImeDisplayId + + " softInputModeState " + softInputModeToString(mSoftInputModeState) + + "}"; + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 5b9a6639bff6..2dbbb1085bb1 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -20,7 +20,6 @@ import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL; import static android.os.IServiceManager.DUMP_FLAG_PROTO; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.server.inputmethod.InputMethodManagerServiceProto.ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD; import static android.server.inputmethod.InputMethodManagerServiceProto.BACK_DISPOSITION; import static android.server.inputmethod.InputMethodManagerServiceProto.BOUND_TO_METHOD; import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_ATTRIBUTE; @@ -39,8 +38,6 @@ import static android.server.inputmethod.InputMethodManagerServiceProto.IN_FULLS import static android.server.inputmethod.InputMethodManagerServiceProto.IS_INTERACTIVE; import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_IME_TARGET_WINDOW_NAME; import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_SWITCH_USER_ID; -import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_EXPLICITLY_REQUESTED; -import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_FORCED; import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_IME_WITH_HARD_KEYBOARD; import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_REQUESTED; import static android.server.inputmethod.InputMethodManagerServiceProto.SYSTEM_READY; @@ -48,17 +45,14 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE; import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; -import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY; -import static com.android.server.EventLogTags.IMF_HIDE_IME; -import static com.android.server.EventLogTags.IMF_SHOW_IME; +import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState; import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT; import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.Manifest; -import android.accessibilityservice.AccessibilityService; import android.annotation.AnyThread; import android.annotation.BinderThread; import android.annotation.DrawableRes; @@ -126,7 +120,6 @@ import android.view.DisplayInfo; import android.view.InputChannel; import android.view.InputDevice; import android.view.MotionEvent; -import android.view.View; import android.view.WindowManager; import android.view.WindowManager.DisplayImePolicy; import android.view.WindowManager.LayoutParams; @@ -299,6 +292,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull private final InputMethodBindingController mBindingController; @NonNull private final AutofillSuggestionsController mAutofillController; + @GuardedBy("ImfLock.class") + @NonNull private final ImeVisibilityStateComputer mVisibilityStateComputer; + + @GuardedBy("ImfLock.class") + @NonNull private final DefaultImeVisibilityApplier mVisibilityApplier; + /** * Cache the result of {@code LocalServices.getService(AudioManagerInternal.class)}. * @@ -530,13 +529,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } /** - * {@code true} if the Ime policy has been set to {@link WindowManager#DISPLAY_IME_POLICY_HIDE}. - * - * This prevents the IME from showing when it otherwise may have shown. - */ - boolean mImeHiddenByDisplayPolicy; - - /** * The client that is currently bound to an input method. */ private ClientState mCurClient; @@ -638,16 +630,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private boolean mShowRequested; /** - * Set if we were explicitly told to show the input method. - */ - boolean mShowExplicitlyRequested; - - /** - * Set if we were forced to be shown. - */ - boolean mShowForced; - - /** * Set if we last told the input method to show itself. */ private boolean mInputShown; @@ -709,8 +691,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ private static final int FALLBACK_DISPLAY_ID = DEFAULT_DISPLAY; - final ImeDisplayValidator mImeDisplayValidator; - /** * If non-null, this is the input method service we are currently connected * to. @@ -786,7 +766,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub int mImeWindowVis; private LocaleList mLastSystemLocales; - private boolean mAccessibilityRequestingNoSoftKeyboard; private final MyPackageMonitor mMyPackageMonitor = new MyPackageMonitor(); private final String mSlotIme; @@ -974,22 +953,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } /** - * Map of generated token to windowToken that is requesting - * {@link InputMethodManager#showSoftInput(View, int)}. - * This map tracks origin of showSoftInput requests. - */ - @GuardedBy("ImfLock.class") - private final WeakHashMap<IBinder, IBinder> mShowRequestWindowMap = new WeakHashMap<>(); - - /** - * Map of generated token to windowToken that is requesting - * {@link InputMethodManager#hideSoftInputFromWindow(IBinder, int)}. - * This map tracks origin of hideSoftInput requests. - */ - @GuardedBy("ImfLock.class") - private final WeakHashMap<IBinder, IBinder> mHideRequestWindowMap = new WeakHashMap<>(); - - /** * A ring buffer to store the history of {@link StartInputInfo}. */ private static final class StartInputHistory { @@ -1207,11 +1170,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } else if (accessibilityRequestingNoImeUri.equals(uri)) { final int accessibilitySoftKeyboardSetting = Settings.Secure.getIntForUser( mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0 /* def */, mUserId); - mAccessibilityRequestingNoSoftKeyboard = - (accessibilitySoftKeyboardSetting & AccessibilityService.SHOW_MODE_MASK) - == AccessibilityService.SHOW_MODE_HIDDEN; - if (mAccessibilityRequestingNoSoftKeyboard) { + Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0, mUserId); + mVisibilityStateComputer.getImePolicy().setA11yRequestNoSoftKeyboard( + accessibilitySoftKeyboardSetting); + if (mVisibilityStateComputer.getImePolicy().isA11yRequestNoSoftKeyboard()) { final boolean showRequested = mShowRequested; hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, null /* resultReceiver */, @@ -1722,7 +1684,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); mImePlatformCompatUtils = new ImePlatformCompatUtils(); mInputMethodDeviceConfigs = new InputMethodDeviceConfigs(); - mImeDisplayValidator = mWindowManagerInternal::getDisplayImePolicy; mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); @@ -1747,6 +1708,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ? bindingControllerForTesting : new InputMethodBindingController(this); mAutofillController = new AutofillSuggestionsController(this); + + mVisibilityStateComputer = new ImeVisibilityStateComputer(this); + mVisibilityApplier = new DefaultImeVisibilityApplier(this); + mPreventImeStartupUnlessTextEditor = mRes.getBoolean( com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor); mNonPreemptibleInputMethods = mRes.getStringArray( @@ -2340,29 +2305,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @GuardedBy("ImfLock.class") - private int getImeShowFlagsLocked() { - int flags = 0; - if (mShowForced) { - flags |= InputMethod.SHOW_FORCED - | InputMethod.SHOW_EXPLICIT; - } else if (mShowExplicitlyRequested) { - flags |= InputMethod.SHOW_EXPLICIT; - } - return flags; - } - - @GuardedBy("ImfLock.class") - private int getAppShowFlagsLocked() { - int flags = 0; - if (mShowForced) { - flags |= InputMethodManager.SHOW_FORCED; - } else if (!mShowExplicitlyRequested) { - flags |= InputMethodManager.SHOW_IMPLICIT; - } - return flags; - } - - @GuardedBy("ImfLock.class") @NonNull InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial) { if (!mBoundToMethod) { @@ -2403,7 +2345,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Re-use current statsToken, if it exists. final ImeTracker.Token statsToken = mCurStatsToken; mCurStatsToken = null; - showCurrentInputLocked(mCurFocusedWindow, statsToken, getAppShowFlagsLocked(), + showCurrentInputLocked(mCurFocusedWindow, statsToken, + mVisibilityStateComputer.getImeShowFlags(), null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT); } @@ -2518,17 +2461,20 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Compute the final shown display ID with validated cs.selfReportedDisplayId for this // session & other conditions. - mDisplayIdToShowIme = computeImeDisplayIdForTarget(cs.mSelfReportedDisplayId, - mImeDisplayValidator); + ImeTargetWindowState winState = mVisibilityStateComputer.getWindowStateOrNull( + mCurFocusedWindow); + if (winState == null) { + return InputBindResult.NOT_IME_TARGET_WINDOW; + } + final int csDisplayId = cs.mSelfReportedDisplayId; + mDisplayIdToShowIme = mVisibilityStateComputer.computeImeDisplayId(winState, csDisplayId); - if (mDisplayIdToShowIme == INVALID_DISPLAY) { - mImeHiddenByDisplayPolicy = true; + if (mVisibilityStateComputer.getImePolicy().isImeHiddenByDisplayPolicy()) { hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, null /* resultReceiver */, SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE); return InputBindResult.NO_IME; } - mImeHiddenByDisplayPolicy = false; if (mCurClient != cs) { prepareClientSwitchLocked(cs); @@ -3385,6 +3331,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } + @GuardedBy("ImfLock.class") + void setRequestImeTokenToWindow(IBinder windowToken, IBinder token) { + mVisibilityStateComputer.setRequestImeTokenToWindow(windowToken, token); + } + @BinderThread @Override public void reportPerceptibleAsync(IBinder windowToken, boolean perceptible) { @@ -3419,18 +3370,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ImeTracker.ORIGIN_SERVER_START_INPUT, reason); } + // TODO(b/246309664): make mShowRequested as per-window state. mShowRequested = true; - if (mAccessibilityRequestingNoSoftKeyboard || mImeHiddenByDisplayPolicy) { - ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY); - return false; - } - ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY); - if ((flags & InputMethodManager.SHOW_FORCED) != 0) { - mShowExplicitlyRequested = true; - mShowForced = true; - } else if ((flags & InputMethodManager.SHOW_IMPLICIT) == 0) { - mShowExplicitlyRequested = true; + if (!mVisibilityStateComputer.onImeShowFlags(statsToken, flags)) { + return false; } if (!mSystemReady) { @@ -3439,39 +3383,25 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY); + mVisibilityStateComputer.requestImeVisibility(windowToken, true); + + // Ensure binding the connection when IME is going to show. mBindingController.setCurrentMethodVisible(); final IInputMethodInvoker curMethod = getCurMethodLocked(); + ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); if (curMethod != null) { - // create a placeholder token for IMS so that IMS cannot inject windows into client app. - Binder showInputToken = new Binder(); - mShowRequestWindowMap.put(showInputToken, windowToken); - ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME); mCurStatsToken = null; - final int showFlags = getImeShowFlagsLocked(); - if (DEBUG) { - Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken - + ", " + showFlags + ", " + resultReceiver + ") for reason: " - + InputMethodDebug.softInputDisplayReasonToString(reason)); - } if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) { curMethod.updateEditorToolType(lastClickToolType); } - // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. - if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) { - if (DEBUG_IME_VISIBILITY) { - EventLog.writeEvent(IMF_SHOW_IME, statsToken.getTag(), - Objects.toString(mCurFocusedWindow), - InputMethodDebug.softInputDisplayReasonToString(reason), - InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode)); - } - onShowHideSoftInputRequested(true /* show */, windowToken, reason, statsToken); - } + mVisibilityApplier.performShowIme(windowToken, statsToken, + mVisibilityStateComputer.getImeShowFlags(), resultReceiver, reason); + // TODO(b/246309664): make mInputShown tracked by the Ime visibility computer. mInputShown = true; return true; } else { - ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_WAIT_IME); mCurStatsToken = statsToken; } @@ -3527,20 +3457,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason); } - if ((flags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0 - && (mShowExplicitlyRequested || mShowForced)) { - if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide"); - ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT); + if (!mVisibilityStateComputer.canHideIme(statsToken, flags)) { return false; } - ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT); - - if (mShowForced && (flags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) { - if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide"); - ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS); - return false; - } - ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS); // There is a chance that IMM#hideSoftInput() is called in a transient state where // IMMS#InputShown is already updated to be true whereas IMMS#mImeWindowVis is still waiting @@ -3549,49 +3468,30 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // application process as a valid request, and have even promised such a behavior with CTS // since Android Eclair. That's why we need to accept IMM#hideSoftInput() even when only // IMMS#InputShown indicates that the software keyboard is shown. - // TODO: Clean up, IMMS#mInputShown, IMMS#mImeWindowVis and mShowRequested. + // TODO(b/246309664): Clean up, IMMS#mInputShown, IMMS#mImeWindowVis and mShowRequested. IInputMethodInvoker curMethod = getCurMethodLocked(); final boolean shouldHideSoftInput = (curMethod != null) && (mInputShown || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0); - boolean res; + + mVisibilityStateComputer.requestImeVisibility(windowToken, false); if (shouldHideSoftInput) { - final Binder hideInputToken = new Binder(); - mHideRequestWindowMap.put(hideInputToken, windowToken); // The IME will report its visible state again after the following message finally // delivered to the IME process as an IPC. Hence the inconsistency between // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in // the final state. ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE); - if (DEBUG) { - Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken - + ", " + resultReceiver + ") for reason: " - + InputMethodDebug.softInputDisplayReasonToString(reason)); - } - // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. - if (curMethod.hideSoftInput(hideInputToken, statsToken, 0 /* flags */, - resultReceiver)) { - if (DEBUG_IME_VISIBILITY) { - EventLog.writeEvent(IMF_HIDE_IME, statsToken.getTag(), - Objects.toString(mCurFocusedWindow), - InputMethodDebug.softInputDisplayReasonToString(reason), - InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode)); - } - onShowHideSoftInputRequested(false /* show */, windowToken, reason, statsToken); - } - res = true; + mVisibilityApplier.performHideIme(windowToken, statsToken, resultReceiver, reason); } else { ImeTracker.get().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE); - res = false; } mBindingController.setCurrentMethodNotVisible(); + mVisibilityStateComputer.clearImeShowFlags(); mInputShown = false; mShowRequested = false; - mShowExplicitlyRequested = false; - mShowForced = false; // Cancel existing statsToken for show IME as we got a hide request. ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); mCurStatsToken = null; - return res; + return shouldHideSoftInput; } private boolean isImeClientFocused(IBinder windowToken, ClientState cs) { @@ -3738,8 +3638,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // In case mShowForced flag affects the next client to keep IME visible, when the current // client is leaving due to the next focused client, we clear mShowForced flag when the // next client's targetSdkVersion is T or higher. - if (mCurFocusedWindow != windowToken && mShowForced && shouldClearFlag) { - mShowForced = false; + final boolean showForced = mVisibilityStateComputer.mShowForced; + if (mCurFocusedWindow != windowToken && showForced && shouldClearFlag) { + mVisibilityStateComputer.mShowForced = false; } // cross-profile access is always allowed here to allow profile-switching. @@ -3763,6 +3664,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final boolean startInputByWinGainedFocus = (startInputFlags & StartInputFlags.WINDOW_GAINED_FOCUS) != 0; + // Init the focused window state (e.g. whether the editor has focused or IME focus has + // changed from another window). + final ImeTargetWindowState windowState = new ImeTargetWindowState( + softInputMode, !sameWindowFocused, isTextEditor); + mVisibilityStateComputer.setWindowState(windowToken, windowState); + if (sameWindowFocused && isTextEditor) { if (DEBUG) { Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client @@ -3812,7 +3719,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Because the app might leverage these flags to hide soft-keyboard with showing their own // UI for input. if (isTextEditor && editorInfo != null - && shouldRestoreImeVisibility(windowToken, softInputMode)) { + && mVisibilityStateComputer.shouldRestoreImeVisibility(windowState)) { if (DEBUG) Slog.v(TAG, "Will show input to restore visibility"); res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, @@ -4001,19 +3908,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return true; } - private boolean shouldRestoreImeVisibility(IBinder windowToken, - @SoftInputModeFlags int softInputMode) { - switch (softInputMode & LayoutParams.SOFT_INPUT_MASK_STATE) { - case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN: - return false; - case LayoutParams.SOFT_INPUT_STATE_HIDDEN: - if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) { - return false; - } - } - return mWindowManagerInternal.shouldRestoreImeVisibility(windowToken); - } - @GuardedBy("ImfLock.class") private boolean canShowInputMethodPickerLocked(IInputMethodClient client) { final int uid = Binder.getCallingUid(); @@ -4746,8 +4640,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } proto.write(CUR_ID, getCurIdLocked()); proto.write(SHOW_REQUESTED, mShowRequested); - proto.write(SHOW_EXPLICITLY_REQUESTED, mShowExplicitlyRequested); - proto.write(SHOW_FORCED, mShowForced); + mVisibilityStateComputer.dumpDebug(proto, fieldId); proto.write(INPUT_SHOWN, mInputShown); proto.write(IN_FULLSCREEN_MODE, mInFullscreenMode); proto.write(CUR_TOKEN, Objects.toString(getCurTokenLocked())); @@ -4760,8 +4653,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub proto.write(BACK_DISPOSITION, mBackDisposition); proto.write(IME_WINDOW_VISIBILITY, mImeWindowVis); proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard()); - proto.write(ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD, - mAccessibilityRequestingNoSoftKeyboard); proto.end(token); } } @@ -4795,25 +4686,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); return; } - if (!setVisible) { - if (mCurClient != null) { - ImeTracker.get().onProgress(statsToken, - ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); - - mWindowManagerInternal.hideIme( - mHideRequestWindowMap.get(windowToken), - mCurClient.mSelfReportedDisplayId, statsToken); - } else { - ImeTracker.get().onFailed(statsToken, - ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); - } - } else { - ImeTracker.get().onProgress(statsToken, - ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); - // Send to window manager to show IME after IME layout finishes. - mWindowManagerInternal.showImePostLayout(mShowRequestWindowMap.get(windowToken), - statsToken); - } + final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(windowToken); + mVisibilityApplier.applyImeVisibility(requestToken, statsToken, + setVisible ? ImeVisibilityStateComputer.STATE_SHOW_IME + : ImeVisibilityStateComputer.STATE_HIDE_IME); } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } @@ -4857,7 +4733,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub /** Called right after {@link com.android.internal.inputmethod.IInputMethod#showSoftInput}. */ @GuardedBy("ImfLock.class") - private void onShowHideSoftInputRequested(boolean show, IBinder requestToken, + void onShowHideSoftInputRequested(boolean show, IBinder requestToken, @SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken) { final WindowManagerInternal.ImeTargetInfo info = mWindowManagerInternal.onToggleImeRequested( @@ -5988,14 +5864,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub method = getCurMethodLocked(); p.println(" mCurMethod=" + getCurMethodLocked()); p.println(" mEnabledSession=" + mEnabledSession); - p.println(" mShowRequested=" + mShowRequested - + " mShowExplicitlyRequested=" + mShowExplicitlyRequested - + " mShowForced=" + mShowForced - + " mInputShown=" + mInputShown); + p.println(" mShowRequested=" + mShowRequested + " mInputShown=" + mInputShown); + mVisibilityStateComputer.dump(pw); p.println(" mInFullscreenMode=" + mInFullscreenMode); p.println(" mSystemReady=" + mSystemReady + " mInteractive=" + mIsInteractive); p.println(" mSettingsObserver=" + mSettingsObserver); - p.println(" mImeHiddenByDisplayPolicy=" + mImeHiddenByDisplayPolicy); p.println(" mStylusIds=" + (mStylusIds != null ? Arrays.toString(mStylusIds.toArray()) : "")); p.println(" mSwitchingController:"); diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java index e1a990d0f6bd..c90554d9cdd8 100644 --- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java +++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java @@ -59,17 +59,9 @@ class BluetoothRouteProvider { private static final String HEARING_AID_ROUTE_ID_PREFIX = "HEARING_AID_"; private static final String LE_AUDIO_ROUTE_ID_PREFIX = "LE_AUDIO_"; - @SuppressWarnings("WeakerAccess") /* synthetic access */ // Maps hardware address to BluetoothRouteInfo - final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>(); - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final List<BluetoothRouteInfo> mActiveRoutes = new ArrayList<>(); - @SuppressWarnings("WeakerAccess") /* synthetic access */ - BluetoothA2dp mA2dpProfile; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - BluetoothHearingAid mHearingAidProfile; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - BluetoothLeAudio mLeAudioProfile; + private final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>(); + private final List<BluetoothRouteInfo> mActiveRoutes = new ArrayList<>(); // Route type -> volume map private final SparseIntArray mVolumeMap = new SparseIntArray(); @@ -83,6 +75,10 @@ class BluetoothRouteProvider { private final BroadcastReceiver mBroadcastReceiver = new BluetoothBroadcastReceiver(); private final BluetoothProfileListener mProfileListener = new BluetoothProfileListener(); + private BluetoothA2dp mA2dpProfile; + private BluetoothHearingAid mHearingAidProfile; + private BluetoothLeAudio mLeAudioProfile; + /** * Create an instance of {@link BluetoothRouteProvider}. * It may return {@code null} if Bluetooth is not supported on this hardware platform. @@ -109,7 +105,7 @@ class BluetoothRouteProvider { buildBluetoothRoutes(); } - public void start(UserHandle user) { + void start(UserHandle user) { mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.LE_AUDIO); @@ -133,7 +129,7 @@ class BluetoothRouteProvider { mIntentFilter, null, null); } - public void stop() { + void stop() { mContext.unregisterReceiver(mBroadcastReceiver); } @@ -144,7 +140,7 @@ class BluetoothRouteProvider { * @param routeId the id of the Bluetooth device. {@code null} denotes to clear the use of * BT routes. */ - public void transferTo(@Nullable String routeId) { + void transferTo(@Nullable String routeId) { if (routeId == null) { clearActiveDevices(); return; @@ -158,7 +154,7 @@ class BluetoothRouteProvider { } if (mBluetoothAdapter != null) { - mBluetoothAdapter.setActiveDevice(btRouteInfo.btDevice, ACTIVE_DEVICE_AUDIO); + mBluetoothAdapter.setActiveDevice(btRouteInfo.mBtDevice, ACTIVE_DEVICE_AUDIO); } } @@ -183,7 +179,7 @@ class BluetoothRouteProvider { for (BluetoothDevice device : bondedDevices) { if (device.isConnected()) { BluetoothRouteInfo newBtRoute = createBluetoothRoute(device); - if (newBtRoute.connectedProfiles.size() > 0) { + if (newBtRoute.mConnectedProfiles.size() > 0) { mBluetoothRoutes.put(device.getAddress(), newBtRoute); } } @@ -195,14 +191,14 @@ class BluetoothRouteProvider { MediaRoute2Info getSelectedRoute() { // For now, active routes can be multiple only when a pair of hearing aid devices is active. // Let the first active device represent them. - return (mActiveRoutes.isEmpty() ? null : mActiveRoutes.get(0).route); + return (mActiveRoutes.isEmpty() ? null : mActiveRoutes.get(0).mRoute); } @NonNull List<MediaRoute2Info> getTransferableRoutes() { List<MediaRoute2Info> routes = getAllBluetoothRoutes(); for (BluetoothRouteInfo btRoute : mActiveRoutes) { - routes.remove(btRoute.route); + routes.remove(btRoute.mRoute); } return routes; } @@ -220,11 +216,11 @@ class BluetoothRouteProvider { for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) { // A pair of hearing aid devices or having the same hardware address - if (routeIds.contains(btRoute.route.getId())) { + if (routeIds.contains(btRoute.mRoute.getId())) { continue; } - routes.add(btRoute.route); - routeIds.add(btRoute.route.getId()); + routes.add(btRoute.mRoute); + routeIds.add(btRoute.mRoute.getId()); } return routes; } @@ -234,7 +230,7 @@ class BluetoothRouteProvider { return null; } for (BluetoothRouteInfo btRouteInfo : mBluetoothRoutes.values()) { - if (TextUtils.equals(btRouteInfo.route.getId(), routeId)) { + if (TextUtils.equals(btRouteInfo.mRoute.getId(), routeId)) { return btRouteInfo; } } @@ -246,7 +242,7 @@ class BluetoothRouteProvider { * * @return true if devices can be handled by the provider. */ - public boolean updateVolumeForDevices(int devices, int volume) { + boolean updateVolumeForDevices(int devices, int volume) { int routeType; if ((devices & (AudioSystem.DEVICE_OUT_HEARING_AID)) != 0) { routeType = MediaRoute2Info.TYPE_HEARING_AID; @@ -263,10 +259,10 @@ class BluetoothRouteProvider { boolean shouldNotify = false; for (BluetoothRouteInfo btRoute : mActiveRoutes) { - if (btRoute.route.getType() != routeType) { + if (btRoute.mRoute.getType() != routeType) { continue; } - btRoute.route = new MediaRoute2Info.Builder(btRoute.route) + btRoute.mRoute = new MediaRoute2Info.Builder(btRoute.mRoute) .setVolume(volume) .build(); shouldNotify = true; @@ -285,7 +281,7 @@ class BluetoothRouteProvider { private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) { BluetoothRouteInfo newBtRoute = new BluetoothRouteInfo(); - newBtRoute.btDevice = device; + newBtRoute.mBtDevice = device; String routeId = device.getAddress(); String deviceName = device.getName(); @@ -293,26 +289,26 @@ class BluetoothRouteProvider { deviceName = mContext.getResources().getText(R.string.unknownName).toString(); } int type = MediaRoute2Info.TYPE_BLUETOOTH_A2DP; - newBtRoute.connectedProfiles = new SparseBooleanArray(); + newBtRoute.mConnectedProfiles = new SparseBooleanArray(); if (mA2dpProfile != null && mA2dpProfile.getConnectedDevices().contains(device)) { - newBtRoute.connectedProfiles.put(BluetoothProfile.A2DP, true); + newBtRoute.mConnectedProfiles.put(BluetoothProfile.A2DP, true); } if (mHearingAidProfile != null && mHearingAidProfile.getConnectedDevices().contains(device)) { - newBtRoute.connectedProfiles.put(BluetoothProfile.HEARING_AID, true); + newBtRoute.mConnectedProfiles.put(BluetoothProfile.HEARING_AID, true); // Intentionally assign the same ID for a pair of devices to publish only one of them. routeId = HEARING_AID_ROUTE_ID_PREFIX + mHearingAidProfile.getHiSyncId(device); type = MediaRoute2Info.TYPE_HEARING_AID; } if (mLeAudioProfile != null && mLeAudioProfile.getConnectedDevices().contains(device)) { - newBtRoute.connectedProfiles.put(BluetoothProfile.LE_AUDIO, true); + newBtRoute.mConnectedProfiles.put(BluetoothProfile.LE_AUDIO, true); routeId = LE_AUDIO_ROUTE_ID_PREFIX + mLeAudioProfile.getGroupId(device); type = MediaRoute2Info.TYPE_BLE_HEADSET; } // Current volume will be set when connected. - newBtRoute.route = new MediaRoute2Info.Builder(routeId, deviceName) + newBtRoute.mRoute = new MediaRoute2Info.Builder(routeId, deviceName) .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO) .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK) .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED) @@ -332,18 +328,18 @@ class BluetoothRouteProvider { Slog.w(TAG, "setRouteConnectionState: route shouldn't be null"); return; } - if (btRoute.route.getConnectionState() == state) { + if (btRoute.mRoute.getConnectionState() == state) { return; } - MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(btRoute.route) + MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(btRoute.mRoute) .setConnectionState(state); builder.setType(btRoute.getRouteType()); if (state == MediaRoute2Info.CONNECTION_STATE_CONNECTED) { builder.setVolume(mVolumeMap.get(btRoute.getRouteType(), 0)); } - btRoute.route = builder.build(); + btRoute.mRoute = builder.build(); } private void addActiveRoute(BluetoothRouteInfo btRoute) { @@ -352,7 +348,7 @@ class BluetoothRouteProvider { return; } if (DEBUG) { - Log.d(TAG, "Adding active route: " + btRoute.route); + Log.d(TAG, "Adding active route: " + btRoute.mRoute); } if (mActiveRoutes.contains(btRoute)) { Slog.w(TAG, "addActiveRoute: btRoute is already added."); @@ -364,7 +360,7 @@ class BluetoothRouteProvider { private void removeActiveRoute(BluetoothRouteInfo btRoute) { if (DEBUG) { - Log.d(TAG, "Removing active route: " + btRoute.route); + Log.d(TAG, "Removing active route: " + btRoute.mRoute); } if (mActiveRoutes.remove(btRoute)) { setRouteConnectionState(btRoute, STATE_DISCONNECTED); @@ -378,7 +374,7 @@ class BluetoothRouteProvider { Iterator<BluetoothRouteInfo> iter = mActiveRoutes.iterator(); while (iter.hasNext()) { BluetoothRouteInfo btRoute = iter.next(); - if (btRoute.route.getType() == type) { + if (btRoute.mRoute.getType() == type) { iter.remove(); setRouteConnectionState(btRoute, STATE_DISCONNECTED); } @@ -398,9 +394,9 @@ class BluetoothRouteProvider { // A bluetooth route with the same route ID should be added. for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) { - if (TextUtils.equals(btRoute.route.getId(), activeBtRoute.route.getId()) - && !TextUtils.equals(btRoute.btDevice.getAddress(), - activeBtRoute.btDevice.getAddress())) { + if (TextUtils.equals(btRoute.mRoute.getId(), activeBtRoute.mRoute.getId()) + && !TextUtils.equals(btRoute.mBtDevice.getAddress(), + activeBtRoute.mBtDevice.getAddress())) { addActiveRoute(btRoute); } } @@ -425,19 +421,19 @@ class BluetoothRouteProvider { void onBluetoothRoutesUpdated(@NonNull List<MediaRoute2Info> routes); } - private class BluetoothRouteInfo { - public BluetoothDevice btDevice; - public MediaRoute2Info route; - public SparseBooleanArray connectedProfiles; + private static class BluetoothRouteInfo { + private BluetoothDevice mBtDevice; + private MediaRoute2Info mRoute; + private SparseBooleanArray mConnectedProfiles; @MediaRoute2Info.Type int getRouteType() { // Let hearing aid profile have a priority. - if (connectedProfiles.get(BluetoothProfile.HEARING_AID, false)) { + if (mConnectedProfiles.get(BluetoothProfile.HEARING_AID, false)) { return MediaRoute2Info.TYPE_HEARING_AID; } - if (connectedProfiles.get(BluetoothProfile.LE_AUDIO, false)) { + if (mConnectedProfiles.get(BluetoothProfile.LE_AUDIO, false)) { return MediaRoute2Info.TYPE_BLE_HEADSET; } @@ -447,6 +443,7 @@ class BluetoothRouteProvider { // These callbacks run on the main thread. private final class BluetoothProfileListener implements BluetoothProfile.ServiceListener { + @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { List<BluetoothDevice> activeDevices; switch (profile) { @@ -480,6 +477,7 @@ class BluetoothRouteProvider { notifyBluetoothRoutesUpdated(); } + @Override public void onServiceDisconnected(int profile) { switch (profile) { case BluetoothProfile.A2DP: @@ -496,6 +494,7 @@ class BluetoothRouteProvider { } } } + private class BluetoothBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -514,6 +513,7 @@ class BluetoothRouteProvider { } private class AdapterStateChangedReceiver implements BluetoothEventReceiver { + @Override public void onReceive(Context context, Intent intent, BluetoothDevice device) { int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); if (state == BluetoothAdapter.STATE_OFF @@ -573,18 +573,18 @@ class BluetoothRouteProvider { if (state == BluetoothProfile.STATE_CONNECTED) { if (btRoute == null) { btRoute = createBluetoothRoute(device); - if (btRoute.connectedProfiles.size() > 0) { + if (btRoute.mConnectedProfiles.size() > 0) { mBluetoothRoutes.put(device.getAddress(), btRoute); notifyBluetoothRoutesUpdated(); } } else { - btRoute.connectedProfiles.put(profile, true); + btRoute.mConnectedProfiles.put(profile, true); } } else if (state == BluetoothProfile.STATE_DISCONNECTING || state == BluetoothProfile.STATE_DISCONNECTED) { if (btRoute != null) { - btRoute.connectedProfiles.delete(profile); - if (btRoute.connectedProfiles.size() == 0) { + btRoute.mConnectedProfiles.delete(profile); + if (btRoute.mConnectedProfiles.size() == 0) { removeActiveRoute(mBluetoothRoutes.remove(device.getAddress())); notifyBluetoothRoutesUpdated(); } diff --git a/services/core/java/com/android/server/NetworkManagementInternal.java b/services/core/java/com/android/server/net/NetworkManagementInternal.java index f53c454cb917..492696078e55 100644 --- a/services/core/java/com/android/server/NetworkManagementInternal.java +++ b/services/core/java/com/android/server/net/NetworkManagementInternal.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.net; /** * NetworkManagement local system service interface. diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/net/NetworkManagementService.java index fc26f0989f45..acfa66595afa 100644 --- a/services/core/java/com/android/server/NetworkManagementService.java +++ b/services/core/java/com/android/server/net/NetworkManagementService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.net; import static android.Manifest.permission.CONNECTIVITY_INTERNAL; import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE; @@ -82,6 +82,8 @@ import com.android.internal.util.HexDump; import com.android.internal.util.Preconditions; import com.android.net.module.util.NetdUtils; import com.android.net.module.util.NetdUtils.ModifyOperation; +import com.android.server.FgThread; +import com.android.server.LocalServices; import com.google.android.collect.Maps; diff --git a/services/core/java/com/android/server/net/TEST_MAPPING b/services/core/java/com/android/server/net/TEST_MAPPING index 4ccf09e5e020..e0376ed6461b 100644 --- a/services/core/java/com/android/server/net/TEST_MAPPING +++ b/services/core/java/com/android/server/net/TEST_MAPPING @@ -15,7 +15,7 @@ "presubmit": [ { "name": "FrameworksServicesTests", - "file_patterns": ["(/|^)NetworkPolicy[^/]*\\.java"], + "file_patterns": ["(/|^)Network(Policy|Management)[^/]*\\.java"], "options": [ { "include-filter": "com.android.server.net." diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java index abaff4afeeea..d252d409732a 100644 --- a/services/core/java/com/android/server/pm/AppDataHelper.java +++ b/services/core/java/com/android/server/pm/AppDataHelper.java @@ -616,6 +616,7 @@ public class AppDataHelper { Slog.w(TAG, String.valueOf(e)); } mPm.getDexManager().notifyPackageDataDestroyed(pkg.getPackageName(), userId); + mPm.getDynamicCodeLogger().notifyPackageDataDestroyed(pkg.getPackageName(), userId); } } diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index 5ff1909b54e4..ac8ff21b7fa6 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -32,6 +32,7 @@ import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_F import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_DELETED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_REPLACED; import static com.android.server.pm.AppsFilterUtils.canQueryAsInstaller; +import static com.android.server.pm.AppsFilterUtils.canQueryAsUpdateOwner; import static com.android.server.pm.AppsFilterUtils.canQueryViaComponents; import static com.android.server.pm.AppsFilterUtils.canQueryViaPackage; import static com.android.server.pm.AppsFilterUtils.canQueryViaUsesLibrary; @@ -670,7 +671,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } } if (canQueryViaPackage(existingPkg, newPkg) - || canQueryAsInstaller(existingSetting, newPkg)) { + || canQueryAsInstaller(existingSetting, newPkg) + || canQueryAsUpdateOwner(existingSetting, newPkg)) { synchronized (mQueriesViaPackageLock) { mQueriesViaPackage.add(existingSetting.getAppId(), newPkgSetting.getAppId()); @@ -697,7 +699,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } } if (canQueryViaPackage(newPkg, existingPkg) - || canQueryAsInstaller(newPkgSetting, existingPkg)) { + || canQueryAsInstaller(newPkgSetting, existingPkg) + || canQueryAsUpdateOwner(newPkgSetting, existingPkg)) { synchronized (mQueriesViaPackageLock) { mQueriesViaPackage.add(newPkgSetting.getAppId(), existingSetting.getAppId()); diff --git a/services/core/java/com/android/server/pm/AppsFilterUtils.java b/services/core/java/com/android/server/pm/AppsFilterUtils.java index 8da1d217b88a..d38b83fa6758 100644 --- a/services/core/java/com/android/server/pm/AppsFilterUtils.java +++ b/services/core/java/com/android/server/pm/AppsFilterUtils.java @@ -91,6 +91,15 @@ final class AppsFilterUtils { return false; } + public static boolean canQueryAsUpdateOwner(PackageStateInternal querying, + AndroidPackage potentialTarget) { + final InstallSource installSource = querying.getInstallSource(); + if (potentialTarget.getPackageName().equals(installSource.mUpdateOwnerPackageName)) { + return true; + } + return false; + } + public static boolean canQueryViaUsesLibrary(AndroidPackage querying, AndroidPackage potentialTarget) { if (potentialTarget.getLibraryNames().isEmpty()) { diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 0eac9ef5e1a2..c6700e62afa0 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -4981,6 +4981,7 @@ public class ComputerEngine implements Computer { String installerPackageName; String initiatingPackageName; String originatingPackageName; + String updateOwnerPackageName; final InstallSource installSource = getInstallSource(packageName, callingUid, userId); if (installSource == null) { @@ -4996,6 +4997,15 @@ public class ComputerEngine implements Computer { } } + updateOwnerPackageName = installSource.mUpdateOwnerPackageName; + if (updateOwnerPackageName != null) { + final PackageStateInternal ps = mSettings.getPackage(updateOwnerPackageName); + if (ps == null + || shouldFilterApplicationIncludingUninstalled(ps, callingUid, userId)) { + updateOwnerPackageName = null; + } + } + if (installSource.mIsInitiatingPackageUninstalled) { // We can't check visibility in the usual way, since the initiating package is no // longer present. So we apply simpler rules to whether to expose the info: @@ -5052,7 +5062,8 @@ public class ComputerEngine implements Computer { } return new InstallSourceInfo(initiatingPackageName, initiatingPackageSigningInfo, - originatingPackageName, installerPackageName, installSource.mPackageSource); + originatingPackageName, installerPackageName, updateOwnerPackageName, + installSource.mPackageSource); } @PackageManager.EnabledState diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index 8757310cce74..cf447a75ea86 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -41,6 +41,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.AppGlobals; +import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; import android.content.pm.SharedLibraryInfo; @@ -84,8 +85,10 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; @@ -416,17 +419,22 @@ public final class DexOptHelper { return true; } + @DexOptResult int dexoptStatus; if (options.isDexoptOnlySecondaryDex()) { - // TODO(b/251903639): Call into ART Service. - try { - return mPm.getDexManager().dexoptSecondaryDex(options); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); + Optional<Integer> artSrvRes = performDexOptWithArtService(options, 0 /* extraFlags */); + if (artSrvRes.isPresent()) { + dexoptStatus = artSrvRes.get(); + } else { + try { + return mPm.getDexManager().dexoptSecondaryDex(options); + } catch (LegacyDexoptDisabledException e) { + throw new RuntimeException(e); + } } } else { - int dexoptStatus = performDexOptWithStatus(options); - return dexoptStatus != PackageDexOptimizer.DEX_OPT_FAILED; + dexoptStatus = performDexOptWithStatus(options); } + return dexoptStatus != PackageDexOptimizer.DEX_OPT_FAILED; } /** @@ -455,7 +463,8 @@ public final class DexOptHelper { // if the package can now be considered up to date for the given filter. @DexOptResult private int performDexOptInternal(DexoptOptions options) { - Optional<Integer> artSrvRes = performDexOptWithArtService(options); + Optional<Integer> artSrvRes = + performDexOptWithArtService(options, ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES); if (artSrvRes.isPresent()) { return artSrvRes.get(); } @@ -492,7 +501,8 @@ public final class DexOptHelper { * @return a {@link DexOptResult}, or empty if the request isn't supported so that it is * necessary to fall back to the legacy code paths. */ - private Optional<Integer> performDexOptWithArtService(DexoptOptions options) { + private Optional<Integer> performDexOptWithArtService(DexoptOptions options, + /*@OptimizeFlags*/ int extraFlags) { ArtManagerLocal artManager = getArtManagerLocal(); if (artManager == null) { return Optional.empty(); @@ -512,18 +522,11 @@ public final class DexOptHelper { return Optional.of(PackageDexOptimizer.DEX_OPT_SKIPPED); } - // TODO(b/245301593): Delete the conditional when ART Service supports - // FLAG_SHOULD_INCLUDE_DEPENDENCIES and we can just set it unconditionally. - /*@OptimizeFlags*/ int extraFlags = ops.getUsesLibraries().isEmpty() - ? 0 - : ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES; - OptimizeParams params = options.convertToOptimizeParams(extraFlags); if (params == null) { return Optional.empty(); } - // TODO(b/251903639): Either remove controlDexOptBlocking, or don't ignore it here. OptimizeResult result; try { result = artManager.optimizePackage(snapshot, options.getPackageName(), params); @@ -532,21 +535,6 @@ public final class DexOptHelper { return Optional.empty(); } - // TODO(b/251903639): Move this to ArtManagerLocal.addOptimizePackageDoneCallback when - // it is implemented. - for (OptimizeResult.PackageOptimizeResult pkgRes : result.getPackageOptimizeResults()) { - PackageState ps = snapshot.getPackageState(pkgRes.getPackageName()); - AndroidPackage ap = ps != null ? ps.getAndroidPackage() : null; - if (ap != null) { - CompilerStats.PackageStats stats = mPm.getOrCreateCompilerPackageStats(ap); - for (OptimizeResult.DexContainerFileOptimizeResult dexRes : - pkgRes.getDexContainerFileOptimizeResults()) { - stats.setCompileTime( - dexRes.getDexContainerFile(), dexRes.getDex2oatWallTimeMillis()); - } - } - } - return Optional.of(convertToDexOptResult(result)); } } @@ -628,7 +616,8 @@ public final class DexOptHelper { // performDexOptWithArtService ignores the snapshot and takes its own, so it can race with // the package checks above, but at worst the effect is only a bit less friendly error // below. - Optional<Integer> artSrvRes = performDexOptWithArtService(options); + Optional<Integer> artSrvRes = + performDexOptWithArtService(options, ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES); int res; if (artSrvRes.isPresent()) { res = artSrvRes.get(); @@ -965,6 +954,51 @@ public final class DexOptHelper { } } + private static class OptimizePackageDoneHandler + implements ArtManagerLocal.OptimizePackageDoneCallback { + @NonNull private final PackageManagerService mPm; + + OptimizePackageDoneHandler(@NonNull PackageManagerService pm) { mPm = pm; } + + /** + * Called after every package optimization operation done by {@link ArtManagerLocal}. + */ + @Override + public void onOptimizePackageDone(@NonNull OptimizeResult result) { + for (OptimizeResult.PackageOptimizeResult pkgRes : result.getPackageOptimizeResults()) { + CompilerStats.PackageStats stats = + mPm.getOrCreateCompilerPackageStats(pkgRes.getPackageName()); + for (OptimizeResult.DexContainerFileOptimizeResult dexRes : + pkgRes.getDexContainerFileOptimizeResults()) { + stats.setCompileTime( + dexRes.getDexContainerFile(), dexRes.getDex2oatWallTimeMillis()); + } + } + + synchronized (mPm.mLock) { + mPm.getPackageUsage().maybeWriteAsync(mPm.mSettings.getPackagesLocked()); + mPm.mCompilerStats.maybeWriteAsync(); + } + } + } + + /** + * Initializes {@link ArtManagerLocal} before {@link getArtManagerLocal} is called. + */ + public static void initializeArtManagerLocal( + @NonNull Context systemContext, @NonNull PackageManagerService pm) { + if (!useArtService()) { + return; + } + + ArtManagerLocal artManager = new ArtManagerLocal(systemContext); + // There doesn't appear to be any checks that @NonNull is heeded, so use requireNonNull + // below to ensure we don't store away a null that we'll fail on later. + artManager.addOptimizePackageDoneCallback(false /* onlyIncludeUpdates */, + Runnable::run, new OptimizePackageDoneHandler(Objects.requireNonNull(pm))); + LocalManagerRegistry.addManager(ArtManagerLocal.class, artManager); + } + /** * Returns {@link ArtManagerLocal} if ART Service should be used for package optimization. */ diff --git a/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java index b4bcd5b3308c..cae7079c75e0 100644 --- a/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java +++ b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java @@ -27,13 +27,17 @@ import android.os.Process; import android.util.EventLog; import android.util.Log; +import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; +import com.android.server.art.DexUseManagerLocal; +import com.android.server.art.model.DexContainerFileUseInfo; import com.android.server.pm.dex.DynamicCodeLogger; import libcore.util.HexEncoding; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -137,6 +141,28 @@ public class DynamicCodeLoggingService extends JobService { return LocalServices.getService(PackageManagerInternal.class).getDynamicCodeLogger(); } + private static void syncDataFromArtService(DynamicCodeLogger dynamicCodeLogger) { + DexUseManagerLocal dexUseManagerLocal = DexOptHelper.getDexUseManagerLocal(); + if (dexUseManagerLocal == null) { + // ART Service is not enabled. + return; + } + PackageManagerLocal packageManagerLocal = + Objects.requireNonNull(LocalManagerRegistry.getManager(PackageManagerLocal.class)); + try (PackageManagerLocal.UnfilteredSnapshot snapshot = + packageManagerLocal.withUnfilteredSnapshot()) { + for (String owningPackageName : snapshot.getPackageStates().keySet()) { + for (DexContainerFileUseInfo info : + dexUseManagerLocal.getSecondaryDexContainerFileUseInfo(owningPackageName)) { + for (String loadingPackageName : info.getLoadingPackages()) { + dynamicCodeLogger.recordDex(info.getUserHandle().getIdentifier(), + info.getDexContainerFile(), owningPackageName, loadingPackageName); + } + } + } + } + } + private class IdleLoggingThread extends Thread { private final JobParameters mParams; @@ -152,6 +178,7 @@ public class DynamicCodeLoggingService extends JobService { } DynamicCodeLogger dynamicCodeLogger = getDynamicCodeLogger(); + syncDataFromArtService(dynamicCodeLogger); for (String packageName : dynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()) { if (mIdleLoggingStopRequested) { Log.w(TAG, "Stopping IdleLoggingJob run at scheduler request"); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 27d312ec4d4d..ac4da2ed7d73 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -311,10 +311,15 @@ final class InstallPackageHelper { pkgSetting.setForceQueryableOverride(true); } - // If this is part of a standard install, set the initiating package name, else rely on - // previous device state. InstallSource installSource = request.getInstallSource(); + final boolean isApex = (scanFlags & SCAN_AS_APEX) != 0; + final String updateOwnerFromSysconfig = isApex || !pkgSetting.isSystem() ? null + : mPm.mInjector.getSystemConfig().getSystemAppUpdateOwnerPackageName( + parsedPackage.getPackageName()); + // For new install (standard install), the installSource isn't null. if (installSource != null) { + // If this is part of a standard install, set the initiating package name, else rely on + // previous device state. if (installSource.mInitiatingPackageName != null) { final PackageSetting ips = mPm.mSettings.getPackageLPr( installSource.mInitiatingPackageName); @@ -323,7 +328,41 @@ final class InstallPackageHelper { ips.getSignatures()); } } + + // Handle the update ownership enforcement for APK + if (updateOwnerFromSysconfig != null) { + // For system app, we always use the update owner from sysconfig if presented. + installSource = installSource.setUpdateOwnerPackageName(updateOwnerFromSysconfig); + } else if (!parsedPackage.isAllowUpdateOwnership()) { + // If the app wants to opt-out of the update ownership enforcement via manifest, + // it overrides the installer's use of #setRequestUpdateOwnership. + installSource = installSource.setUpdateOwnerPackageName(null); + } else if (!isApex) { + final boolean isUpdate = oldPkgSetting != null; + final String oldUpdateOwner = + isUpdate ? oldPkgSetting.getInstallSource().mUpdateOwnerPackageName : null; + final boolean isUpdateOwnershipEnabled = oldUpdateOwner != null; + final boolean isRequestUpdateOwnership = (request.getInstallFlags() + & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0; + + // Here we assign the update owner for the package, and the rules are: + // -. If the installer doesn't request update ownership on initial installation, + // keep the update owner as null. + // -. If the installer doesn't want to be the owner to provide the subsequent + // update (doesn't request to be the update owner), e.g., non-store installer + // (file manager), ADB, or DO/PO, we should not update the owner. + // -. Else, the installer requests update ownership on initial installation or + // update, we use installSource.mUpdateOwnerPackageName as the update owner. + if (!isRequestUpdateOwnership || (isUpdate && !isUpdateOwnershipEnabled)) { + installSource = installSource.setUpdateOwnerPackageName(oldUpdateOwner); + } + } + pkgSetting.setInstallSource(installSource); + // non-standard install (addForInit and install existing packages), installSource is null. + } else if (updateOwnerFromSysconfig != null) { + // For system app, we always use the update owner from sysconfig if presented. + pkgSetting.setUpdateOwnerPackage(updateOwnerFromSysconfig); } if ((scanFlags & SCAN_AS_APK_IN_APEX) != 0) { @@ -1039,15 +1078,48 @@ final class InstallPackageHelper { DeviceConfig.getInt(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE, "MinInstallableTargetSdk__min_installable_target_sdk", 0); - if (parsedPackage.getTargetSdkVersion() < minInstallableTargetSdk) { + + // Skip enforcement when the bypass flag is set + boolean bypassLowTargetSdkBlock = + ((installFlags & PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK) != 0); + + // Skip enforcement for tests that were installed from adb + if (!bypassLowTargetSdkBlock + && ((installFlags & PackageManager.INSTALL_FROM_ADB) != 0)) { + bypassLowTargetSdkBlock = true; + } + + // Skip enforcement if the installer package name is not set + // (e.g. "pm install" from shell) + if (!bypassLowTargetSdkBlock) { + if (request.getInstallerPackageName() == null) { + bypassLowTargetSdkBlock = true; + } else { + // Also skip if the install is occurring from an app that was installed from adb + if (mContext + .getPackageManager() + .getInstallerPackageName(request.getInstallerPackageName()) == null) { + bypassLowTargetSdkBlock = true; + } + } + } + + // Skip enforcement when the testOnly flag is set + if (!bypassLowTargetSdkBlock && parsedPackage.isTestOnly()) { + bypassLowTargetSdkBlock = true; + } + + // Enforce the low target sdk install block except when + // the --bypass-low-target-sdk-block is set for the install + if (!bypassLowTargetSdkBlock + && parsedPackage.getTargetSdkVersion() < minInstallableTargetSdk) { Slog.w(TAG, "App " + parsedPackage.getPackageName() + " targets deprecated sdk version"); throw new PrepareFailure(INSTALL_FAILED_DEPRECATED_SDK_VERSION, - "App package must target at least version " - + minInstallableTargetSdk); + "App package must target at least SDK version " + + minInstallableTargetSdk + ", but found " + + parsedPackage.getTargetSdkVersion()); } - } else { - Slog.i(TAG, "Minimum installable target sdk enforcement not enabled"); } // Instant apps have several additional install-time checks. diff --git a/services/core/java/com/android/server/pm/InstallSource.java b/services/core/java/com/android/server/pm/InstallSource.java index dde9905ccc86..65bde518d0a1 100644 --- a/services/core/java/com/android/server/pm/InstallSource.java +++ b/services/core/java/com/android/server/pm/InstallSource.java @@ -34,12 +34,18 @@ public final class InstallSource { * An instance of InstallSource representing an absence of knowledge of the source of * a package. Used in preference to null. */ - static final InstallSource EMPTY = new InstallSource(null, null, null, INVALID_UID, null, - false, false, null, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); + static final InstallSource EMPTY = new InstallSource(null /* initiatingPackageName */, + null /* originatingPackageName */, null /* installerPackageName */, INVALID_UID, + null /* updateOwnerPackageName */, null /* installerAttributionTag */, + false /* isOrphaned */, false /* isInitiatingPackageUninstalled */, + null /* initiatingPackageSignatures */, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); /** We also memoize this case because it is common - all un-updated system apps. */ private static final InstallSource EMPTY_ORPHANED = new InstallSource( - null, null, null, INVALID_UID, null, true, false, null, + null /* initiatingPackageName */, null /* originatingPackageName */, + null /* installerPackageName */, INVALID_UID, null /* updateOwnerPackageName */, + null /* installerAttributionTag */, true /* isOrphaned */, + false /* isInitiatingPackageUninstalled */, null /* initiatingPackageSignatures */, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); /** @@ -73,6 +79,13 @@ public final class InstallSource { final String mInstallerPackageName; /** + * Package name of the app that requested the installer ownership. Note that this may be + * modified. + */ + @Nullable + final String mUpdateOwnerPackageName; + + /** * UID of the installer package, corresponding to the {@link #mInstallerPackageName}. */ final int mInstallerPackageUid; @@ -96,55 +109,64 @@ public final class InstallSource { static InstallSource create(@Nullable String initiatingPackageName, @Nullable String originatingPackageName, @Nullable String installerPackageName, - int installerPackageUid, @Nullable String installerAttributionTag, boolean isOrphaned, + int installerPackageUid, @Nullable String updateOwnerPackageName, + @Nullable String installerAttributionTag, boolean isOrphaned, boolean isInitiatingPackageUninstalled) { return create(initiatingPackageName, originatingPackageName, installerPackageName, - installerPackageUid, installerAttributionTag, + installerPackageUid, updateOwnerPackageName, installerAttributionTag, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED, isOrphaned, isInitiatingPackageUninstalled); } static InstallSource create(@Nullable String initiatingPackageName, @Nullable String originatingPackageName, @Nullable String installerPackageName, - int installerPackageUid, @Nullable String installerAttributionTag, int packageSource) { + int installerPackageUid, @Nullable String updateOwnerPackageName, + @Nullable String installerAttributionTag, int packageSource) { return create(initiatingPackageName, originatingPackageName, installerPackageName, - installerPackageUid, installerAttributionTag, packageSource, false, false); + installerPackageUid, updateOwnerPackageName, installerAttributionTag, + packageSource, false /* isOrphaned */, false /* isInitiatingPackageUninstalled */); } static InstallSource create(@Nullable String initiatingPackageName, @Nullable String originatingPackageName, @Nullable String installerPackageName, - int installerPackageUid, @Nullable String installerAttributionTag, int packageSource, - boolean isOrphaned, boolean isInitiatingPackageUninstalled) { + int installerPackageUid, @Nullable String updateOwnerPackageName, + @Nullable String installerAttributionTag, int packageSource, boolean isOrphaned, + boolean isInitiatingPackageUninstalled) { return createInternal( intern(initiatingPackageName), intern(originatingPackageName), intern(installerPackageName), installerPackageUid, + intern(updateOwnerPackageName), installerAttributionTag, packageSource, - isOrphaned, isInitiatingPackageUninstalled, null); + isOrphaned, isInitiatingPackageUninstalled, + null /* initiatingPackageSignatures */); } private static InstallSource createInternal(@Nullable String initiatingPackageName, @Nullable String originatingPackageName, @Nullable String installerPackageName, - int installerPackageUid, @Nullable String installerAttributionTag, int packageSource, - boolean isOrphaned, boolean isInitiatingPackageUninstalled, + int installerPackageUid, @Nullable String updateOwnerPackageName, + @Nullable String installerAttributionTag, int packageSource, boolean isOrphaned, + boolean isInitiatingPackageUninstalled, @Nullable PackageSignatures initiatingPackageSignatures) { if (initiatingPackageName == null && originatingPackageName == null - && installerPackageName == null && initiatingPackageSignatures == null + && installerPackageName == null && updateOwnerPackageName == null + && initiatingPackageSignatures == null && !isInitiatingPackageUninstalled && packageSource == PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED) { return isOrphaned ? EMPTY_ORPHANED : EMPTY; } return new InstallSource(initiatingPackageName, originatingPackageName, - installerPackageName, installerPackageUid, installerAttributionTag, isOrphaned, - isInitiatingPackageUninstalled, initiatingPackageSignatures, packageSource + installerPackageName, installerPackageUid, updateOwnerPackageName, + installerAttributionTag, isOrphaned, isInitiatingPackageUninstalled, + initiatingPackageSignatures, packageSource ); } private InstallSource(@Nullable String initiatingPackageName, @Nullable String originatingPackageName, @Nullable String installerPackageName, - int installerPackageUid, + int installerPackageUid, @Nullable String updateOwnerPackageName, @Nullable String installerAttributionTag, boolean isOrphaned, boolean isInitiatingPackageUninstalled, @Nullable PackageSignatures initiatingPackageSignatures, @@ -157,6 +179,7 @@ public final class InstallSource { mOriginatingPackageName = originatingPackageName; mInstallerPackageName = installerPackageName; mInstallerPackageUid = installerPackageUid; + mUpdateOwnerPackageName = updateOwnerPackageName; mInstallerAttributionTag = installerAttributionTag; mIsOrphaned = isOrphaned; mIsInitiatingPackageUninstalled = isInitiatingPackageUninstalled; @@ -174,9 +197,23 @@ public final class InstallSource { return this; } return createInternal(mInitiatingPackageName, mOriginatingPackageName, - intern(installerPackageName), installerPackageUid, mInstallerAttributionTag, - mPackageSource, mIsOrphaned, mIsInitiatingPackageUninstalled, - mInitiatingPackageSignatures); + intern(installerPackageName), installerPackageUid, mUpdateOwnerPackageName, + mInstallerAttributionTag, mPackageSource, mIsOrphaned, + mIsInitiatingPackageUninstalled, mInitiatingPackageSignatures); + } + + /** + * Return an InstallSource the same as this one except with the specified + * {@link #mUpdateOwnerPackageName}. + */ + InstallSource setUpdateOwnerPackageName(@Nullable String updateOwnerPackageName) { + if (Objects.equals(updateOwnerPackageName, mUpdateOwnerPackageName)) { + return this; + } + return createInternal(mInitiatingPackageName, mOriginatingPackageName, + mInstallerPackageName, mInstallerPackageUid, intern(updateOwnerPackageName), + mInstallerAttributionTag, mPackageSource, mIsOrphaned, + mIsInitiatingPackageUninstalled, mInitiatingPackageSignatures); } /** @@ -188,8 +225,8 @@ public final class InstallSource { return this; } return createInternal(mInitiatingPackageName, mOriginatingPackageName, - mInstallerPackageName, - mInstallerPackageUid, mInstallerAttributionTag, mPackageSource, isOrphaned, + mInstallerPackageName, mInstallerPackageUid, mUpdateOwnerPackageName, + mInstallerAttributionTag, mPackageSource, isOrphaned, mIsInitiatingPackageUninstalled, mInitiatingPackageSignatures); } @@ -202,8 +239,8 @@ public final class InstallSource { return this; } return createInternal(mInitiatingPackageName, mOriginatingPackageName, - mInstallerPackageName, - mInstallerPackageUid, mInstallerAttributionTag, mPackageSource, mIsOrphaned, + mInstallerPackageName, mInstallerPackageUid, mUpdateOwnerPackageName, + mInstallerAttributionTag, mPackageSource, mIsOrphaned, mIsInitiatingPackageUninstalled, signatures); } @@ -220,6 +257,7 @@ public final class InstallSource { boolean isInitiatingPackageUninstalled = mIsInitiatingPackageUninstalled; String originatingPackageName = mOriginatingPackageName; String installerPackageName = mInstallerPackageName; + String updateOwnerPackageName = mUpdateOwnerPackageName; int installerPackageUid = mInstallerPackageUid; boolean isOrphaned = mIsOrphaned; @@ -242,13 +280,18 @@ public final class InstallSource { isOrphaned = true; modified = true; } + if (packageName.equals(updateOwnerPackageName)) { + updateOwnerPackageName = null; + modified = true; + } if (!modified) { return this; } return createInternal(mInitiatingPackageName, originatingPackageName, installerPackageName, - installerPackageUid, null, mPackageSource, isOrphaned, + installerPackageUid, updateOwnerPackageName, + null /* installerAttributionTag */, mPackageSource, isOrphaned, isInitiatingPackageUninstalled, mInitiatingPackageSignatures); } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 8c5bab6a55dd..239853c857cc 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -878,9 +878,14 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements requestedInstallerPackageName = null; } + if (isApex || mContext.checkCallingOrSelfPermission( + Manifest.permission.ENFORCE_UPDATE_OWNERSHIP) == PackageManager.PERMISSION_DENIED) { + params.installFlags &= ~PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP; + } + InstallSource installSource = InstallSource.create(installerPackageName, originatingPackageName, requestedInstallerPackageName, requestedInstallerPackageUid, - installerAttributionTag, params.packageSource); + requestedInstallerPackageName, installerAttributionTag, params.packageSource); session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this, mSilentUpdatePolicy, mInstallThread.getLooper(), mStagingManager, sessionId, userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid, diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 0556c3ba8557..350f5ef7439c 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -89,6 +89,7 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.PreapprovalDetails; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionParams; +import android.content.pm.PackageInstaller.UserActionReason; import android.content.pm.PackageManager; import android.content.pm.PackageManager.PackageInfoFlags; import android.content.pm.PackageManagerInternal; @@ -226,6 +227,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final String ATTR_USER_ID = "userId"; private static final String ATTR_INSTALLER_PACKAGE_NAME = "installerPackageName"; private static final String ATTR_INSTALLER_PACKAGE_UID = "installerPackageUid"; + private static final String ATTR_UPDATE_OWNER_PACKAGE_NAME = "updateOwnererPackageName"; private static final String ATTR_INSTALLER_ATTRIBUTION_TAG = "installerAttributionTag"; private static final String ATTR_INSTALLER_UID = "installerUid"; private static final String ATTR_INITIATING_PACKAGE_NAME = @@ -446,6 +448,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private boolean mHasDeviceAdminReceiver; + @GuardedBy("mLock") + private int mUserActionRequirement; + static class FileEntry { private final int mIndex; private final InstallationFile mFile; @@ -842,10 +847,17 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final int USER_ACTION_NOT_NEEDED = 0; private static final int USER_ACTION_REQUIRED = 1; private static final int USER_ACTION_PENDING_APK_PARSING = 2; - - @IntDef({USER_ACTION_NOT_NEEDED, USER_ACTION_REQUIRED, USER_ACTION_PENDING_APK_PARSING}) - @interface - UserActionRequirement {} + private static final int USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED = 3; + private static final int USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED = 4; + + @IntDef({ + USER_ACTION_NOT_NEEDED, + USER_ACTION_REQUIRED, + USER_ACTION_PENDING_APK_PARSING, + USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED, + USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED + }) + @interface UserActionRequirement {} /** * Checks if the permissions still need to be confirmed. @@ -899,8 +911,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final String existingInstallerPackageName = existingInstallSourceInfo != null ? existingInstallSourceInfo.getInstallingPackageName() : null; + final String existingUpdateOwnerPackageName = existingInstallSourceInfo != null + ? existingInstallSourceInfo.getUpdateOwnerPackageName() + : null; final boolean isInstallerOfRecord = isUpdate && Objects.equals(existingInstallerPackageName, getInstallerPackageName()); + final boolean isUpdateOwner = Objects.equals(existingUpdateOwnerPackageName, + getInstallerPackageName()); final boolean isSelfUpdate = targetPackageUid == mInstallerUid; final boolean isPermissionGranted = isInstallPermissionGranted || (isUpdatePermissionGranted && isUpdate) @@ -908,16 +925,35 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { || (isInstallDpcPackagesPermissionGranted && hasDeviceAdminReceiver); final boolean isInstallerRoot = (mInstallerUid == Process.ROOT_UID); final boolean isInstallerSystem = (mInstallerUid == Process.SYSTEM_UID); + final boolean isInstallerShell = (mInstallerUid == Process.SHELL_UID); + final boolean isUpdateOwnershipEnforcementEnabled = + mPm.isUpdateOwnershipEnforcementAvailable() + && existingUpdateOwnerPackageName != null; - // Device owners and affiliated profile owners are allowed to silently install packages, so + // Device owners and affiliated profile owners are allowed to silently install packages, so // the permission check is waived if the installer is the device owner. - final boolean noUserActionNecessary = isPermissionGranted || isInstallerRoot - || isInstallerSystem || isInstallerDeviceOwnerOrAffiliatedProfileOwner(); + final boolean noUserActionNecessary = isInstallerRoot || isInstallerSystem + || isInstallerDeviceOwnerOrAffiliatedProfileOwner(); if (noUserActionNecessary) { return USER_ACTION_NOT_NEEDED; } + if (isUpdateOwnershipEnforcementEnabled + && !isApexSession() + && !isUpdateOwner + && !isInstallerShell) { + final boolean isRequestUpdateOwner = + (params.installFlags & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0; + + return isRequestUpdateOwner ? USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED + : USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED; + } + + if (isPermissionGranted) { + return USER_ACTION_NOT_NEEDED; + } + if (snapshot.isInstallDisabledForPackage(getInstallerPackageName(), mInstallerUid, userId)) { // show the installer to account for device policy or unknown sources use cases @@ -926,13 +962,20 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (params.requireUserAction == SessionParams.USER_ACTION_NOT_REQUIRED && isUpdateWithoutUserActionPermissionGranted - && (isInstallerOfRecord || isSelfUpdate)) { + && ((isUpdateOwnershipEnforcementEnabled ? isUpdateOwner + : isInstallerOfRecord) || isSelfUpdate)) { return USER_ACTION_PENDING_APK_PARSING; } return USER_ACTION_REQUIRED; } + private void updateUserActionRequirement(int requirement) { + synchronized (mLock) { + mUserActionRequirement = requirement; + } + } + @SuppressWarnings("GuardedBy" /*mPm.mInstaller is {@code final} field*/) public PackageInstallerSession(PackageInstallerService.InternalCallback callback, Context context, PackageManagerService pm, @@ -1121,6 +1164,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { info.installerUid = mInstallerUid; info.packageSource = params.packageSource; info.keepApplicationEnabledSetting = params.keepApplicationEnabledSetting; + info.pendingUserActionReason = userActionRequirementToReason(mUserActionRequirement); } return info; } @@ -1798,6 +1842,60 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { dispatchSessionSealed(); } + @Override + public void seal() { + assertNotChild("seal"); + assertCallerIsOwnerOrRoot(); + try { + sealInternal(); + for (var child : getChildSessions()) { + child.sealInternal(); + } + } catch (PackageManagerException e) { + throw new IllegalStateException("Package is not valid", e); + } + } + + private void sealInternal() throws PackageManagerException { + synchronized (mLock) { + sealLocked(); + } + } + + @Override + public List<String> fetchPackageNames() { + assertNotChild("fetchPackageNames"); + assertCallerIsOwnerOrRoot(); + var sessions = getSelfOrChildSessions(); + var result = new ArrayList<String>(sessions.size()); + for (var s : sessions) { + result.add(s.fetchPackageName()); + } + return result; + } + + private String fetchPackageName() { + assertSealed("fetchPackageName"); + synchronized (mLock) { + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final List<File> addedFiles = getAddedApksLocked(); + for (File addedFile : addedFiles) { + final ParseResult<ApkLite> result = + ApkLiteParseUtils.parseApkLite(input.reset(), addedFile, 0); + if (result.isError()) { + throw new IllegalStateException( + "Can't parse package for session=" + sessionId, result.getException()); + } + final ApkLite apk = result.getResult(); + var packageName = apk.getPackageName(); + if (packageName != null) { + return packageName; + } + } + throw new IllegalStateException("Can't fetch package name for session=" + sessionId); + } + } + /** * Kicks off the install flow. The first step is to persist 'sealed' flags * to prevent mutations of hard links created later. @@ -2051,6 +2149,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + @NonNull + private List<PackageInstallerSession> getSelfOrChildSessions() { + return isMultiPackage() ? getChildSessions() : Collections.singletonList(this); + } + /** * Seal the session to prevent further modification. * @@ -2205,8 +2308,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } mInstallerUid = newOwnerAppInfo.uid; - mInstallSource = InstallSource.create(packageName, null, packageName, - mInstallerUid, null, params.packageSource); + mInstallSource = InstallSource.create(packageName, null /* originatingPackageName */, + packageName, mInstallerUid, packageName, null /* installerAttributionTag */, + params.packageSource); } } @@ -2220,7 +2324,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @UserActionRequirement int userActionRequirement = USER_ACTION_NOT_NEEDED; // TODO(b/159331446): Move this to makeSessionActiveForInstall and update javadoc userActionRequirement = session.computeUserActionRequirement(); - if (userActionRequirement == USER_ACTION_REQUIRED) { + session.updateUserActionRequirement(userActionRequirement); + if (userActionRequirement == USER_ACTION_REQUIRED + || userActionRequirement == USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED + || userActionRequirement == USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED) { session.sendPendingUserActionIntent(target); return true; } @@ -2253,6 +2360,18 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { return false; } + private static @UserActionReason int userActionRequirementToReason( + @UserActionRequirement int requirement) { + switch (requirement) { + case USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED: + return PackageInstaller.REASON_OWNERSHIP_CHANGED; + case USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED: + return PackageInstaller.REASON_REMIND_OWNERSHIP; + default: + return PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE; + } + } + /** * Find out any session needs user action. * @@ -4438,6 +4557,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { return params.keepApplicationEnabledSetting; } + @Override + public boolean isRequestUpdateOwnership() { + return (params.installFlags & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0; + } + void setSessionReady() { synchronized (mLock) { // Do not allow destroyed/failed session to change state @@ -4774,6 +4898,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { writeStringAttribute(out, ATTR_INSTALLER_PACKAGE_NAME, mInstallSource.mInstallerPackageName); out.attributeInt(null, ATTR_INSTALLER_PACKAGE_UID, mInstallSource.mInstallerPackageUid); + writeStringAttribute(out, ATTR_UPDATE_OWNER_PACKAGE_NAME, + mInstallSource.mUpdateOwnerPackageName); writeStringAttribute(out, ATTR_INSTALLER_ATTRIBUTION_TAG, mInstallSource.mInstallerAttributionTag); out.attributeInt(null, ATTR_INSTALLER_UID, mInstallerUid); @@ -4941,6 +5067,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final String installerPackageName = readStringAttribute(in, ATTR_INSTALLER_PACKAGE_NAME); final int installPackageUid = in.getAttributeInt(null, ATTR_INSTALLER_PACKAGE_UID, INVALID_UID); + final String updateOwnerPackageName = readStringAttribute(in, + ATTR_UPDATE_OWNER_PACKAGE_NAME); final String installerAttributionTag = readStringAttribute(in, ATTR_INSTALLER_ATTRIBUTION_TAG); final int installerUid = in.getAttributeInt(null, ATTR_INSTALLER_UID, pm.snapshotComputer() @@ -5113,7 +5241,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { InstallSource installSource = InstallSource.create(installInitiatingPackageName, installOriginatingPackageName, installerPackageName, installPackageUid, - installerAttributionTag, params.packageSource); + updateOwnerPackageName, installerAttributionTag, params.packageSource); return new PackageInstallerSession(callback, context, pm, sessionProvider, silentUpdatePolicy, installerThread, stagingManager, sessionId, userId, installerUid, installSource, params, createdMillis, committedMillis, stageDir, diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java index 04f5e56c4eb7..99fff720221e 100644 --- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java +++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java @@ -49,7 +49,6 @@ import android.util.SparseArray; import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.dex.DexManager; -import com.android.server.pm.dex.DynamicCodeLogger; import com.android.server.pm.permission.PermissionManagerServiceInternal; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; @@ -773,10 +772,4 @@ abstract class PackageManagerInternalBase extends PackageManagerInternal { public final void shutdown() { mService.shutdown(); } - - @Override - @Deprecated - public final DynamicCodeLogger getDynamicCodeLogger() { - return getDexManager().getDynamicCodeLogger(); - } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 9a98e1e7d0e6..92bbb7e86327 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -204,6 +204,7 @@ import com.android.server.pm.Settings.VersionInfo; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.ArtUtils; import com.android.server.pm.dex.DexManager; +import com.android.server.pm.dex.DynamicCodeLogger; import com.android.server.pm.dex.ViewCompiler; import com.android.server.pm.local.PackageManagerLocalImpl; import com.android.server.pm.parsing.PackageInfoUtils; @@ -793,6 +794,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService // DexManager handles the usage of dex files (e.g. secondary files, whether or not a package // is used by other apps). private final DexManager mDexManager; + private final DynamicCodeLogger mDynamicCodeLogger; final ViewCompiler mViewCompiler; @@ -1529,7 +1531,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService (i, pm) -> new PackageDexOptimizer(i.getInstaller(), i.getInstallLock(), i.getContext(), "*dexopt*"), (i, pm) -> new DexManager(i.getContext(), i.getPackageDexOptimizer(), - i.getInstaller(), i.getInstallLock()), + i.getInstaller(), i.getInstallLock(), i.getDynamicCodeLogger()), + (i, pm) -> new DynamicCodeLogger(i.getInstaller()), (i, pm) -> new ArtManagerService(i.getContext(), i.getInstaller(), i.getInstallLock()), (i, pm) -> ApexManager.getInstance(), @@ -1711,6 +1714,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService mDefaultAppProvider = testParams.defaultAppProvider; mLegacyPermissionManager = testParams.legacyPermissionManagerInternal; mDexManager = testParams.dexManager; + mDynamicCodeLogger = testParams.dynamicCodeLogger; mFactoryTest = testParams.factoryTest; mIncrementalManager = testParams.incrementalManager; mInstallerService = testParams.installerService; @@ -1889,6 +1893,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService mPackageDexOptimizer = injector.getPackageDexOptimizer(); mDexManager = injector.getDexManager(); + mDynamicCodeLogger = injector.getDynamicCodeLogger(); mBackgroundDexOptService = injector.getBackgroundDexOptService(); mArtManagerService = injector.getArtManagerService(); mMoveCallbacks = new MovePackageHelper.MoveCallbacks(FgThread.get().getLooper()); @@ -2316,6 +2321,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService .getList()); } mDexManager.load(userPackages); + mDynamicCodeLogger.load(userPackages); if (mIsUpgrade) { FrameworkStatsLog.write( FrameworkStatsLog.BOOT_TIME_EVENT_DURATION_REPORTED, @@ -2980,9 +2986,14 @@ public class PackageManagerService implements PackageSender, TestUtilityService return mDexManager; } + /*package*/ DynamicCodeLogger getDynamicCodeLogger() { + return mDynamicCodeLogger; + } + public void shutdown() { mCompilerStats.writeNow(); mDexManager.writePackageDexUsageNow(); + mDynamicCodeLogger.writeNow(); PackageWatchdog.getInstance(mContext).writeNow(); synchronized (mLock) { @@ -6013,6 +6024,42 @@ public class PackageManagerService implements PackageSender, TestUtilityService } @Override + public void relinquishUpdateOwnership(String targetPackage) { + final int callingUid = Binder.getCallingUid(); + final int callingUserId = UserHandle.getUserId(callingUid); + final Computer snapshot = snapshotComputer(); + + final PackageStateInternal targetPackageState = + snapshot.getPackageStateForInstalledAndFiltered(targetPackage, callingUid, + callingUserId); + if (targetPackageState == null) { + throw new IllegalArgumentException("Unknown target package: " + targetPackage); + } + + final String targetUpdateOwnerPackageName = + targetPackageState.getInstallSource().mUpdateOwnerPackageName; + final PackageStateInternal targetUpdateOwnerPkgSetting = + targetUpdateOwnerPackageName == null ? null + : snapshot.getPackageStateInternal(targetUpdateOwnerPackageName); + + if (targetUpdateOwnerPkgSetting == null) { + return; + } + + final int callingAppId = UserHandle.getAppId(callingUid); + final int targetUpdateOwnerAppId = targetUpdateOwnerPkgSetting.getAppId(); + if (callingAppId != Process.SYSTEM_UID + && callingAppId != Process.SHELL_UID + && callingAppId != targetUpdateOwnerAppId) { + throw new SecurityException("Caller is not the current update owner."); + } + + commitPackageStateMutation(null /* initialState */, targetPackage, + state -> state.setUpdateOwner(null /* updateOwnerPackageName */)); + scheduleWriteSettings(); + } + + @Override public boolean setInstantAppCookie(String packageName, byte[] cookie, int userId) { if (HIDE_EPHEMERAL_APIS) { return true; @@ -6346,6 +6393,12 @@ public class PackageManagerService implements PackageSender, TestUtilityService return mDexManager; } + @NonNull + @Override + public DynamicCodeLogger getDynamicCodeLogger() { + return mDynamicCodeLogger; + } + @Override public boolean isPlatformSigned(String packageName) { PackageStateInternal packageState = snapshot().getPackageStateInternal(packageName); diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java index 76e6e45fc873..eb033cb343d4 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java @@ -30,6 +30,7 @@ import com.android.server.SystemConfig; import com.android.server.compat.PlatformCompat; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.DexManager; +import com.android.server.pm.dex.DynamicCodeLogger; import com.android.server.pm.dex.ViewCompiler; import com.android.server.pm.parsing.PackageParser2; import com.android.server.pm.permission.LegacyPermissionManagerInternal; @@ -106,6 +107,7 @@ public class PackageManagerServiceInjector { private final Singleton<PackageDexOptimizer> mPackageDexOptimizerProducer; private final Singleton<DexManager> mDexManagerProducer; + private final Singleton<DynamicCodeLogger> mDynamicCodeLoggerProducer; private final Singleton<ArtManagerService> mArtManagerServiceProducer; private final Singleton<ApexManager> mApexManagerProducer; @@ -154,6 +156,7 @@ public class PackageManagerServiceInjector { Producer<SystemConfig> systemConfigProducer, Producer<PackageDexOptimizer> packageDexOptimizerProducer, Producer<DexManager> dexManagerProducer, + Producer<DynamicCodeLogger> dynamicCodeLoggerProducer, Producer<ArtManagerService> artManagerServiceProducer, Producer<ApexManager> apexManagerProducer, Producer<ViewCompiler> viewCompilerProducer, @@ -200,6 +203,7 @@ public class PackageManagerServiceInjector { mPackageDexOptimizerProducer = new Singleton<>( packageDexOptimizerProducer); mDexManagerProducer = new Singleton<>(dexManagerProducer); + mDynamicCodeLoggerProducer = new Singleton<>(dynamicCodeLoggerProducer); mArtManagerServiceProducer = new Singleton<>( artManagerServiceProducer); mApexManagerProducer = new Singleton<>(apexManagerProducer); @@ -314,6 +318,10 @@ public class PackageManagerServiceInjector { return mDexManagerProducer.get(this, mPackageManager); } + public DynamicCodeLogger getDynamicCodeLogger() { + return mDynamicCodeLoggerProducer.get(this, mPackageManager); + } + public ArtManagerService getArtManagerService() { return mArtManagerServiceProducer.get(this, mPackageManager); } diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java index bffbb84bcfae..0c617aef1ed6 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java @@ -32,6 +32,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.om.OverlayConfig; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.DexManager; +import com.android.server.pm.dex.DynamicCodeLogger; import com.android.server.pm.dex.ViewCompiler; import com.android.server.pm.parsing.PackageParser2; import com.android.server.pm.permission.LegacyPermissionManagerInternal; @@ -49,6 +50,7 @@ public final class PackageManagerServiceTestParams { public int defParseFlags; public DefaultAppProvider defaultAppProvider; public DexManager dexManager; + public DynamicCodeLogger dynamicCodeLogger; public List<ScanPartition> dirsToScanAsSystem; public boolean factoryTest; public ArrayMap<String, FeatureInfo> availableFeatures; diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index a72ae56c8c41..0de1a4e0bc7c 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -33,6 +33,7 @@ import static com.android.server.pm.PackageManagerService.RANDOM_DIR_PREFIX; import static com.android.server.pm.PackageManagerService.STUB_SUFFIX; import static com.android.server.pm.PackageManagerService.TAG; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -113,6 +114,8 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.file.Path; import java.security.SecureRandom; import java.security.cert.CertificateEncodingException; @@ -148,6 +151,29 @@ public class PackageManagerServiceUtils { ThreadLocal.withInitial(() -> false); /** + * Type used with {@link #canJoinSharedUserId(String, SigningDetails, SharedUserSetting, int)} + * when the package attempting to join the sharedUserId is a new install. + */ + public static final int SHARED_USER_ID_JOIN_TYPE_INSTALL = 0; + /** + * Type used with {@link #canJoinSharedUserId(String, SigningDetails, SharedUserSetting, int)} + * when the package attempting to join the sharedUserId is an update. + */ + public static final int SHARED_USER_ID_JOIN_TYPE_UPDATE = 1; + /** + * Type used with {@link #canJoinSharedUserId(String, SigningDetails, SharedUserSetting, int)} + * when the package attempting to join the sharedUserId is a part of the system image. + */ + public static final int SHARED_USER_ID_JOIN_TYPE_SYSTEM = 2; + @IntDef(prefix = { "TYPE_" }, value = { + SHARED_USER_ID_JOIN_TYPE_INSTALL, + SHARED_USER_ID_JOIN_TYPE_UPDATE, + SHARED_USER_ID_JOIN_TYPE_SYSTEM, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SharedUserIdJoinType {} + + /** * Components of apps targeting Android T and above will stop receiving intents from * external callers that do not match its declared intent filters. * @@ -575,17 +601,9 @@ public class PackageManagerServiceUtils { // the older ones. We check to see if either the new package is signed by an older cert // with which the current sharedUser is ok, or if it is signed by a newer one, and is ok // with being sharedUser with the existing signing cert. - boolean match = canJoinSharedUserId(parsedSignatures, - sharedUserSetting.getSigningDetails()); - // Special case: if the sharedUserId capability check failed it could be due to this - // being the only package in the sharedUserId so far and the lineage being updated to - // deny the sharedUserId capability of the previous key in the lineage. - final ArraySet<PackageStateInternal> susPackageStates = - (ArraySet<PackageStateInternal>) sharedUserSetting.getPackageStates(); - if (!match && susPackageStates.size() == 1 - && susPackageStates.valueAt(0).getPackageName().equals(packageName)) { - match = true; - } + boolean match = canJoinSharedUserId(packageName, parsedSignatures, sharedUserSetting, + pkgSetting.getSigningDetails().getSignatures() != null + ? SHARED_USER_ID_JOIN_TYPE_UPDATE : SHARED_USER_ID_JOIN_TYPE_INSTALL); if (!match && compareCompat) { match = matchSignaturesCompat( packageName, sharedUserSetting.signatures, parsedSignatures); @@ -608,36 +626,6 @@ public class PackageManagerServiceUtils { + " has no signatures that match those in shared user " + sharedUserSetting.name + "; ignoring!"); } - // It is possible that this package contains a lineage that blocks sharedUserId access - // to an already installed package in the sharedUserId signed with a previous key. - // Iterate over all of the packages in the sharedUserId and ensure any that are signed - // with a key in this package's lineage have the SHARED_USER_ID capability granted. - if (parsedSignatures.hasPastSigningCertificates()) { - for (int i = 0; i < susPackageStates.size(); i++) { - PackageStateInternal shUidPkgSetting = susPackageStates.valueAt(i); - // if the current package in the sharedUserId is the package being updated then - // skip this check as the update may revoke the sharedUserId capability from - // the key with which this app was previously signed. - if (packageName.equals(shUidPkgSetting.getPackageName())) { - continue; - } - SigningDetails shUidSigningDetails = - shUidPkgSetting.getSigningDetails(); - // The capability check only needs to be performed against the package if it is - // signed with a key that is in the lineage of the package being installed. - if (parsedSignatures.hasAncestor(shUidSigningDetails)) { - if (!parsedSignatures.checkCapability(shUidSigningDetails, - SigningDetails.CertCapabilities.SHARED_USER_ID)) { - throw new PackageManagerException( - INSTALL_FAILED_SHARED_USER_INCOMPATIBLE, - "Package " + packageName - + " revoked the sharedUserId capability from the" - + " signing key used to sign " - + shUidPkgSetting.getPackageName()); - } - } - } - } // If the lineage of this package diverges from the lineage of the sharedUserId then // do not allow the installation to proceed. if (!parsedSignatures.hasCommonAncestor( @@ -651,25 +639,97 @@ public class PackageManagerServiceUtils { } /** - * Returns whether the package with {@code packageSigningDetails} can join the sharedUserId - * with {@code sharedUserSigningDetails}. + * Returns whether the package {@code packageName} can join the sharedUserId based on the + * settings in {@code sharedUserSetting}. * <p> * A sharedUserId maintains a shared {@link SigningDetails} containing the full lineage and * capabilities for each package in the sharedUserId. A package can join the sharedUserId if * its current signer is the same as the shared signer, or if the current signer of either * is in the signing lineage of the other with the {@link * SigningDetails.CertCapabilities#SHARED_USER_ID} capability granted to that previous signer - * in the lineage. + * in the lineage. In the case of a key compromise, an app signed with a lineage revoking + * this capability from a previous signing key can still join the sharedUserId with another + * app signed with this previous key if the joining app is being updated; however, a new + * install will not be allowed until all apps have rotated off the key with the capability + * revoked. * + * @param packageName the name of the package seeking to join the sharedUserId * @param packageSigningDetails the {@code SigningDetails} of the package seeking to join the - * sharedUserId - * @param sharedUserSigningDetails the {@code SigningDetails} of the sharedUserId + * sharedUserId + * @param sharedUserSetting the {@code SharedUserSetting} for the sharedUserId {@code + * packageName} is seeking to join + * @param joinType the type of join (install, update, system, etc) * @return true if the package seeking to join the sharedUserId meets the requirements */ - public static boolean canJoinSharedUserId(@NonNull SigningDetails packageSigningDetails, - @NonNull SigningDetails sharedUserSigningDetails) { - return packageSigningDetails.checkCapability(sharedUserSigningDetails, SHARED_USER_ID) - || sharedUserSigningDetails.checkCapability(packageSigningDetails, SHARED_USER_ID); + public static boolean canJoinSharedUserId(@NonNull String packageName, + @NonNull SigningDetails packageSigningDetails, + @NonNull SharedUserSetting sharedUserSetting, @SharedUserIdJoinType int joinType) { + SigningDetails sharedUserSigningDetails = sharedUserSetting.getSigningDetails(); + boolean capabilityGranted = + packageSigningDetails.checkCapability(sharedUserSigningDetails, SHARED_USER_ID) + || sharedUserSigningDetails.checkCapability(packageSigningDetails, + SHARED_USER_ID); + + // If the current signer for either the package or the sharedUserId is the current signer + // of the other or in the lineage of the other with the SHARED_USER_ID capability granted, + // then a system and update join type can proceed; an install join type is not allowed here + // since the sharedUserId may contain packages that are signed with a key untrusted by + // the new package. + if (capabilityGranted && joinType != SHARED_USER_ID_JOIN_TYPE_INSTALL) { + return true; + } + + // If the package is signed with a key that is no longer trusted by the sharedUserId, then + // the join should not be allowed unless this is a system join type; system packages can + // join the sharedUserId as long as they share a common lineage. + if (!capabilityGranted && sharedUserSigningDetails.hasAncestor(packageSigningDetails)) { + if (joinType == SHARED_USER_ID_JOIN_TYPE_SYSTEM) { + return true; + } + return false; + } + + // If the package is signed with a rotated key that no longer trusts the sharedUserId key, + // then allow system and update join types to rotate away from an untrusted key; install + // join types are not allowed since a new package that doesn't trust a previous key + // shouldn't be allowed to join until all packages in the sharedUserId have rotated off the + // untrusted key. + if (!capabilityGranted && packageSigningDetails.hasAncestor(sharedUserSigningDetails)) { + if (joinType != SHARED_USER_ID_JOIN_TYPE_INSTALL) { + return true; + } + return false; + } + + // If the capability is not granted and the package signatures are not an ancestor + // or descendant of the sharedUserId signatures, then do not allow any join type to join + // the sharedUserId since there are no common signatures. + if (!capabilityGranted) { + return false; + } + + // At this point this is a new install with the capability granted; ensure the current + // packages in the sharedUserId are all signed by a key trusted by the new package. + final ArraySet<PackageStateInternal> susPackageStates = + (ArraySet<PackageStateInternal>) sharedUserSetting.getPackageStates(); + if (packageSigningDetails.hasPastSigningCertificates()) { + for (PackageStateInternal shUidPkgSetting : susPackageStates) { + SigningDetails shUidSigningDetails = shUidPkgSetting.getSigningDetails(); + // The capability check only needs to be performed against the package if it is + // signed with a key that is in the lineage of the package being installed. + if (packageSigningDetails.hasAncestor(shUidSigningDetails)) { + if (!packageSigningDetails.checkCapability(shUidSigningDetails, + SigningDetails.CertCapabilities.SHARED_USER_ID)) { + Slog.d(TAG, "Package " + packageName + + " revoked the sharedUserId capability from the" + + " signing key used to sign " + + shUidPkgSetting.getPackageName()); + return false; + } + } + } + } + return true; } /** diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 9fc6c6387223..849cbeba0300 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -172,6 +172,11 @@ class PackageManagerShellCommand extends ShellCommand { SUPPORTED_PERMISSION_FLAGS.put("revoke-when-requested", FLAG_PERMISSION_REVOKE_WHEN_REQUESTED); } + // For backward compatibility. DO NOT add new commands here. New ART Service commands should be + // added under the "art" namespace. + private static final Set<String> ART_SERVICE_COMMANDS = Set.of("compile", + "reconcile-secondary-dex-files", "force-dex-opt", "bg-dexopt-job", + "cancel-bg-dexopt-job", "delete-dexopt", "dump-profiles", "snapshot-profile", "art"); final IPackageManager mInterface; final LegacyPermissionManagerInternal mLegacyPermissionManager; @@ -250,22 +255,6 @@ class PackageManagerShellCommand extends ShellCommand { return runMovePackage(); case "move-primary-storage": return runMovePrimaryStorage(); - case "compile": - return runCompile(); - case "reconcile-secondary-dex-files": - return runreconcileSecondaryDexFiles(); - case "force-dex-opt": - return runForceDexOpt(); - case "bg-dexopt-job": - return runBgDexOpt(); - case "cancel-bg-dexopt-job": - return cancelBgDexOptJob(); - case "delete-dexopt": - return runDeleteDexOpt(); - case "dump-profiles": - return runDumpProfiles(); - case "snapshot-profile": - return runSnapshotProfile(); case "uninstall": return runUninstall(); case "clear": @@ -355,9 +344,19 @@ class PackageManagerShellCommand extends ShellCommand { return runBypassAllowedApexUpdateCheck(); case "set-silent-updates-policy": return runSetSilentUpdatesPolicy(); - case "art": - return runArtSubCommand(); default: { + if (ART_SERVICE_COMMANDS.contains(cmd)) { + if (DexOptHelper.useArtService()) { + return runArtServiceCommand(); + } else { + try { + return runLegacyDexoptCommand(cmd); + } catch (LegacyDexoptDisabledException e) { + throw new RuntimeException(e); + } + } + } + Boolean domainVerificationResult = mDomainVerificationShell.runCommand(this, cmd); if (domainVerificationResult != null) { @@ -381,12 +380,39 @@ class PackageManagerShellCommand extends ShellCommand { } } catch (RemoteException e) { pw.println("Remote exception: " + e); - } catch (ManagerNotFoundException e) { - pw.println(e); } return -1; } + private int runLegacyDexoptCommand(@NonNull String cmd) + throws RemoteException, LegacyDexoptDisabledException { + Installer.checkLegacyDexoptDisabled(); + switch (cmd) { + case "compile": + return runCompile(); + case "reconcile-secondary-dex-files": + return runreconcileSecondaryDexFiles(); + case "force-dex-opt": + return runForceDexOpt(); + case "bg-dexopt-job": + return runBgDexOpt(); + case "cancel-bg-dexopt-job": + return cancelBgDexOptJob(); + case "delete-dexopt": + return runDeleteDexOpt(); + case "dump-profiles": + return runDumpProfiles(); + case "snapshot-profile": + return runSnapshotProfile(); + case "art": + getOutPrintWriter().println("ART Service not enabled"); + return -1; + default: + // Can't happen. + throw new IllegalArgumentException(); + } + } + /** * Shows module info * @@ -3211,6 +3237,15 @@ class PackageManagerShellCommand extends ShellCommand { case "--install-reason": sessionParams.installReason = Integer.parseInt(getNextArg()); break; + case "--update-ownership": + if (params.installerPackageName == null) { + // Enabling update ownership enforcement needs an installer. Since the + // default installer is null when using adb install, that effectively + // disable this enforcement. + params.installerPackageName = "com.android.shell"; + } + sessionParams.installFlags |= PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP; + break; case "--force-uuid": sessionParams.installFlags |= PackageManager.INSTALL_FORCE_VOLUME_UUID; sessionParams.volumeUuid = getNextArg(); @@ -3258,6 +3293,10 @@ class PackageManagerShellCommand extends ShellCommand { case "--skip-enable": sessionParams.setKeepApplicationEnabledSetting(); break; + case "--bypass-low-target-sdk-block": + sessionParams.installFlags |= + PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK; + break; default: throw new IllegalArgumentException("Unknown option " + opt); } @@ -3479,17 +3518,18 @@ class PackageManagerShellCommand extends ShellCommand { return 1; } - private int runArtSubCommand() throws ManagerNotFoundException { - // Remove the first arg "art" and forward to ART module. - String[] args = getAllArgs(); - args = Arrays.copyOfRange(args, 1, args.length); + private int runArtServiceCommand() { try (var in = ParcelFileDescriptor.dup(getInFileDescriptor()); var out = ParcelFileDescriptor.dup(getOutFileDescriptor()); var err = ParcelFileDescriptor.dup(getErrFileDescriptor())) { return LocalManagerRegistry.getManagerOrThrow(ArtManagerLocal.class) - .handleShellCommand(getTarget(), in, out, err, args); + .handleShellCommand(getTarget(), in, out, err, getAllArgs()); } catch (IOException e) { throw new IllegalStateException(e); + } catch (ManagerNotFoundException e) { + PrintWriter epw = getErrPrintWriter(); + epw.println("ART Service is not ready. Please try again later"); + return -1; } } @@ -4095,6 +4135,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --install-reason: indicates why the app is being installed:"); pw.println(" 0=unknown, 1=admin policy, 2=device restore,"); pw.println(" 3=device setup, 4=user request"); + pw.println(" --update-ownership: request the update ownership enforcement"); pw.println(" --force-uuid: force install on to disk volume with given UUID"); pw.println(" --apex: install an .apex file, not an .apk"); pw.println(" --staged-ready-timeout: By default, staged sessions wait " @@ -4118,7 +4159,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]"); pw.println(" [--preload] [--instant] [--full] [--dont-kill]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [--apex] [-S BYTES]"); - pw.println(" [--multi-package] [--staged]"); + pw.println(" [--multi-package] [--staged] [--update-ownership]"); pw.println(" Like \"install\", but starts an install session. Use \"install-write\""); pw.println(" to push data into the session, and \"install-commit\" to finish."); pw.println(""); @@ -4257,6 +4298,76 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(""); pw.println(" get-max-running-users"); pw.println(""); + pw.println(" set-home-activity [--user USER_ID] TARGET-COMPONENT"); + pw.println(" Set the default home activity (aka launcher)."); + pw.println(" TARGET-COMPONENT can be a package name (com.package.my) or a full"); + pw.println(" component (com.package.my/component.name). However, only the package name"); + pw.println(" matters: the actual component used will be determined automatically from"); + pw.println(" the package."); + pw.println(""); + pw.println(" set-installer PACKAGE INSTALLER"); + pw.println(" Set installer package name"); + pw.println(""); + pw.println(" get-instantapp-resolver"); + pw.println( + " Return the name of the component that is the current instant app installer."); + pw.println(""); + pw.println(" set-harmful-app-warning [--user <USER_ID>] <PACKAGE> [<WARNING>]"); + pw.println(" Mark the app as harmful with the given warning message."); + pw.println(""); + pw.println(" get-harmful-app-warning [--user <USER_ID>] <PACKAGE>"); + pw.println(" Return the harmful app warning message for the given app, if present"); + pw.println(); + pw.println(" uninstall-system-updates [<PACKAGE>]"); + pw.println(" Removes updates to the given system application and falls back to its"); + pw.println(" /system version. Does nothing if the given package is not a system app."); + pw.println(" If no package is specified, removes updates to all system applications."); + pw.println(""); + pw.println(" get-moduleinfo [--all | --installed] [module-name]"); + pw.println(" Displays module info. If module-name is specified only that info is shown"); + pw.println(" By default, without any argument only installed modules are shown."); + pw.println(" --all: show all module info"); + pw.println(" --installed: show only installed modules"); + pw.println(""); + pw.println(" log-visibility [--enable|--disable] <PACKAGE>"); + pw.println(" Turns on debug logging when visibility is blocked for the given package."); + pw.println(" --enable: turn on debug logging (default)"); + pw.println(" --disable: turn off debug logging"); + pw.println(""); + pw.println(" set-silent-updates-policy [--allow-unlimited-silent-updates <INSTALLER>]"); + pw.println(" [--throttle-time <SECONDS>] [--reset]"); + pw.println(" Sets the policies of the silent updates."); + pw.println(" --allow-unlimited-silent-updates: allows unlimited silent updated"); + pw.println(" installation requests from the installer without the throttle time."); + pw.println(" --throttle-time: update the silent updates throttle time in seconds."); + pw.println(" --reset: restore the installer and throttle time to the default, and"); + pw.println(" clear tracks of silent updates in the system."); + pw.println(""); + if (DexOptHelper.useArtService()) { + printArtServiceHelp(); + } else { + printLegacyDexoptHelp(); + } + pw.println(""); + mDomainVerificationShell.printHelp(pw); + pw.println(""); + Intent.printIntentArgsHelp(pw, ""); + } + + private void printArtServiceHelp() { + final var ipw = new IndentingPrintWriter(getOutPrintWriter(), " " /* singleIndent */); + ipw.increaseIndent(); + try { + LocalManagerRegistry.getManagerOrThrow(ArtManagerLocal.class) + .printShellCommandHelp(ipw); + } catch (ManagerNotFoundException e) { + ipw.println("ART Service is not ready. Please try again later"); + } + ipw.decreaseIndent(); + } + + private void printLegacyDexoptHelp() { + final PrintWriter pw = getOutPrintWriter(); pw.println(" compile [-m MODE | -r REASON] [-f] [-c] [--split SPLIT_NAME]"); pw.println(" [--reset] [--check-prof (true | false)] (-a | TARGET-PACKAGE)"); pw.println(" Trigger compilation of TARGET-PACKAGE or all packages if \"-a\". Options are:"); @@ -4329,57 +4440,6 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" " + ART_PROFILE_SNAPSHOT_DEBUG_LOCATION + "TARGET-PACKAGE[-code-path].prof"); pw.println(" If TARGET-PACKAGE=android it will take a snapshot of the boot image"); - pw.println(""); - pw.println(" set-home-activity [--user USER_ID] TARGET-COMPONENT"); - pw.println(" Set the default home activity (aka launcher)."); - pw.println(" TARGET-COMPONENT can be a package name (com.package.my) or a full"); - pw.println(" component (com.package.my/component.name). However, only the package name"); - pw.println(" matters: the actual component used will be determined automatically from"); - pw.println(" the package."); - pw.println(""); - pw.println(" set-installer PACKAGE INSTALLER"); - pw.println(" Set installer package name"); - pw.println(""); - pw.println(" get-instantapp-resolver"); - pw.println(" Return the name of the component that is the current instant app installer."); - pw.println(""); - pw.println(" set-harmful-app-warning [--user <USER_ID>] <PACKAGE> [<WARNING>]"); - pw.println(" Mark the app as harmful with the given warning message."); - pw.println(""); - pw.println(" get-harmful-app-warning [--user <USER_ID>] <PACKAGE>"); - pw.println(" Return the harmful app warning message for the given app, if present"); - pw.println(); - pw.println(" uninstall-system-updates [<PACKAGE>]"); - pw.println(" Removes updates to the given system application and falls back to its"); - pw.println(" /system version. Does nothing if the given package is not a system app."); - pw.println(" If no package is specified, removes updates to all system applications."); - pw.println(""); - pw.println(" get-moduleinfo [--all | --installed] [module-name]"); - pw.println(" Displays module info. If module-name is specified only that info is shown"); - pw.println(" By default, without any argument only installed modules are shown."); - pw.println(" --all: show all module info"); - pw.println(" --installed: show only installed modules"); - pw.println(""); - pw.println(" log-visibility [--enable|--disable] <PACKAGE>"); - pw.println(" Turns on debug logging when visibility is blocked for the given package."); - pw.println(" --enable: turn on debug logging (default)"); - pw.println(" --disable: turn off debug logging"); - pw.println(""); - pw.println(" set-silent-updates-policy [--allow-unlimited-silent-updates <INSTALLER>]"); - pw.println(" [--throttle-time <SECONDS>] [--reset]"); - pw.println(" Sets the policies of the silent updates."); - pw.println(" --allow-unlimited-silent-updates: allows unlimited silent updated"); - pw.println(" installation requests from the installer without the throttle time."); - pw.println(" --throttle-time: update the silent updates throttle time in seconds."); - pw.println(" --reset: restore the installer and throttle time to the default, and"); - pw.println(" clear tracks of silent updates in the system."); - pw.println(""); - pw.println(" art [<SUB-COMMANDS>]"); - pw.println(" Invokes ART services commands. (Run `pm art help` for details.)"); - pw.println(""); - mDomainVerificationShell.printHelp(pw); - pw.println(""); - Intent.printIntentArgsHelp(pw , ""); } private static class LocalIntentReceiver { diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 877b1127cfb4..6562de96388f 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -300,6 +300,8 @@ public class PackageSetting extends SettingBase implements PackageStateInternal installSource.mInitiatingPackageName); proto.write(PackageProto.InstallSourceProto.ORIGINATING_PACKAGE_NAME, installSource.mOriginatingPackageName); + proto.write(PackageProto.InstallSourceProto.UPDATE_OWNER_PACKAGE_NAME, + installSource.mUpdateOwnerPackageName); proto.end(sourceToken); } proto.write(PackageProto.StatesProto.IS_LOADING, isLoading()); @@ -368,6 +370,12 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return this; } + public PackageSetting setUpdateOwnerPackage(@Nullable String updateOwnerPackageName) { + installSource = installSource.setUpdateOwnerPackageName(updateOwnerPackageName); + onChanged(); + return this; + } + public PackageSetting setInstallSource(InstallSource installSource) { this.installSource = Objects.requireNonNull(installSource); onChanged(); diff --git a/services/core/java/com/android/server/pm/ReconcilePackageUtils.java b/services/core/java/com/android/server/pm/ReconcilePackageUtils.java index 99bcbc9f95e6..58dcb0201365 100644 --- a/services/core/java/com/android/server/pm/ReconcilePackageUtils.java +++ b/services/core/java/com/android/server/pm/ReconcilePackageUtils.java @@ -213,8 +213,9 @@ final class ReconcilePackageUtils { if (sharedUserSetting != null) { if (sharedUserSetting.signaturesChanged != null && !PackageManagerServiceUtils.canJoinSharedUserId( - parsedPackage.getSigningDetails(), - sharedUserSetting.getSigningDetails())) { + parsedPackage.getPackageName(), parsedPackage.getSigningDetails(), + sharedUserSetting, + PackageManagerServiceUtils.SHARED_USER_ID_JOIN_TYPE_SYSTEM)) { if (SystemProperties.getInt("ro.product.first_api_level", 0) <= 29) { // Mismatched signatures is an error and silently skipping system // packages will likely break the device in unforeseen ways. diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 6ebef2057e78..97fb0c2e3fe9 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -3050,6 +3050,9 @@ public final class Settings implements Watchable, Snappable { if (installSource.mInstallerPackageUid != INVALID_UID) { serializer.attributeInt(null, "installerUid", installSource.mInstallerPackageUid); } + if (installSource.mUpdateOwnerPackageName != null) { + serializer.attribute(null, "updateOwner", installSource.mUpdateOwnerPackageName); + } if (installSource.mInstallerAttributionTag != null) { serializer.attribute(null, "installerAttributionTag", installSource.mInstallerAttributionTag); @@ -3880,6 +3883,7 @@ public final class Settings implements Watchable, Snappable { String systemStr = null; String installerPackageName = null; int installerPackageUid = INVALID_UID; + String updateOwnerPackageName = null; String installerAttributionTag = null; int packageSource = PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED; boolean isOrphaned = false; @@ -3923,6 +3927,7 @@ public final class Settings implements Watchable, Snappable { versionCode = parser.getAttributeLong(null, "version", 0); installerPackageName = parser.getAttributeValue(null, "installer"); installerPackageUid = parser.getAttributeInt(null, "installerUid", INVALID_UID); + updateOwnerPackageName = parser.getAttributeValue(null, "updateOwner"); installerAttributionTag = parser.getAttributeValue(null, "installerAttributionTag"); packageSource = parser.getAttributeInt(null, "packageSource", PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); @@ -4065,8 +4070,9 @@ public final class Settings implements Watchable, Snappable { if (packageSetting != null) { InstallSource installSource = InstallSource.create( installInitiatingPackageName, installOriginatingPackageName, - installerPackageName, installerPackageUid, installerAttributionTag, - packageSource, isOrphaned, installInitiatorUninstalled); + installerPackageName, installerPackageUid, updateOwnerPackageName, + installerAttributionTag, packageSource, isOrphaned, + installInitiatorUninstalled); packageSetting.setInstallSource(installSource) .setVolumeUuid(volumeUuid) .setCategoryOverride(categoryHint) @@ -4734,6 +4740,8 @@ public final class Settings implements Watchable, Snappable { pw.print(ps.getInstallSource().mInstallerPackageName != null ? ps.getInstallSource().mInstallerPackageName : "?"); pw.print(ps.getInstallSource().mInstallerPackageUid); + pw.print(ps.getInstallSource().mUpdateOwnerPackageName != null + ? ps.getInstallSource().mUpdateOwnerPackageName : "?"); pw.print(ps.getInstallSource().mInstallerAttributionTag != null ? "(" + ps.getInstallSource().mInstallerAttributionTag + ")" : ""); pw.print(","); @@ -5017,6 +5025,10 @@ public final class Settings implements Watchable, Snappable { pw.print(prefix); pw.print(" installerPackageUid="); pw.println(ps.getInstallSource().mInstallerPackageUid); } + if (ps.getInstallSource().mUpdateOwnerPackageName != null) { + pw.print(prefix); pw.print(" updateOwnerPackageName="); + pw.println(ps.getInstallSource().mUpdateOwnerPackageName); + } if (ps.getInstallSource().mInstallerAttributionTag != null) { pw.print(prefix); pw.print(" installerAttributionTag="); pw.println(ps.getInstallSource().mInstallerAttributionTag); diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java index d2ce23efd47c..99878679431c 100644 --- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java +++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java @@ -17,6 +17,7 @@ package com.android.server.pm; import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SHARED_LIBRARY; +import static android.content.pm.PackageManager.INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import static com.android.server.pm.PackageManagerService.SCAN_BOOTING; @@ -1035,8 +1036,17 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable } else { // lib signing cert could have rotated beyond the one expected, check to see // if the new one has been blessed by the old - byte[] digestBytes = HexEncoding.decode( - expectedCertDigests[0], false /* allowSingleChar */); + final byte[] digestBytes; + try { + digestBytes = HexEncoding.decode( + expectedCertDigests[0], false /* allowSingleChar */); + } catch (IllegalArgumentException e) { + throw new PackageManagerException( + INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST, + "Package " + packageName + " declares bad certificate digest " + + "for " + libraryType + " library " + libName + + "; failing!"); + } if (!libPkg.hasSha256Certificate(digestBytes)) { throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY, "Package " + packageName + " requires differently signed " diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index 2ae8b52da172..7b15e760107b 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -388,8 +388,8 @@ public abstract class UserManagerInternal { * and the user is {@link UserManager#isUserVisible() visible}. * * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user - * is started). If other clients (like {@code CarService} need to explicitly change the user / - * display assignment, we'll need to provide other APIs. + * is started); for extra unassignments, callers should call {@link + * #assignUserToExtraDisplay(int, int)} instead. * * <p><b>NOTE: </b>this method doesn't validate if the display exists, it's up to the caller to * pass a valid display id. @@ -398,15 +398,43 @@ public abstract class UserManagerInternal { @UserIdInt int profileGroupId, @UserStartMode int userStartMode, int displayId); /** + * Assigns an extra display to the given user, so the user is visible on that display. + * + * <p>This method is meant to be used on automotive builds where a passenger zone has more than + * one display (for example, the "main" display and a smaller display used for input). + * + * <p><b>NOTE: </b>this call will be ignored on devices that do not + * {@link UserManager#isVisibleBackgroundUsersSupported() support visible background users}. + * + * @return whether the operation succeeded, in which case the user would be visible on the + * display. + */ + public abstract boolean assignUserToExtraDisplay(@UserIdInt int userId, int displayId); + + /** * Unassigns a user from its current display when it's stopping. * * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user - * is stopped). If other clients (like {@code CarService} need to explicitly change the user / - * display assignment, we'll need to provide other APIs. + * is stopped); for extra unassignments, callers should call + * {@link #unassignUserFromExtraDisplay(int, int)} instead. */ public abstract void unassignUserFromDisplayOnStop(@UserIdInt int userId); /** + * Unassigns the extra display from the given user. + * + * <p>This method is meant to be used on automotive builds where a passenger zone has more than + * one display (for example, the "main" display and a smaller display used for input). + * + * <p><b>NOTE: </b>this call will be ignored on devices that do not + * {@link UserManager#isVisibleBackgroundUsersSupported() support visible background users}. + * + * @return whether the operation succeeded, i.e., the user was previously + * {@link #assignUserToExtraDisplay(int, int) assigned to an extra display}. + */ + public abstract boolean unassignUserFromExtraDisplay(@UserIdInt int userId, int displayId); + + /** * Returns {@code true} if the user is visible (as defined by * {@link UserManager#isUserVisible()}. */ diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 81f83b0591c4..a8cf8cb2b034 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1476,20 +1476,15 @@ public class UserManagerService extends IUserManager.Stub { @Override public void revokeUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("revoke admin privileges"); - synchronized (mPackagesLock) { - UserInfo info; synchronized (mUsersLock) { - info = getUserInfoLU(userId); - } - if (info == null || !info.isAdmin()) { - // Exit if no user found with that id, or the user is not an Admin. - return; - } - - info.flags ^= UserInfo.FLAG_ADMIN; - synchronized (mUsersLock) { - writeUserLP(getUserDataLU(info.id)); + UserData user = getUserDataLU(userId); + if (user == null || !user.info.isAdmin()) { + // Exit if no user found with that id, or the user is not an Admin. + return; + } + user.info.flags ^= UserInfo.FLAG_ADMIN; + writeUserLP(user); } } } @@ -1834,23 +1829,6 @@ public class UserManagerService extends IUserManager.Stub { } /** - * Gets the current user id, or the target user id in case there is a started user switch. - * - * @return id of current or target foreground user, or {@link UserHandle#USER_NULL} if - * {@link ActivityManagerInternal} is not available yet. - */ - @VisibleForTesting - int getCurrentOrTargetUserId() { - ActivityManagerInternal activityManagerInternal = getActivityManagerInternal(); - if (activityManagerInternal == null) { - Slog.w(LOG_TAG, "getCurrentOrTargetUserId() called too early, ActivityManagerInternal" - + " is not set yet"); - return UserHandle.USER_NULL; - } - return activityManagerInternal.getCurrentUser().id; - } - - /** * Gets whether the user is the current foreground user or a started profile of that user. * * <p>Doesn't perform any permission check. @@ -5424,7 +5402,8 @@ public class UserManagerService extends IUserManager.Stub { final long ident = Binder.clearCallingIdentity(); try { final UserData userData; - if (userId == getCurrentOrTargetUserId()) { + int currentUser = getCurrentUserId(); + if (currentUser == userId) { Slog.w(LOG_TAG, "Current user cannot be removed."); return false; } @@ -7043,6 +7022,16 @@ public class UserManagerService extends IUserManager.Stub { } @Override + public boolean assignUserToExtraDisplay(int userId, int displayId) { + return mUserVisibilityMediator.assignUserToExtraDisplay(userId, displayId); + } + + @Override + public boolean unassignUserFromExtraDisplay(int userId, int displayId) { + return mUserVisibilityMediator.unassignUserFromExtraDisplay(userId, displayId); + } + + @Override public void unassignUserFromDisplayOnStop(@UserIdInt int userId) { mUserVisibilityMediator.unassignUserFromDisplayOnStop(userId); } diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index 27d74d517fb9..214fd617c999 100644 --- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java +++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java @@ -149,7 +149,8 @@ public class UserRestrictionsUtils { UserManager.DISALLOW_WIFI_DIRECT, UserManager.DISALLOW_ADD_WIFI_CONFIG, UserManager.DISALLOW_CELLULAR_2G, - UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO + UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO, + UserManager.DISALLOW_CONFIG_DEFAULT_APPS }); public static final Set<String> DEPRECATED_USER_RESTRICTIONS = Sets.newArraySet( diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java index d8e4dac48ebe..66d390f4f3e9 100644 --- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java +++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java @@ -19,6 +19,7 @@ import static android.content.pm.UserInfo.NO_PROFILE_GROUP_ID; import static android.os.UserHandle.USER_NULL; import static android.os.UserHandle.USER_SYSTEM; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE; @@ -113,7 +114,18 @@ public final class UserVisibilityMediator implements Dumpable { */ @Nullable @GuardedBy("mLock") - private final SparseIntArray mUsersOnDisplaysMap; + private final SparseIntArray mUsersAssignedToDisplayOnStart; + + /** + * Map of extra (i.e., not assigned on start, but by explicit calls to + * {@link #assignUserToExtraDisplay(int, int)}) displays assigned to user (key is display id, + * value is user id). + * + * <p>Only set when {@code mUsersOnSecondaryDisplaysEnabled} is {@code true}. + */ + @Nullable + @GuardedBy("mLock") + private final SparseIntArray mExtraDisplaysAssignedToUsers; /** * Mapping from each started user to its profile group. @@ -137,7 +149,13 @@ public final class UserVisibilityMediator implements Dumpable { @VisibleForTesting UserVisibilityMediator(boolean backgroundUsersOnDisplaysEnabled, Handler handler) { mVisibleBackgroundUsersEnabled = backgroundUsersOnDisplaysEnabled; - mUsersOnDisplaysMap = mVisibleBackgroundUsersEnabled ? new SparseIntArray() : null; + if (mVisibleBackgroundUsersEnabled) { + mUsersAssignedToDisplayOnStart = new SparseIntArray(); + mExtraDisplaysAssignedToUsers = new SparseIntArray(); + } else { + mUsersAssignedToDisplayOnStart = null; + mExtraDisplaysAssignedToUsers = null; + } mHandler = handler; // TODO(b/242195409): might need to change this if boot logic is refactored for HSUM devices mStartedProfileGroupIds.put(INITIAL_CURRENT_USER_ID, INITIAL_CURRENT_USER_ID); @@ -207,7 +225,7 @@ public final class UserVisibilityMediator implements Dumpable { if (DBG) { Slogf.d(TAG, "adding user / display mapping (%d -> %d)", userId, displayId); } - mUsersOnDisplaysMap.put(userId, displayId); + mUsersAssignedToDisplayOnStart.put(userId, displayId); break; case SECONDARY_DISPLAY_MAPPING_NOT_NEEDED: if (DBG) { @@ -341,9 +359,9 @@ public final class UserVisibilityMediator implements Dumpable { } // Check if display is available - for (int i = 0; i < mUsersOnDisplaysMap.size(); i++) { - int assignedUserId = mUsersOnDisplaysMap.keyAt(i); - int assignedDisplayId = mUsersOnDisplaysMap.valueAt(i); + for (int i = 0; i < mUsersAssignedToDisplayOnStart.size(); i++) { + int assignedUserId = mUsersAssignedToDisplayOnStart.keyAt(i); + int assignedDisplayId = mUsersAssignedToDisplayOnStart.valueAt(i); if (DBG) { Slogf.d(TAG, "%d: assignedUserId=%d, assignedDisplayId=%d", i, assignedUserId, assignedDisplayId); @@ -363,6 +381,100 @@ public final class UserVisibilityMediator implements Dumpable { } /** + * See {@link UserManagerInternal#assignUserToExtraDisplay(int, int)}. + */ + public boolean assignUserToExtraDisplay(@UserIdInt int userId, int displayId) { + if (DBG) { + Slogf.d(TAG, "assignUserToExtraDisplay(%d, %d)", userId, displayId); + } + if (!mVisibleBackgroundUsersEnabled) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): called when not supported", userId, + displayId); + return false; + } + if (displayId == INVALID_DISPLAY) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): called with INVALID_DISPLAY", userId, + displayId); + return false; + } + if (displayId == DEFAULT_DISPLAY) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): DEFAULT_DISPLAY is automatically " + + "assigned to current user", userId, displayId); + return false; + } + + synchronized (mLock) { + if (!isUserVisible(userId)) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because user is not visible", + userId, displayId); + return false; + } + if (isStartedProfile(userId)) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because user is a profile", + userId, displayId); + return false; + } + + if (mExtraDisplaysAssignedToUsers.get(displayId, USER_NULL) == userId) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because user is already " + + "assigned to that display", userId, displayId); + return false; + } + + int userAssignedToDisplay = getUserAssignedToDisplay(displayId, + /* returnCurrentUserByDefault= */ false); + if (userAssignedToDisplay != USER_NULL) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because display was assigned" + + " to user %d on start", userId, displayId, userAssignedToDisplay); + return false; + } + userAssignedToDisplay = mExtraDisplaysAssignedToUsers.get(userId, USER_NULL); + if (userAssignedToDisplay != USER_NULL) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because user %d was already " + + "assigned that extra display", userId, displayId, userAssignedToDisplay); + return false; + } + if (DBG) { + Slogf.d(TAG, "addding %d -> %d to map", displayId, userId); + } + mExtraDisplaysAssignedToUsers.put(displayId, userId); + } + return true; + } + + /** + * See {@link UserManagerInternal#unassignUserFromExtraDisplay(int, int)}. + */ + public boolean unassignUserFromExtraDisplay(@UserIdInt int userId, int displayId) { + if (DBG) { + Slogf.d(TAG, "unassignUserFromExtraDisplay(%d, %d)", userId, displayId); + } + if (!mVisibleBackgroundUsersEnabled) { + Slogf.w(TAG, "unassignUserFromExtraDisplay(%d, %d): called when not supported", + userId, displayId); + return false; + } + synchronized (mLock) { + int assignedUserId = mExtraDisplaysAssignedToUsers.get(displayId, USER_NULL); + if (assignedUserId == USER_NULL) { + Slogf.w(TAG, "unassignUserFromExtraDisplay(%d, %d): not assigned to any user", + userId, displayId); + return false; + } + if (assignedUserId != userId) { + Slogf.w(TAG, "unassignUserFromExtraDisplay(%d, %d): was assigned to user %d", + userId, displayId, assignedUserId); + return false; + } + if (DBG) { + Slogf.d(TAG, "removing %d from map", displayId); + } + mExtraDisplaysAssignedToUsers.delete(displayId); + } + return true; + } + + /** * See {@link UserManagerInternal#unassignUserFromDisplayOnStop(int)}. */ public void unassignUserFromDisplayOnStop(@UserIdInt int userId) { @@ -373,7 +485,7 @@ public final class UserVisibilityMediator implements Dumpable { synchronized (mLock) { visibleUsersBefore = getVisibleUsers(); - unassignUserFromDisplayOnStopLocked(userId); + unassignUserFromAllDisplaysOnStopLocked(userId); visibleUsersAfter = getVisibleUsers(); } @@ -381,7 +493,7 @@ public final class UserVisibilityMediator implements Dumpable { } @GuardedBy("mLock") - private void unassignUserFromDisplayOnStopLocked(@UserIdInt int userId) { + private void unassignUserFromAllDisplaysOnStopLocked(@UserIdInt int userId) { if (DBG) { Slogf.d(TAG, "Removing %d from mStartedProfileGroupIds (%s)", userId, mStartedProfileGroupIds); @@ -395,10 +507,21 @@ public final class UserVisibilityMediator implements Dumpable { return; } if (DBG) { - Slogf.d(TAG, "Removing %d from mUsersOnSecondaryDisplays (%s)", userId, - mUsersOnDisplaysMap); + Slogf.d(TAG, "Removing user %d from mUsersOnDisplaysMap (%s)", userId, + mUsersAssignedToDisplayOnStart); + } + mUsersAssignedToDisplayOnStart.delete(userId); + + // Remove extra displays as well + for (int i = mExtraDisplaysAssignedToUsers.size() - 1; i >= 0; i--) { + if (mExtraDisplaysAssignedToUsers.valueAt(i) == userId) { + if (DBG) { + Slogf.d(TAG, "Removing display %d from mExtraDisplaysAssignedToUsers (%s)", + mExtraDisplaysAssignedToUsers.keyAt(i), mExtraDisplaysAssignedToUsers); + } + mExtraDisplaysAssignedToUsers.removeAt(i); + } } - mUsersOnDisplaysMap.delete(userId); } /** @@ -424,7 +547,7 @@ public final class UserVisibilityMediator implements Dumpable { boolean visible; synchronized (mLock) { - visible = mUsersOnDisplaysMap.indexOfKey(userId) >= 0; + visible = mUsersAssignedToDisplayOnStart.indexOfKey(userId) >= 0; } if (DBG) { Slogf.d(TAG, "isUserVisible(%d): %b from mapping", userId, visible); @@ -448,7 +571,12 @@ public final class UserVisibilityMediator implements Dumpable { } synchronized (mLock) { - return mUsersOnDisplaysMap.get(userId, Display.INVALID_DISPLAY) == displayId; + if (mUsersAssignedToDisplayOnStart.get(userId, Display.INVALID_DISPLAY) == displayId) { + // User assigned to display on start + return true; + } + // Check for extra assignment + return mExtraDisplaysAssignedToUsers.get(displayId, USER_NULL) == userId; } } @@ -465,24 +593,34 @@ public final class UserVisibilityMediator implements Dumpable { } synchronized (mLock) { - return mUsersOnDisplaysMap.get(userId, Display.INVALID_DISPLAY); + return mUsersAssignedToDisplayOnStart.get(userId, Display.INVALID_DISPLAY); } } /** * See {@link UserManagerInternal#getUserAssignedToDisplay(int)}. */ - public int getUserAssignedToDisplay(@UserIdInt int displayId) { - if (displayId == Display.DEFAULT_DISPLAY || !mVisibleBackgroundUsersEnabled) { + public @UserIdInt int getUserAssignedToDisplay(@UserIdInt int displayId) { + return getUserAssignedToDisplay(displayId, /* returnCurrentUserByDefault= */ true); + } + + /** + * Gets the user explicitly assigned to a display, or the current user when no user is assigned + * to it (and {@code returnCurrentUserByDefault} is {@code true}). + */ + private @UserIdInt int getUserAssignedToDisplay(@UserIdInt int displayId, + boolean returnCurrentUserByDefault) { + if (returnCurrentUserByDefault + && (displayId == Display.DEFAULT_DISPLAY || !mVisibleBackgroundUsersEnabled)) { return getCurrentUserId(); } synchronized (mLock) { - for (int i = 0; i < mUsersOnDisplaysMap.size(); i++) { - if (mUsersOnDisplaysMap.valueAt(i) != displayId) { + for (int i = 0; i < mUsersAssignedToDisplayOnStart.size(); i++) { + if (mUsersAssignedToDisplayOnStart.valueAt(i) != displayId) { continue; } - int userId = mUsersOnDisplaysMap.keyAt(i); + int userId = mUsersAssignedToDisplayOnStart.keyAt(i); if (!isStartedProfile(userId)) { return userId; } else if (DBG) { @@ -491,6 +629,13 @@ public final class UserVisibilityMediator implements Dumpable { } } } + if (!returnCurrentUserByDefault) { + if (DBG) { + Slogf.d(TAG, "getUserAssignedToDisplay(%d): no user assigned to display, returning " + + "USER_NULL instead", displayId); + } + return USER_NULL; + } int currentUserId = getCurrentUserId(); if (DBG) { @@ -618,9 +763,11 @@ public final class UserVisibilityMediator implements Dumpable { ipw.print("Supports visible background users on displays: "); ipw.println(mVisibleBackgroundUsersEnabled); - if (mUsersOnDisplaysMap != null) { - dumpSparseIntArray(ipw, mUsersOnDisplaysMap, "user / display", "u", "d"); - } + dumpSparseIntArray(ipw, mUsersAssignedToDisplayOnStart, "user / display", "u", "d"); + + dumpSparseIntArray(ipw, mExtraDisplaysAssignedToUsers, "extra display / user", + "d", "u"); + int numberListeners = mListeners.size(); ipw.print("Number of listeners: "); ipw.println(numberListeners); @@ -638,8 +785,14 @@ public final class UserVisibilityMediator implements Dumpable { ipw.decreaseIndent(); } - private static void dumpSparseIntArray(IndentingPrintWriter ipw, SparseIntArray array, + private static void dumpSparseIntArray(IndentingPrintWriter ipw, @Nullable SparseIntArray array, String arrayDescription, String keyName, String valueName) { + if (array == null) { + ipw.print("No "); + ipw.print(arrayDescription); + ipw.println(" mappings"); + return; + } ipw.print("Number of "); ipw.print(arrayDescription); ipw.print(" mappings: "); diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java index 3d4f7b0e4ecc..7f0c3f9f4f06 100644 --- a/services/core/java/com/android/server/pm/dex/DexManager.java +++ b/services/core/java/com/android/server/pm/dex/DexManager.java @@ -126,20 +126,21 @@ public class DexManager { private static int DEX_SEARCH_FOUND_SECONDARY = 3; // dex file is a secondary dex public DexManager(Context context, PackageDexOptimizer pdo, Installer installer, - Object installLock) { - this(context, pdo, installer, installLock, null); + Object installLock, DynamicCodeLogger dynamicCodeLogger) { + this(context, pdo, installer, installLock, dynamicCodeLogger, null); } @VisibleForTesting public DexManager(Context context, PackageDexOptimizer pdo, Installer installer, - Object installLock, @Nullable IPackageManager packageManager) { + Object installLock, DynamicCodeLogger dynamicCodeLogger, + @Nullable IPackageManager packageManager) { mContext = context; mPackageCodeLocationsCache = new HashMap<>(); mPackageDexUsage = new PackageDexUsage(); mPackageDexOptimizer = pdo; mInstaller = installer; mInstallLock = installLock; - mDynamicCodeLogger = new DynamicCodeLogger(installer); + mDynamicCodeLogger = dynamicCodeLogger; mPackageManager = packageManager; // This is currently checked to handle tests that pass in a null context. @@ -169,10 +170,6 @@ public class DexManager { return mPackageManager; } - public DynamicCodeLogger getDynamicCodeLogger() { - return mDynamicCodeLogger; - } - /** * Notify about dex files loads. * Note that this method is invoked when apps load dex files and it should @@ -328,7 +325,6 @@ public class DexManager { loadInternal(existingPackages); } catch (RuntimeException e) { mPackageDexUsage.clear(); - mDynamicCodeLogger.clear(); Slog.w(TAG, "Exception while loading. Starting with a fresh state.", e); } } @@ -379,12 +375,10 @@ public class DexManager { if (mPackageDexUsage.removePackage(packageName)) { mPackageDexUsage.maybeWriteAsync(); } - mDynamicCodeLogger.removePackage(packageName); } else { if (mPackageDexUsage.removeUserPackage(packageName, userId)) { mPackageDexUsage.maybeWriteAsync(); } - mDynamicCodeLogger.removeUserPackage(packageName, userId); } } @@ -463,14 +457,6 @@ public class DexManager { Slog.w(TAG, "Exception while loading package dex usage. " + "Starting with a fresh state.", e); } - - try { - mDynamicCodeLogger.readAndSync(packageToUsersMap); - } catch (RuntimeException e) { - mDynamicCodeLogger.clear(); - Slog.w(TAG, "Exception while loading package dynamic code usage. " - + "Starting with a fresh state.", e); - } } /** @@ -819,7 +805,6 @@ public class DexManager { */ public void writePackageDexUsageNow() { mPackageDexUsage.writeNow(); - mDynamicCodeLogger.writeNow(); } /** diff --git a/services/core/java/com/android/server/pm/dex/DynamicCodeLogger.java b/services/core/java/com/android/server/pm/dex/DynamicCodeLogger.java index 9b94e993f967..da8fafaca5d8 100644 --- a/services/core/java/com/android/server/pm/dex/DynamicCodeLogger.java +++ b/services/core/java/com/android/server/pm/dex/DynamicCodeLogger.java @@ -43,6 +43,9 @@ import libcore.util.HexEncoding; import java.io.File; import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -64,7 +67,7 @@ public class DynamicCodeLogger { private final PackageDynamicCodeLoading mPackageDynamicCodeLoading; private final Installer mInstaller; - DynamicCodeLogger(Installer installer) { + public DynamicCodeLogger(Installer installer) { mInstaller = installer; mPackageDynamicCodeLoading = new PackageDynamicCodeLoading(); } @@ -220,8 +223,12 @@ public class DynamicCodeLogger { EventLog.writeEvent(SNET_TAG, subtag, uid, message); } - void recordDex(int loaderUserId, String dexPath, String owningPackageName, - String loadingPackageName) { + /** + * Records that an app running in the specified uid has executed dex code from the file at + * {@code path}. + */ + public void recordDex( + int loaderUserId, String dexPath, String owningPackageName, String loadingPackageName) { if (mPackageDynamicCodeLoading.record(owningPackageName, dexPath, FILE_TYPE_DEX, loaderUserId, loadingPackageName)) { mPackageDynamicCodeLoading.maybeWriteAsync(); @@ -229,8 +236,8 @@ public class DynamicCodeLogger { } /** - * Record that an app running in the specified uid has executed native code from the file at - * {@param path}. + * Records that an app running in the specified uid has executed native code from the file at + * {@code path}. */ public void recordNative(int loadingUid, String path) { String[] packages; @@ -274,7 +281,39 @@ public class DynamicCodeLogger { mPackageDynamicCodeLoading.syncData(packageToUsersMap); } - void writeNow() { + /** Writes the in-memory dynamic code information to disk right away. */ + public void writeNow() { mPackageDynamicCodeLoading.writeNow(); } + + /** Reads the dynamic code information from disk. */ + public void load(Map<Integer, List<PackageInfo>> userToPackagesMap) { + // Compute a reverse map. + Map<String, Set<Integer>> packageToUsersMap = new HashMap<>(); + for (Map.Entry<Integer, List<PackageInfo>> entry : userToPackagesMap.entrySet()) { + List<PackageInfo> packageInfoList = entry.getValue(); + int userId = entry.getKey(); + for (PackageInfo pi : packageInfoList) { + Set<Integer> users = + packageToUsersMap.computeIfAbsent(pi.packageName, k -> new HashSet<>()); + users.add(userId); + } + } + + readAndSync(packageToUsersMap); + } + + /** + * Notifies that the user {@code userId} data for package {@code packageName} was destroyed. + * This will remove all dynamic code information associated with the package for the given user. + * {@code userId} is allowed to be {@code UserHandle.USER_ALL} in which case + * all dynamic code information for the package will be removed. + */ + public void notifyPackageDataDestroyed(String packageName, int userId) { + if (userId == UserHandle.USER_ALL) { + removePackage(packageName); + } else { + removeUserPackage(packageName, userId); + } + } } diff --git a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java index 1778e57ce4ae..d7c4a09d045c 100644 --- a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java +++ b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java @@ -1810,6 +1810,11 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override + public boolean isAllowUpdateOwnership() { + return getBoolean2(Booleans2.ALLOW_UPDATE_OWNERSHIP); + } + + @Override public boolean isVmSafeMode() { return getBoolean(Booleans.VM_SAFE_MODE); } @@ -2513,6 +2518,11 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override + public PackageImpl setAllowUpdateOwnership(boolean value) { + return setBoolean2(Booleans2.ALLOW_UPDATE_OWNERSHIP, value); + } + + @Override public PackageImpl sortActivities() { Collections.sort(this.activities, ORDER_COMPARATOR); return this; @@ -3726,5 +3736,6 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, private static final long STUB = 1L; private static final long APEX = 1L << 1; + private static final long ALLOW_UPDATE_OWNERSHIP = 1L << 2; } } diff --git a/services/core/java/com/android/server/pm/permission/Permission.java b/services/core/java/com/android/server/pm/permission/Permission.java index f3b9246de371..c81d6d7d0918 100644 --- a/services/core/java/com/android/server/pm/permission/Permission.java +++ b/services/core/java/com/android/server/pm/permission/Permission.java @@ -105,6 +105,15 @@ public final class Permission { mType = type; } + public Permission(@NonNull PermissionInfo permissionInfo, @PermissionType int type, + boolean reconciled, int uid, int[] gids, boolean gidsPerUser) { + this(permissionInfo, type); + mReconciled = reconciled; + mUid = uid; + mGids = gids; + mGidsPerUser = gidsPerUser; + } + @NonNull public PermissionInfo getPermissionInfo() { return mPermissionInfo; diff --git a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java index 78091bc49449..ad738730598f 100644 --- a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java +++ b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java @@ -1483,4 +1483,10 @@ public interface AndroidPackage { * @hide */ boolean isVisibleToInstantApps(); + + /** + * @see R.styleable#AndroidManifest_allowUpdateOwnership + * @hide + */ + boolean isAllowUpdateOwnership(); } diff --git a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java index 4a8ef963959b..5947d4735faa 100644 --- a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java +++ b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java @@ -291,6 +291,15 @@ public class PackageStateMutator { return this; } + @NonNull + @Override + public PackageStateWrite setUpdateOwner(@NonNull String updateOwnerPackageName) { + if (mState != null) { + mState.setUpdateOwnerPackage(updateOwnerPackageName); + } + return this; + } + private static class UserStateWriteWrapper implements PackageUserStateWrite { @Nullable diff --git a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateWrite.java b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateWrite.java index dc9cd3b6ceb7..c610c02a6e9c 100644 --- a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateWrite.java +++ b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateWrite.java @@ -57,4 +57,7 @@ public interface PackageStateWrite { @NonNull PackageStateWrite setInstaller(@Nullable String installerPackageName, int installerPackageUid); + + @NonNull + PackageStateWrite setUpdateOwner(@Nullable String updateOwnerPackageName); } diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java index 69f2716a6c6e..bb36758f1e77 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java @@ -387,6 +387,8 @@ public interface ParsingPackage { ParsingPackage setLocaleConfigRes(int localeConfigRes); + ParsingPackage setAllowUpdateOwnership(boolean value); + /** * Sets the trusted host certificates of apps that are allowed to embed activities of this * application. 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 995b9e58b3e3..31f291fa948d 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 @@ -219,6 +219,7 @@ public class ParsingPackageUtils { public static final int PARSE_DEFAULT_INSTALL_LOCATION = PackageInfo.INSTALL_LOCATION_UNSPECIFIED; public static final int PARSE_DEFAULT_TARGET_SANDBOX = 1; + public static final boolean PARSE_DEFAULT_ALLOW_UPDATE_OWNERSHIP = true; /** * If set to true, we will only allow package files that exactly match the DTD. Otherwise, we @@ -247,6 +248,9 @@ public class ParsingPackageUtils { private static final String MAX_NUM_COMPONENTS_ERR_MSG = "Total number of components has exceeded the maximum number: " + MAX_NUM_COMPONENTS; + /** The maximum permission name length. */ + private static final int MAX_PERMISSION_NAME_LENGTH = 512; + @IntDef(flag = true, prefix = { "PARSE_" }, value = { PARSE_CHATTY, PARSE_COLLECT_CERTIFICATES, @@ -883,7 +887,9 @@ public class ParsingPackageUtils { .setTargetSandboxVersion(anInteger(PARSE_DEFAULT_TARGET_SANDBOX, R.styleable.AndroidManifest_targetSandboxVersion, sa)) /* Set the global "on SD card" flag */ - .setExternalStorage((flags & PARSE_EXTERNAL_STORAGE) != 0); + .setExternalStorage((flags & PARSE_EXTERNAL_STORAGE) != 0) + .setAllowUpdateOwnership(bool(PARSE_DEFAULT_ALLOW_UPDATE_OWNERSHIP, + R.styleable.AndroidManifest_allowUpdateOwnership, sa)); boolean foundApp = false; final int depth = parser.getDepth(); @@ -1260,6 +1266,11 @@ public class ParsingPackageUtils { // that may change. String name = sa.getNonResourceString( R.styleable.AndroidManifestUsesPermission_name); + if (TextUtils.length(name) > MAX_PERMISSION_NAME_LENGTH) { + return input.error(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + "The name in the <uses-permission> is greater than " + + MAX_PERMISSION_NAME_LENGTH); + } int minSdkVersion = parseMinOrMaxSdkVersion(sa, R.styleable.AndroidManifestUsesPermission_minSdkVersion, diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java index 868f34bf45ce..0e92709e25f6 100644 --- a/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java +++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java @@ -21,10 +21,12 @@ import android.os.OperationCanceledException; import android.os.OutcomeReceiver; import android.security.rkp.IGetKeyCallback; import android.security.rkp.IRegistration; +import android.security.rkp.IStoreUpgradedKeyCallback; import android.security.rkp.service.RegistrationProxy; import android.security.rkp.service.RemotelyProvisionedKey; import android.util.Log; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; @@ -36,8 +38,10 @@ import java.util.concurrent.Executor; */ final class RemoteProvisioningRegistration extends IRegistration.Stub { static final String TAG = RemoteProvisioningService.TAG; - private final ConcurrentHashMap<IGetKeyCallback, CancellationSignal> mOperations = + private final ConcurrentHashMap<IGetKeyCallback, CancellationSignal> mGetKeyOperations = new ConcurrentHashMap<>(); + private final Set<IStoreUpgradedKeyCallback> mStoreUpgradedKeyOperations = + ConcurrentHashMap.newKeySet(); private final RegistrationProxy mRegistration; private final Executor mExecutor; @@ -49,7 +53,7 @@ final class RemoteProvisioningRegistration extends IRegistration.Stub { @Override public void onResult(RemotelyProvisionedKey result) { - mOperations.remove(mCallback); + mGetKeyOperations.remove(mCallback); Log.i(TAG, "Successfully fetched key for client " + mCallback.hashCode()); android.security.rkp.RemotelyProvisionedKey parcelable = new android.security.rkp.RemotelyProvisionedKey(); @@ -60,7 +64,7 @@ final class RemoteProvisioningRegistration extends IRegistration.Stub { @Override public void onError(Exception e) { - mOperations.remove(mCallback); + mGetKeyOperations.remove(mCallback); if (e instanceof OperationCanceledException) { Log.i(TAG, "Operation cancelled for client " + mCallback.hashCode()); wrapCallback(mCallback::onCancel); @@ -79,7 +83,7 @@ final class RemoteProvisioningRegistration extends IRegistration.Stub { @Override public void getKey(int keyId, IGetKeyCallback callback) { CancellationSignal cancellationSignal = new CancellationSignal(); - if (mOperations.putIfAbsent(callback, cancellationSignal) != null) { + if (mGetKeyOperations.putIfAbsent(callback, cancellationSignal) != null) { Log.e(TAG, "Client can only request one call at a time " + callback.hashCode()); throw new IllegalArgumentException( "Callback is already associated with an existing operation: " @@ -92,14 +96,14 @@ final class RemoteProvisioningRegistration extends IRegistration.Stub { new GetKeyReceiver(callback)); } catch (Exception e) { Log.e(TAG, "getKeyAsync threw an exception for client " + callback.hashCode(), e); - mOperations.remove(callback); + mGetKeyOperations.remove(callback); wrapCallback(() -> callback.onError(e.getMessage())); } } @Override public void cancelGetKey(IGetKeyCallback callback) { - CancellationSignal cancellationSignal = mOperations.remove(callback); + CancellationSignal cancellationSignal = mGetKeyOperations.remove(callback); if (cancellationSignal == null) { throw new IllegalArgumentException( "Invalid client in cancelGetKey: " + callback.hashCode()); @@ -110,9 +114,35 @@ final class RemoteProvisioningRegistration extends IRegistration.Stub { } @Override - public void storeUpgradedKey(byte[] oldKeyBlob, byte[] newKeyBlob) { - // TODO(b/262748535) - Log.e(TAG, "RegistrationBinder.storeUpgradedKey NOT YET IMPLEMENTED"); + public void storeUpgradedKeyAsync(byte[] oldKeyBlob, byte[] newKeyBlob, + IStoreUpgradedKeyCallback callback) { + if (!mStoreUpgradedKeyOperations.add(callback)) { + throw new IllegalArgumentException( + "Callback is already associated with an existing operation: " + + callback.hashCode()); + } + + try { + mRegistration.storeUpgradedKeyAsync(oldKeyBlob, newKeyBlob, mExecutor, + new OutcomeReceiver<>() { + @Override + public void onResult(Void result) { + mStoreUpgradedKeyOperations.remove(callback); + wrapCallback(callback::onSuccess); + } + + @Override + public void onError(Exception e) { + mStoreUpgradedKeyOperations.remove(callback); + wrapCallback(() -> callback.onError(e.getMessage())); + } + }); + } catch (Exception e) { + Log.e(TAG, "storeUpgradedKeyAsync threw an exception for client " + + callback.hashCode(), e); + mStoreUpgradedKeyOperations.remove(callback); + wrapCallback(() -> callback.onError(e.getMessage())); + } } interface CallbackRunner { diff --git a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java index 41824de5953c..b0d301e42634 100644 --- a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java +++ b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java @@ -16,6 +16,7 @@ package com.android.server.timedetector; +import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -39,12 +40,14 @@ import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.SystemClock; import android.provider.Settings; +import android.util.IndentingPrintWriter; import android.util.LocalLog; import android.util.Log; import android.util.NtpTrustedTime; import android.util.NtpTrustedTime.TimeResult; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; import com.android.server.LocalServices; @@ -52,12 +55,17 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.time.Duration; import java.util.Objects; +import java.util.function.Supplier; /** - * Monitors the network time. If looking up the network time fails for some reason, it tries a few - * times with a short interval and then resets to checking on longer intervals. + * Refreshes network time periodically, when network connectivity becomes available and when the + * user enables automatic time detection. * - * <p>When available, the time is always suggested to the {@link + * <p>For periodic requests, this service attempts to leave an interval between successful requests. + * If a request fails, it retries a number of times with a "short" interval and then resets to the + * normal interval. The process then repeats. + * + * <p>When a valid network time is available, the time is always suggested to the {@link * com.android.server.timedetector.TimeDetectorService} where it may be used to set the device * system clock, depending on user settings and what other signals are available. */ @@ -72,25 +80,11 @@ public class NetworkTimeUpdateService extends Binder { private final Object mLock = new Object(); private final Context mContext; - private final NtpTrustedTime mNtpTrustedTime; - private final AlarmManager mAlarmManager; - private final TimeDetectorInternal mTimeDetectorInternal; private final ConnectivityManager mCM; - private final PendingIntent mPendingPollIntent; private final PowerManager.WakeLock mWakeLock; - - // Normal polling frequency - private final int mNormalPollingIntervalMillis; - // Try-again polling interval, in case the network request failed - private final int mShortPollingIntervalMillis; - // Number of times to try again - private final int mTryAgainTimesMax; - - /** - * A log that records the decisions to fetch a network time update. - * This is logged in bug reports to assist with debugging issues with network time suggestions. - */ - private final LocalLog mLocalLog = new LocalLog(30, false /* useLocalTimestamps */); + private final NtpTrustedTime mNtpTrustedTime; + private final Engine.RefreshCallbacks mRefreshCallbacks; + private final Engine mEngine; // Blocking NTP lookup is done using this handler private final Handler mHandler; @@ -100,33 +94,43 @@ public class NetworkTimeUpdateService extends Binder { @Nullable private Network mDefaultNetwork = null; - // Keeps track of how many quick attempts were made to fetch NTP time. - // During bootup, the network may not have been up yet, or it's taking time for the - // connection to happen. - // This field is only updated and accessed by the mHandler thread (except dump()). - @GuardedBy("mLock") - private int mTryAgainCounter; - public NetworkTimeUpdateService(@NonNull Context context) { mContext = Objects.requireNonNull(context); - mAlarmManager = mContext.getSystemService(AlarmManager.class); - mTimeDetectorInternal = LocalServices.getService(TimeDetectorInternal.class); mCM = mContext.getSystemService(ConnectivityManager.class); mWakeLock = context.getSystemService(PowerManager.class).newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, TAG); mNtpTrustedTime = NtpTrustedTime.getInstance(context); - mTryAgainTimesMax = mContext.getResources().getInteger( + Supplier<Long> elapsedRealtimeMillisSupplier = SystemClock::elapsedRealtime; + int tryAgainTimesMax = mContext.getResources().getInteger( com.android.internal.R.integer.config_ntpRetry); - mNormalPollingIntervalMillis = mContext.getResources().getInteger( + int normalPollingIntervalMillis = mContext.getResources().getInteger( com.android.internal.R.integer.config_ntpPollingInterval); - mShortPollingIntervalMillis = mContext.getResources().getInteger( + int shortPollingIntervalMillis = mContext.getResources().getInteger( com.android.internal.R.integer.config_ntpPollingIntervalShorter); + mEngine = new EngineImpl(elapsedRealtimeMillisSupplier, normalPollingIntervalMillis, + shortPollingIntervalMillis, tryAgainTimesMax, mNtpTrustedTime); + AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class); + TimeDetectorInternal timeDetectorInternal = + LocalServices.getService(TimeDetectorInternal.class); // Broadcast alarms sent by system are immutable Intent pollIntent = new Intent(ACTION_POLL, null); - mPendingPollIntent = PendingIntent.getBroadcast(mContext, POLL_REQUEST, + PendingIntent pendingPollIntent = PendingIntent.getBroadcast(mContext, POLL_REQUEST, pollIntent, PendingIntent.FLAG_IMMUTABLE); + mRefreshCallbacks = new Engine.RefreshCallbacks() { + @Override + public void scheduleNextRefresh(@ElapsedRealtimeLong long elapsedRealtimeMillis) { + alarmManager.cancel(pendingPollIntent); + alarmManager.set( + AlarmManager.ELAPSED_REALTIME, elapsedRealtimeMillis, pendingPollIntent); + } + + @Override + public void submitSuggestion(NetworkTimeSuggestion suggestion) { + timeDetectorInternal.suggestNetworkTime(suggestion); + } + }; HandlerThread thread = new HandlerThread(TAG); thread.start(); @@ -217,12 +221,7 @@ public class NetworkTimeUpdateService extends Binder { } if (network == null) return false; - boolean success = mNtpTrustedTime.forceRefresh(network); - if (success) { - makeNetworkTimeSuggestion(mNtpTrustedTime.getCachedTimeResult(), - "Origin: NetworkTimeUpdateService: forceRefreshForTests"); - } - return success; + return mEngine.forceRefreshForTests(network, mRefreshCallbacks); } finally { Binder.restoreCallingIdentity(token); } @@ -238,96 +237,12 @@ public class NetworkTimeUpdateService extends Binder { mWakeLock.acquire(); try { - onPollNetworkTimeUnderWakeLock(network, reason); + mEngine.refreshIfRequiredAndReschedule(network, reason, mRefreshCallbacks); } finally { mWakeLock.release(); } } - private void onPollNetworkTimeUnderWakeLock( - @NonNull Network network, @NonNull String reason) { - long currentElapsedRealtimeMillis = SystemClock.elapsedRealtime(); - - final int maxNetworkTimeAgeMillis = mNormalPollingIntervalMillis; - // Force an NTP fix when outdated - NtpTrustedTime.TimeResult cachedNtpResult = mNtpTrustedTime.getCachedTimeResult(); - if (cachedNtpResult == null - || cachedNtpResult.getAgeMillis(currentElapsedRealtimeMillis) - >= maxNetworkTimeAgeMillis) { - if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh using network=" + network); - boolean success = mNtpTrustedTime.forceRefresh(network); - if (success) { - synchronized (mLock) { - mTryAgainCounter = 0; - } - } else { - String logMsg = "forceRefresh() returned false:" - + " cachedNtpResult=" + cachedNtpResult - + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis; - - if (DBG) { - Log.d(TAG, logMsg); - } - mLocalLog.log(logMsg); - } - - cachedNtpResult = mNtpTrustedTime.getCachedTimeResult(); - } - - if (cachedNtpResult != null - && cachedNtpResult.getAgeMillis(currentElapsedRealtimeMillis) - < maxNetworkTimeAgeMillis) { - // Obtained fresh fix; schedule next normal update - scheduleNextRefresh(mNormalPollingIntervalMillis - - cachedNtpResult.getAgeMillis(currentElapsedRealtimeMillis)); - - makeNetworkTimeSuggestion(cachedNtpResult, reason); - } else { - synchronized (mLock) { - // No fresh fix; schedule retry - mTryAgainCounter++; - if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) { - scheduleNextRefresh(mShortPollingIntervalMillis); - } else { - // Try much later - String logMsg = "mTryAgainTimesMax exceeded," - + " cachedNtpResult=" + cachedNtpResult; - if (DBG) { - Log.d(TAG, logMsg); - } - mLocalLog.log(logMsg); - mTryAgainCounter = 0; - - scheduleNextRefresh(mNormalPollingIntervalMillis); - } - } - } - } - - /** Suggests the time to the time detector. It may choose use it to set the system clock. */ - private void makeNetworkTimeSuggestion( - @NonNull TimeResult ntpResult, @NonNull String debugInfo) { - UnixEpochTime timeSignal = new UnixEpochTime( - ntpResult.getElapsedRealtimeMillis(), ntpResult.getTimeMillis()); - NetworkTimeSuggestion timeSuggestion = - new NetworkTimeSuggestion(timeSignal, ntpResult.getUncertaintyMillis()); - timeSuggestion.addDebugInfo(debugInfo); - timeSuggestion.addDebugInfo(ntpResult.toString()); - mTimeDetectorInternal.suggestNetworkTime(timeSuggestion); - } - - /** - * Cancel old alarm and starts a new one for the specified interval. - * - * @param delayMillis when to trigger the alarm, starting from now. - */ - private void scheduleNextRefresh(long delayMillis) { - mAlarmManager.cancel(mPendingPollIntent); - long now = SystemClock.elapsedRealtime(); - long next = now + delayMillis; - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, next, mPendingPollIntent); - } - // All callbacks will be invoked using mHandler because of how the callback is registered. private class NetworkTimeUpdateCallback extends NetworkCallback { @Override @@ -385,21 +300,10 @@ public class NetworkTimeUpdateService extends Binder { protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; - pw.println("mNormalPollingIntervalMillis=" - + Duration.ofMillis(mNormalPollingIntervalMillis)); - pw.println("mShortPollingIntervalMillis=" - + Duration.ofMillis(mShortPollingIntervalMillis)); - pw.println("mTryAgainTimesMax=" + mTryAgainTimesMax); synchronized (mLock) { pw.println("mDefaultNetwork=" + mDefaultNetwork); - pw.println("mTryAgainCounter=" + mTryAgainCounter); } - pw.println(); - pw.println("NtpTrustedTime:"); - mNtpTrustedTime.dump(pw); - pw.println(); - pw.println("Local logs:"); - mLocalLog.dump(fd, pw, args); + mEngine.dump(pw); pw.println(); } @@ -409,4 +313,204 @@ public class NetworkTimeUpdateService extends Binder { new NetworkTimeUpdateServiceShellCommand(this).exec( this, in, out, err, args, callback, resultReceiver); } + + /** + * The interface the service uses to interact with the time refresh logic. + * Extracted for testing. + */ + @VisibleForTesting + interface Engine { + interface RefreshCallbacks { + void scheduleNextRefresh(@ElapsedRealtimeLong long elapsedRealtimeMillis); + + void submitSuggestion(@NonNull NetworkTimeSuggestion suggestion); + } + + /** + * Forces the engine to refresh the network time (for tests). See {@link + * NetworkTimeUpdateService#forceRefreshForTests()}. This is a blocking call. This method + * must not schedule any calls. + */ + boolean forceRefreshForTests( + @NonNull Network network, @NonNull RefreshCallbacks refreshCallbacks); + + /** + * Attempts to refresh the network time if required, i.e. if there isn't a recent-enough + * network time available. It must also schedule the next call. This is a blocking call. + * + * @param network the network to use + * @param reason the reason for the refresh (for logging) + */ + void refreshIfRequiredAndReschedule(@NonNull Network network, @NonNull String reason, + @NonNull RefreshCallbacks refreshCallbacks); + + void dump(@NonNull PrintWriter pw); + } + + @VisibleForTesting + static class EngineImpl implements Engine { + + /** + * A log that records the decisions to fetch a network time update. + * This is logged in bug reports to assist with debugging issues with network time + * suggestions. + */ + @NonNull + private final LocalLog mLocalDebugLog = new LocalLog(30, false /* useLocalTimestamps */); + + private final int mNormalPollingIntervalMillis; + private final int mShortPollingIntervalMillis; + private final int mTryAgainTimesMax; + private final NtpTrustedTime mNtpTrustedTime; + + /** + * Keeps track of successive time refresh failures have occurred. This is reset to zero when + * time refresh is successful or if the number exceeds (a non-negative) {@link + * #mTryAgainTimesMax}. + */ + @GuardedBy("this") + private int mTryAgainCounter; + + private final Supplier<Long> mElapsedRealtimeMillisSupplier; + + @VisibleForTesting + EngineImpl(@NonNull Supplier<Long> elapsedRealtimeMillisSupplier, + int normalPollingIntervalMillis, int shortPollingIntervalMillis, + int tryAgainTimesMax, @NonNull NtpTrustedTime ntpTrustedTime) { + mElapsedRealtimeMillisSupplier = Objects.requireNonNull(elapsedRealtimeMillisSupplier); + mNormalPollingIntervalMillis = normalPollingIntervalMillis; + mShortPollingIntervalMillis = shortPollingIntervalMillis; + mTryAgainTimesMax = tryAgainTimesMax; + mNtpTrustedTime = Objects.requireNonNull(ntpTrustedTime); + } + + @Override + public boolean forceRefreshForTests( + @NonNull Network network, @NonNull RefreshCallbacks refreshCallbacks) { + boolean success = mNtpTrustedTime.forceRefresh(network); + logToDebugAndDumpsys("forceRefreshForTests: success=" + success); + + if (success) { + makeNetworkTimeSuggestion(mNtpTrustedTime.getCachedTimeResult(), + "EngineImpl.forceRefreshForTests()", refreshCallbacks); + } + return success; + } + + @Override + public void refreshIfRequiredAndReschedule( + @NonNull Network network, @NonNull String reason, + @NonNull RefreshCallbacks refreshCallbacks) { + long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get(); + + final int maxNetworkTimeAgeMillis = mNormalPollingIntervalMillis; + // Force an NTP fix when outdated + NtpTrustedTime.TimeResult initialTimeResult = mNtpTrustedTime.getCachedTimeResult(); + if (calculateTimeResultAgeMillis(initialTimeResult, currentElapsedRealtimeMillis) + >= maxNetworkTimeAgeMillis) { + if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh using network=" + network); + boolean successful = mNtpTrustedTime.forceRefresh(network); + if (successful) { + synchronized (this) { + mTryAgainCounter = 0; + } + } else { + String logMsg = "forceRefresh() returned false:" + + " initialTimeResult=" + initialTimeResult + + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis; + logToDebugAndDumpsys(logMsg); + } + } + + synchronized (this) { + long nextPollDelayMillis; + NtpTrustedTime.TimeResult latestTimeResult = mNtpTrustedTime.getCachedTimeResult(); + if (calculateTimeResultAgeMillis(latestTimeResult, currentElapsedRealtimeMillis) + < maxNetworkTimeAgeMillis) { + // Obtained fresh fix; schedule next normal update + nextPollDelayMillis = mNormalPollingIntervalMillis + - latestTimeResult.getAgeMillis(currentElapsedRealtimeMillis); + + makeNetworkTimeSuggestion(latestTimeResult, reason, refreshCallbacks); + } else { + // No fresh fix; schedule retry + mTryAgainCounter++; + if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) { + nextPollDelayMillis = mShortPollingIntervalMillis; + } else { + // Try much later + mTryAgainCounter = 0; + + nextPollDelayMillis = mNormalPollingIntervalMillis; + } + } + long nextRefreshElapsedRealtimeMillis = + currentElapsedRealtimeMillis + nextPollDelayMillis; + refreshCallbacks.scheduleNextRefresh(nextRefreshElapsedRealtimeMillis); + + logToDebugAndDumpsys("refreshIfRequiredAndReschedule:" + + " network=" + network + + ", reason=" + reason + + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis + + ", initialTimeResult=" + initialTimeResult + + ", latestTimeResult=" + latestTimeResult + + ", mTryAgainCounter=" + mTryAgainCounter + + ", nextPollDelayMillis=" + nextPollDelayMillis + + ", nextRefreshElapsedRealtimeMillis=" + + Duration.ofMillis(nextRefreshElapsedRealtimeMillis) + + " (" + nextRefreshElapsedRealtimeMillis + ")"); + } + } + + private static long calculateTimeResultAgeMillis( + @Nullable TimeResult timeResult, + @ElapsedRealtimeLong long currentElapsedRealtimeMillis) { + return timeResult == null ? Long.MAX_VALUE + : timeResult.getAgeMillis(currentElapsedRealtimeMillis); + } + + /** Suggests the time to the time detector. It may choose use it to set the system clock. */ + private void makeNetworkTimeSuggestion(@NonNull TimeResult ntpResult, + @NonNull String debugInfo, @NonNull RefreshCallbacks refreshCallbacks) { + UnixEpochTime timeSignal = new UnixEpochTime( + ntpResult.getElapsedRealtimeMillis(), ntpResult.getTimeMillis()); + NetworkTimeSuggestion timeSuggestion = + new NetworkTimeSuggestion(timeSignal, ntpResult.getUncertaintyMillis()); + timeSuggestion.addDebugInfo(debugInfo); + timeSuggestion.addDebugInfo(ntpResult.toString()); + refreshCallbacks.submitSuggestion(timeSuggestion); + } + + @Override + public void dump(PrintWriter pw) { + IndentingPrintWriter ipw = new IndentingPrintWriter(pw); + ipw.println("mNormalPollingIntervalMillis=" + mNormalPollingIntervalMillis); + ipw.println("mShortPollingIntervalMillis=" + mShortPollingIntervalMillis); + ipw.println("mTryAgainTimesMax=" + mTryAgainTimesMax); + + synchronized (this) { + ipw.println("mTryAgainCounter=" + mTryAgainCounter); + } + ipw.println(); + + ipw.println("NtpTrustedTime:"); + ipw.increaseIndent(); + mNtpTrustedTime.dump(ipw); + ipw.decreaseIndent(); + ipw.println(); + + ipw.println("Debug log:"); + ipw.increaseIndent(); + mLocalDebugLog.dump(ipw); + ipw.decreaseIndent(); + ipw.println(); + } + + private void logToDebugAndDumpsys(String logMsg) { + if (DBG) { + Log.d(TAG, logMsg); + } + mLocalDebugLog.log(logMsg); + } + } } diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java index 1be9074e079a..3e2395303354 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorService.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorService.java @@ -143,7 +143,7 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub return getTimeCapabilitiesAndConfig(userId); } - TimeCapabilitiesAndConfig getTimeCapabilitiesAndConfig(@UserIdInt int userId) { + private TimeCapabilitiesAndConfig getTimeCapabilitiesAndConfig(@UserIdInt int userId) { enforceManageTimeDetectorPermission(); final long token = mCallerIdentityInjector.clearCallingIdentity(); @@ -163,6 +163,9 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub return updateConfiguration(callingUserId, configuration); } + /** + * Updates the user's configuration. Exposed for use by {@link TimeDetectorShellCommand}. + */ boolean updateConfiguration(@UserIdInt int userId, @NonNull TimeConfiguration configuration) { // Resolve constants like USER_CURRENT to the true user ID as needed. int resolvedUserId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), @@ -256,7 +259,7 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub } } - void handleConfigurationInternalChangedOnHandlerThread() { + private void handleConfigurationInternalChangedOnHandlerThread() { // Configuration has changed, but each user may have a different view of the configuration. // It's possible that this will cause unnecessary notifications but that shouldn't be a // problem. @@ -287,6 +290,10 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub } } + /** + * Sets the system time state. See {@link TimeState} for details. For use by {@link + * TimeDetectorShellCommand}. + */ void setTimeState(@NonNull TimeState timeState) { enforceManageTimeDetectorPermission(); @@ -353,6 +360,9 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub } } + /** + * Suggests network time with permission checks. For use by {@link TimeDetectorShellCommand}. + */ void suggestNetworkTime(@NonNull NetworkTimeSuggestion timeSignal) { enforceSuggestNetworkTimePermission(); Objects.requireNonNull(timeSignal); @@ -360,6 +370,23 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub mHandler.post(() -> mTimeDetectorStrategy.suggestNetworkTime(timeSignal)); } + /** + * Clears the cached network time information. For use during tests to simulate when no network + * time has been made available. For use by {@link TimeDetectorShellCommand}. + * + * <p>This operation takes place in the calling thread. + */ + void clearNetworkTime() { + enforceSuggestNetworkTimePermission(); + + final long token = Binder.clearCallingIdentity(); + try { + mTimeDetectorStrategy.clearLatestNetworkSuggestion(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + @Override public UnixEpochTime latestNetworkTime() { NetworkTimeSuggestion suggestion = getLatestNetworkSuggestion(); @@ -388,6 +415,9 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub } } + /** + * Suggests GNSS time with permission checks. For use by {@link TimeDetectorShellCommand}. + */ void suggestGnssTime(@NonNull GnssTimeSuggestion timeSignal) { enforceSuggestGnssTimePermission(); Objects.requireNonNull(timeSignal); diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java b/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java index 990c00feae16..cce570986168 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java @@ -15,7 +15,9 @@ */ package com.android.server.timedetector; +import static android.app.timedetector.TimeDetector.SHELL_COMMAND_CLEAR_NETWORK_TIME; import static android.app.timedetector.TimeDetector.SHELL_COMMAND_CONFIRM_TIME; +import static android.app.timedetector.TimeDetector.SHELL_COMMAND_GET_NETWORK_TIME; import static android.app.timedetector.TimeDetector.SHELL_COMMAND_GET_TIME_STATE; import static android.app.timedetector.TimeDetector.SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED; import static android.app.timedetector.TimeDetector.SHELL_COMMAND_SERVICE_NAME; @@ -70,6 +72,10 @@ class TimeDetectorShellCommand extends ShellCommand { return runSuggestTelephonyTime(); case SHELL_COMMAND_SUGGEST_NETWORK_TIME: return runSuggestNetworkTime(); + case SHELL_COMMAND_GET_NETWORK_TIME: + return runGetNetworkTime(); + case SHELL_COMMAND_CLEAR_NETWORK_TIME: + return runClearNetworkTime(); case SHELL_COMMAND_SUGGEST_GNSS_TIME: return runSuggestGnssTime(); case SHELL_COMMAND_SUGGEST_EXTERNAL_TIME: @@ -122,6 +128,18 @@ class TimeDetectorShellCommand extends ShellCommand { mInterface::suggestNetworkTime); } + private int runGetNetworkTime() { + NetworkTimeSuggestion networkTimeSuggestion = mInterface.getLatestNetworkSuggestion(); + final PrintWriter pw = getOutPrintWriter(); + pw.println(networkTimeSuggestion); + return 0; + } + + private int runClearNetworkTime() { + mInterface.clearNetworkTime(); + return 0; + } + private int runSuggestGnssTime() { return runSuggestTime( () -> GnssTimeSuggestion.parseCommandLineArg(this), @@ -196,6 +214,10 @@ class TimeDetectorShellCommand extends ShellCommand { pw.printf(" Sets the current time state for tests.\n"); pw.printf(" %s <unix epoch time options>\n", SHELL_COMMAND_CONFIRM_TIME); pw.printf(" Tries to confirms the time, raising the confidence.\n"); + pw.printf(" %s\n", SHELL_COMMAND_GET_NETWORK_TIME); + pw.printf(" Prints the network time information held by the detector.\n"); + pw.printf(" %s\n", SHELL_COMMAND_CLEAR_NETWORK_TIME); + pw.printf(" Clears the network time information held by the detector.\n"); pw.println(); ManualTimeSuggestion.printCommandLineOpts(pw); pw.println(); diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java index 03f236d9b30d..9dca6ec26d29 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java @@ -18,6 +18,7 @@ package com.android.server.timedetector; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.time.ExternalTimeSuggestion; import android.app.time.TimeState; @@ -103,6 +104,20 @@ public interface TimeDetectorStrategy extends Dumpable { /** Processes the suggested time from network sources. */ void suggestNetworkTime(@NonNull NetworkTimeSuggestion timeSuggestion); + /** + * Returns the latest (accepted) network time suggestion. Returns {@code null} if there isn't + * one. + */ + @Nullable + NetworkTimeSuggestion getLatestNetworkSuggestion(); + + /** + * Clears the latest network time suggestion, leaving none. The remaining time signals from + * other sources will be reassessed causing the device's time to be updated if config and + * settings allow. + */ + void clearLatestNetworkSuggestion(); + /** Processes the suggested time from gnss sources. */ void suggestGnssTime(@NonNull GnssTimeSuggestion timeSuggestion); diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java index 13ec75329e39..09bb8036406d 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java @@ -316,6 +316,21 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { } @Override + @Nullable + public synchronized NetworkTimeSuggestion getLatestNetworkSuggestion() { + return mLastNetworkSuggestion.get(); + } + + @Override + public synchronized void clearLatestNetworkSuggestion() { + mLastNetworkSuggestion.set(null); + + // The loss of network time may change the time signal to use to set the system clock. + String reason = "Network time cleared"; + doAutoTimeDetection(reason); + } + + @Override @NonNull public synchronized TimeState getTimeState() { boolean userShouldConfirmTime = mEnvironment.systemClockConfidence() < TIME_CONFIDENCE_HIGH; @@ -1068,15 +1083,6 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { */ @VisibleForTesting @Nullable - public synchronized NetworkTimeSuggestion getLatestNetworkSuggestion() { - return mLastNetworkSuggestion.get(); - } - - /** - * A method used to inspect state during tests. Not intended for general use. - */ - @VisibleForTesting - @Nullable public synchronized GnssTimeSuggestion getLatestGnssSuggestion() { return mLastGnssSuggestion.get(); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9a7b16516bb4..45ae3d8210c8 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -727,6 +727,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** * When set to true, the IME insets will be frozen until the next app becomes IME input target. * @see InsetsPolicy#adjustVisibilityForIme + * @see ImeInsetsSourceProvider#updateClientVisibility */ boolean mImeInsetsFrozenUntilStartInput; @@ -1576,7 +1577,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (newParent != null) { if (isState(RESUMED)) { newParent.setResumedActivity(this, "onParentChanged"); - mImeInsetsFrozenUntilStartInput = false; } mLetterboxUiController.onActivityParentChanged(newParent); } @@ -8874,13 +8874,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } - @Override - void onResize() { - // Reset freezing IME insets flag when the activity resized. - mImeInsetsFrozenUntilStartInput = false; - super.onResize(); - } - private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, Rect containingBounds) { return applyAspectRatio(outBounds, containingAppBounds, containingBounds, diff --git a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java new file mode 100644 index 000000000000..64af9dd755ed --- /dev/null +++ b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java @@ -0,0 +1,114 @@ +/* + * 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; + +import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER; + +import static com.android.server.wm.ActivityStarter.ASM_RESTRICTIONS; + +import android.annotation.NonNull; +import android.app.compat.CompatChanges; +import android.content.pm.PackageManager; +import android.provider.DeviceConfig; + +import com.android.internal.annotations.GuardedBy; + +import java.util.HashSet; +import java.util.concurrent.Executor; + +/** + * Contains utility methods to query whether or not go/activity-security should be enabled + * asm_start_rules_enabled - Enable rule enforcement in ActivityStarter.java + * asm_start_rules_toasts_enabled - Show toasts when rules would block from ActivityStarter.java + * asm_start_rules_exception_list - Comma separated list of packages to exclude from the above + * 2 rules. + * TODO(b/258792202) Cleanup once ASM is ready to launch + */ +class ActivitySecurityModelFeatureFlags { + // TODO(b/230590090): Replace with public documentation once ready + static final String DOC_LINK = "go/android-asm"; + + private static final String NAMESPACE = NAMESPACE_WINDOW_MANAGER; + private static final String KEY_ASM_RESTRICTIONS_ENABLED = "asm_restrictions_enabled"; + private static final String KEY_ASM_TOASTS_ENABLED = "asm_toasts_enabled"; + private static final String KEY_ASM_EXEMPTED_PACKAGES = "asm_exempted_packages"; + private static final int VALUE_DISABLE = 0; + private static final int VALUE_ENABLE_FOR_U = 1; + private static final int VALUE_ENABLE_FOR_ALL = 2; + + private static final int DEFAULT_VALUE = VALUE_DISABLE; + private static final String DEFAULT_EXCEPTION_LIST = ""; + + private static int sAsmToastsEnabled; + private static int sAsmRestrictionsEnabled; + private static final HashSet<String> sExcludedPackageNames = new HashSet<>(); + private static PackageManager sPm; + + @GuardedBy("ActivityTaskManagerService.mGlobalLock") + static void initialize(@NonNull Executor executor, @NonNull PackageManager pm) { + updateFromDeviceConfig(); + DeviceConfig.addOnPropertiesChangedListener(NAMESPACE, executor, + properties -> updateFromDeviceConfig()); + sPm = pm; + } + + @GuardedBy("ActivityTaskManagerService.mGlobalLock") + static boolean shouldShowToast(int uid) { + return flagEnabledForUid(sAsmToastsEnabled, uid); + } + + @GuardedBy("ActivityTaskManagerService.mGlobalLock") + static boolean shouldBlockActivityStart(int uid) { + return flagEnabledForUid(sAsmRestrictionsEnabled, uid); + } + + private static boolean flagEnabledForUid(int flag, int uid) { + boolean flagEnabled = flag == VALUE_ENABLE_FOR_ALL + || (flag == VALUE_ENABLE_FOR_U + && CompatChanges.isChangeEnabled(ASM_RESTRICTIONS, uid)); + + if (flagEnabled) { + String[] packageNames = sPm.getPackagesForUid(uid); + for (int i = 0; i < packageNames.length; i++) { + if (sExcludedPackageNames.contains(packageNames[i])) { + return false; + } + } + return true; + } + + return false; + } + + private static void updateFromDeviceConfig() { + sAsmToastsEnabled = DeviceConfig.getInt(NAMESPACE, KEY_ASM_TOASTS_ENABLED, + DEFAULT_VALUE); + sAsmRestrictionsEnabled = DeviceConfig.getInt(NAMESPACE, KEY_ASM_RESTRICTIONS_ENABLED, + DEFAULT_VALUE); + + String rawExceptionList = DeviceConfig.getString(NAMESPACE, + KEY_ASM_EXEMPTED_PACKAGES, DEFAULT_EXCEPTION_LIST); + sExcludedPackageNames.clear(); + String[] packages = rawExceptionList.split(","); + for (String packageName : packages) { + String packageNameTrimmed = packageName.trim(); + if (!packageNameTrimmed.isEmpty()) { + sExcludedPackageNames.add(packageNameTrimmed); + } + } + } +} diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 86493ebde535..40432dc49790 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -56,7 +56,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS; @@ -125,6 +125,7 @@ import android.service.voice.IVoiceInteractionSession; import android.text.TextUtils; import android.util.Pools.SynchronizedPool; import android.util.Slog; +import android.widget.Toast; import android.window.RemoteTransition; import com.android.internal.annotations.VisibleForTesting; @@ -132,6 +133,7 @@ import com.android.internal.app.HeavyWeightSwitcherActivity; import com.android.internal.app.IVoiceInteractor; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.UiThread; import com.android.server.am.PendingIntentRecord; import com.android.server.pm.InstantAppResolver; import com.android.server.power.ShutdownCheckPoints; @@ -168,6 +170,13 @@ class ActivityStarter { @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) static final long ENABLE_PENDING_INTENT_BAL_OPTION = 192341120L; + /** + * Feature flag for go/activity-security rules + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + static final long ASM_RESTRICTIONS = 230590090L; + private final ActivityTaskManagerService mService; private final RootWindowContainer mRootWindowContainer; private final ActivityTaskSupervisor mSupervisor; @@ -1859,7 +1868,7 @@ class ActivityStarter { } if (!checkActivitySecurityModel(r, newTask, targetTask)) { - return START_SUCCESS; + return START_ABORTED; } return START_SUCCESS; @@ -1925,11 +1934,6 @@ class ActivityStarter { : targetTask.getActivity(ar -> !ar.isState(FINISHING) && !ar.isAlwaysOnTop()); - Slog.i(TAG, "Launching r: " + r - + " from background: " + mSourceRecord - + ". New task: " + newTask - + ". Top activity: " + targetTopActivity); - int action = newTask || mSourceRecord == null ? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_NEW_TASK : (mSourceRecord.getTask().equals(targetTask) @@ -1965,7 +1969,29 @@ class ActivityStarter { && !targetTask.equals(mSourceRecord.getTask()) && targetTask.isVisible() ); - return false; + boolean shouldBlockActivityStart = + ActivitySecurityModelFeatureFlags.shouldBlockActivityStart(mCallingUid); + + if (ActivitySecurityModelFeatureFlags.shouldShowToast(mCallingUid)) { + UiThread.getHandler().post(() -> Toast.makeText(mService.mContext, + (shouldBlockActivityStart + ? "Activity start blocked by " + : "Activity start would be blocked by ") + + ActivitySecurityModelFeatureFlags.DOC_LINK, + Toast.LENGTH_SHORT).show()); + } + + + if (shouldBlockActivityStart) { + Slog.e(TAG, "Abort Launching r: " + r + + " as source: " + mSourceRecord + + "is in background. New task: " + newTask + + ". Top activity: " + targetTopActivity); + + return false; + } + + return true; } /** @@ -2889,7 +2915,7 @@ class ActivityStarter { if (taskFragment.isOrganized()) { mService.mWindowOrganizerController.sendTaskFragmentOperationFailure( taskFragment.getTaskFragmentOrganizer(), mRequest.errorCallbackToken, - taskFragment, HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT, + taskFragment, OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT, new SecurityException(errMsg)); } else { // If the taskFragment is not organized, just dump error message as warning logs. diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index fd6d6062fbf2..9a8ef19d1637 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -859,6 +859,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mRecentTasks.onSystemReadyLocked(); mTaskSupervisor.onSystemReady(); mActivityClientController.onSystemReady(); + // TODO(b/258792202) Cleanup once ASM is ready to launch + ActivitySecurityModelFeatureFlags.initialize(mContext.getMainExecutor(), pm); } } @@ -1495,7 +1497,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { a.persistableMode = ActivityInfo.PERSIST_NEVER; a.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; a.colorMode = ActivityInfo.COLOR_MODE_DEFAULT; - a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS; + a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS | ActivityInfo.FLAG_NO_HISTORY; a.resizeMode = RESIZE_MODE_UNRESIZEABLE; a.configChanges = 0xffffffff; diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 74d52b2c1458..d65c2f96e1ce 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -900,7 +900,7 @@ public class AppTransitionController { * * TODO(b/213312721): Remove this predicate and its callers once ShellTransition is enabled. */ - private static boolean isTaskViewTask(WindowContainer wc) { + static boolean isTaskViewTask(WindowContainer wc) { // We use Task#mRemoveWithTaskOrganizer to identify an embedded Task, but this is a hack and // it is not guaranteed to work this logic in the future version. return wc instanceof Task && ((Task) wc).mRemoveWithTaskOrganizer; diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index e7a5ee7f01d9..eadb11e7c6e0 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4509,11 +4509,35 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mImeWindowsContainer.getParent().mSurfaceControl)); updateImeControlTarget(forceUpdateImeParent); } - // Unfreeze IME insets after the new target updated, in case updateAboveInsetsState may - // deliver unrelated IME insets change to the non-IME requester. - if (target != null) { - target.unfreezeInsetsAfterStartInput(); + } + + /** + * Callback from {@link ImeInsetsSourceProvider#updateClientVisibility} for the system to + * judge whether or not to notify the IME insets provider to dispatch this reported IME client + * visibility state to the app clients when needed. + */ + boolean onImeInsetsClientVisibilityUpdate() { + boolean[] changed = new boolean[1]; + + // Unlike the IME layering target or the control target can be updated during the layout + // change, the IME input target requires to be changed after gaining the input focus. + // In case unfreezing IME insets state may too early during IME focus switching, we unfreeze + // when activities going to be visible until the input target changed, or the + // activity was the current input target that has to unfreeze after updating the IME + // client visibility. + final ActivityRecord inputTargetActivity = + mImeInputTarget != null ? mImeInputTarget.getActivityRecord() : null; + final boolean targetChanged = mImeInputTarget != mLastImeInputTarget; + if (targetChanged || inputTargetActivity != null && inputTargetActivity.isVisibleRequested() + && inputTargetActivity.mImeInsetsFrozenUntilStartInput) { + forAllActivities(r -> { + if (r.mImeInsetsFrozenUntilStartInput && r.isVisibleRequested()) { + r.mImeInsetsFrozenUntilStartInput = false; + changed[0] = true; + } + }); } + return changed[0]; } void updateImeControlTarget() { diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index ba0413df6325..c6037dab6568 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -203,8 +203,11 @@ final class DisplayRotationCompatPolicy { || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) { return; } - boolean cycleThroughStop = mWmService.mLetterboxConfiguration - .isCameraCompatRefreshCycleThroughStopEnabled(); + boolean cycleThroughStop = + mWmService.mLetterboxConfiguration + .isCameraCompatRefreshCycleThroughStopEnabled() + && !activity.mLetterboxUiController + .shouldRefreshActivityViaPauseForCameraCompat(); try { activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true); ProtoLog.v(WM_DEBUG_STATES, @@ -255,7 +258,8 @@ final class DisplayRotationCompatPolicy { Configuration lastReportedConfig) { return newConfig.windowConfiguration.getDisplayRotation() != lastReportedConfig.windowConfiguration.getDisplayRotation() - && isTreatmentEnabledForActivity(activity); + && isTreatmentEnabledForActivity(activity) + && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat(); } /** @@ -294,7 +298,8 @@ final class DisplayRotationCompatPolicy { // handle dynamic changes so we shouldn't force rotate them. && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED - && mCameraIdPackageBiMap.containsPackageName(activity.packageName); + && mCameraIdPackageBiMap.containsPackageName(activity.packageName) + && activity.mLetterboxUiController.shouldForceRotateForCameraCompat(); } private synchronized void notifyCameraOpened( diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index d54f77a73661..c3c727a1d879 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -335,10 +335,6 @@ class EmbeddedWindowController { } @Override - public void unfreezeInsetsAfterStartInput() { - } - - @Override public InsetsControlTarget getImeControlTarget() { return mWmService.getDefaultDisplayContentLocked().mRemoteInsetsControlTarget; } diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 90d0f16e6478..85938e3bfd71 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -140,10 +140,14 @@ final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider @Override protected boolean updateClientVisibility(InsetsControlTarget caller) { + if (caller != getControlTarget()) { + return false; + } boolean changed = super.updateClientVisibility(caller); if (changed && caller.isRequestedVisible(mSource.getType())) { reportImeDrawnForOrganizer(caller); } + changed |= mDisplayContent.onImeInsetsClientVisibilityUpdate(); return changed; } diff --git a/services/core/java/com/android/server/wm/InputTarget.java b/services/core/java/com/android/server/wm/InputTarget.java index b5ab62b6e03f..653f5f5a74e9 100644 --- a/services/core/java/com/android/server/wm/InputTarget.java +++ b/services/core/java/com/android/server/wm/InputTarget.java @@ -16,8 +16,8 @@ package com.android.server.wm; -import android.view.IWindow; import android.util.proto.ProtoOutputStream; +import android.view.IWindow; /** * Common interface between focusable objects. @@ -58,7 +58,6 @@ interface InputTarget { boolean canScreenshotIme(); ActivityRecord getActivityRecord(); - void unfreezeInsetsAfterStartInput(); boolean isInputMethodClientFocus(int uid, int pid); diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 0c8a6453e6fb..75ba2146267d 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -17,12 +17,18 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.pm.ActivityInfo.screenOrientationToString; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM; @@ -132,6 +138,15 @@ final class LetterboxUiController { @Nullable private Letterbox mLetterbox; + @Nullable + private final Boolean mBooleanPropertyCameraCompatAllowForceRotation; + + @Nullable + private final Boolean mBooleanPropertyCameraCompatAllowRefresh; + + @Nullable + private final Boolean mBooleanPropertyCameraCompatEnableRefreshViaPause; + // Whether activity "refresh" was requested but not finished in // ActivityRecord#activityResumedLocked following the camera compat force rotation in // DisplayRotationCompatPolicy. @@ -154,8 +169,33 @@ final class LetterboxUiController { readComponentProperty(packageManager, mActivityRecord.packageName, mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled, PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION); + mBooleanPropertyCameraCompatAllowForceRotation = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true), + PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION); + mBooleanPropertyCameraCompatAllowRefresh = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true), + PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH); + mBooleanPropertyCameraCompatEnableRefreshViaPause = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true), + PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE); } + /** + * Reads a {@link Boolean} component property fot a given {@code packageName} and a {@code + * propertyName}. Returns {@code null} if {@code gatingCondition} is {@code false} or if the + * property isn't specified for the package. + * + * <p>Return value is {@link Boolean} rather than {@code boolean} so we can know when the + * property is unset. Particularly, when this returns {@code null}, {@link + * #shouldEnableWithOverrideAndProperty} will check the value of override for the final + * decision. + */ @Nullable private static Boolean readComponentProperty(PackageManager packageManager, String packageName, BooleanSupplier gatingCondition, String propertyName) { @@ -210,15 +250,11 @@ final class LetterboxUiController { * </ul> */ boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) { - if (!mLetterboxConfiguration.isPolicyForIgnoringRequestedOrientationEnabled()) { - return false; - } - if (Boolean.FALSE.equals(mBooleanPropertyIgnoreRequestedOrientation)) { - return false; - } - if (!Boolean.TRUE.equals(mBooleanPropertyIgnoreRequestedOrientation) - && !mActivityRecord.info.isChangeEnabled( - OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION)) { + if (!shouldEnableWithOverrideAndProperty( + /* gatingCondition */ mLetterboxConfiguration + ::isPolicyForIgnoringRequestedOrientationEnabled, + OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION, + mBooleanPropertyIgnoreRequestedOrientation)) { return false; } if (mIsRelauchingAfterRequestedOrientationChanged) { @@ -262,6 +298,109 @@ final class LetterboxUiController { mIsRefreshAfterRotationRequested = isRequested; } + /** + * Whether activity is eligible for activity "refresh" after camera compat force rotation + * treatment. See {@link DisplayRotationCompatPolicy} for context. + * + * <p>This treatment is enabled when the following conditions are met: + * <ul> + * <li>Flag gating the camera compat treatment is enabled. + * <li>Activity isn't opted out by the device manufacturer with override or by the app + * developers with the component property. + * </ul> + */ + boolean shouldRefreshActivityForCameraCompat() { + return shouldEnableWithOptOutOverrideAndProperty( + /* gatingCondition */ () -> mLetterboxConfiguration + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true), + OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH, + mBooleanPropertyCameraCompatAllowRefresh); + } + + /** + * Whether activity should be "refreshed" after the camera compat force rotation treatment + * using the "resumed -> paused -> resumed" cycle rather than the "resumed -> ... -> stopped + * -> ... -> resumed" cycle. See {@link DisplayRotationCompatPolicy} for context. + * + * <p>This treatment is enabled when the following conditions are met: + * <ul> + * <li>Flag gating the camera compat treatment is enabled. + * <li>Activity "refresh" via "resumed -> paused -> resumed" cycle isn't disabled with the + * component property by the app developers. + * <li>Activity "refresh" via "resumed -> paused -> resumed" cycle is enabled by the device + * manufacturer with override / by the app developers with the component property. + * </ul> + */ + boolean shouldRefreshActivityViaPauseForCameraCompat() { + return shouldEnableWithOverrideAndProperty( + /* gatingCondition */ () -> mLetterboxConfiguration + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true), + OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, + mBooleanPropertyCameraCompatEnableRefreshViaPause); + } + + /** + * Whether activity is eligible for camera compat force rotation treatment. See {@link + * DisplayRotationCompatPolicy} for context. + * + * <p>This treatment is enabled when the following conditions are met: + * <ul> + * <li>Flag gating the camera compat treatment is enabled. + * <li>Activity isn't opted out by the device manufacturer with override or by the app + * developers with the component property. + * </ul> + */ + boolean shouldForceRotateForCameraCompat() { + return shouldEnableWithOptOutOverrideAndProperty( + /* gatingCondition */ () -> mLetterboxConfiguration + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true), + OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION, + mBooleanPropertyCameraCompatAllowForceRotation); + } + + /** + * Returns {@code true} when the following conditions are met: + * <ul> + * <li>{@code gatingCondition} isn't {@code false} + * <li>OEM didn't opt out with a {@code overrideChangeId} override + * <li>App developers didn't opt out with a component {@code property} + * </ul> + * + * <p>This is used for the treatments that are enabled based with the heuristic but can be + * disabled on per-app basis by OEMs or app developers. + */ + private boolean shouldEnableWithOptOutOverrideAndProperty(BooleanSupplier gatingCondition, + long overrideChangeId, Boolean property) { + if (!gatingCondition.getAsBoolean()) { + return false; + } + return !Boolean.FALSE.equals(property) + && !mActivityRecord.info.isChangeEnabled(overrideChangeId); + } + + /** + * Returns {@code true} when the following conditions are met: + * <ul> + * <li>{@code gatingCondition} isn't {@code false} + * <li>App developers didn't opt out with a component {@code property} + * <li>App developers opted in with a component {@code property} or an OEM opted in with a + * component {@code property} + * </ul> + * + * <p>This is used for the treatments that are enabled only on per-app basis. + */ + private boolean shouldEnableWithOverrideAndProperty(BooleanSupplier gatingCondition, + long overrideChangeId, Boolean property) { + if (!gatingCondition.getAsBoolean()) { + return false; + } + if (Boolean.FALSE.equals(property)) { + return false; + } + return Boolean.TRUE.equals(property) + || mActivityRecord.info.isChangeEnabled(overrideChangeId); + } + boolean hasWallpaperBackgroundForLetterbox() { return mShowWallpaperForLetterboxBackground; } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 852c9b297040..e253ce03e46d 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -6265,6 +6265,11 @@ class Task extends TaskFragment { return this; } + Builder setRemoveWithTaskOrganizer(boolean removeWithTaskOrganizer) { + mRemoveWithTaskOrganizer = removeWithTaskOrganizer; + return this; + } + private Builder setUserId(int userId) { mUserId = userId; return this; @@ -6462,7 +6467,7 @@ class Task extends TaskFragment { mCallingPackage = mActivityInfo.packageName; mResizeMode = mActivityInfo.resizeMode; mSupportsPictureInPicture = mActivityInfo.supportsPictureInPicture(); - if (mActivityOptions != null) { + if (!mRemoveWithTaskOrganizer && mActivityOptions != null) { mRemoveWithTaskOrganizer = mActivityOptions.getRemoveWithTaskOranizer(); } diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 90a0dffa25f2..5f186a178f2d 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.TaskFragmentOperation; import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; @@ -297,7 +298,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr @NonNull TaskFragmentTransaction.Change prepareTaskFragmentError( @Nullable IBinder errorCallbackToken, @Nullable TaskFragment taskFragment, - int opType, @NonNull Throwable exception) { + @TaskFragmentOperation.OperationType int opType, @NonNull Throwable exception) { ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Sending TaskFragment error exception=%s", exception.toString()); final TaskFragmentInfo info = @@ -629,7 +630,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr void onTaskFragmentError(@NonNull ITaskFragmentOrganizer organizer, @Nullable IBinder errorCallbackToken, @Nullable TaskFragment taskFragment, - int opType, @NonNull Throwable exception) { + @TaskFragmentOperation.OperationType int opType, @NonNull Throwable exception) { if (taskFragment != null && taskFragment.mTaskFragmentVanishedSent) { return; } @@ -803,6 +804,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr // Set when the event is deferred due to the host task is invisible. The defer time will // be the last active time of the host task. private long mDeferTime; + @TaskFragmentOperation.OperationType private int mOpType; private PendingTaskFragmentEvent(@EventType int eventType, @@ -812,7 +814,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr @Nullable Throwable exception, @Nullable ActivityRecord activity, @Nullable Task task, - int opType) { + @TaskFragmentOperation.OperationType int opType) { mEventType = eventType; mTaskFragmentOrg = taskFragmentOrg; mTaskFragment = taskFragment; @@ -853,6 +855,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr private ActivityRecord mActivity; @Nullable private Task mTask; + @TaskFragmentOperation.OperationType private int mOpType; Builder(@EventType int eventType, @NonNull ITaskFragmentOrganizer taskFragmentOrg) { @@ -885,7 +888,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr return this; } - Builder setOpType(int opType) { + Builder setOpType(@TaskFragmentOperation.OperationType int opType) { mOpType = opType; return this; } diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index 274d7ff4671a..8570db275a08 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -783,7 +783,8 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { } @Override - public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { enforceTaskPermission("createRootTask()"); final long origId = Binder.clearCallingIdentity(); try { @@ -795,7 +796,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { return; } - createRootTask(display, windowingMode, launchCookie); + createRootTask(display, windowingMode, launchCookie, removeWithTaskOrganizer); } } finally { Binder.restoreCallingIdentity(origId); @@ -804,6 +805,12 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { @VisibleForTesting Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie) { + return createRootTask(display, windowingMode, launchCookie, + false /* removeWithTaskOrganizer */); + } + + Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Create root task displayId=%d winMode=%d", display.mDisplayId, windowingMode); // We want to defer the task appear signal until the task is fully created and attached to @@ -816,6 +823,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { .setDeferTaskAppear(true) .setLaunchCookie(launchCookie) .setParent(display.getDefaultTaskDisplayArea()) + .setRemoveWithTaskOrganizer(removeWithTaskOrganizer) .build(); task.setDeferTaskAppear(false /* deferTaskAppear */); return task; diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 7f9e808c4c93..16541c10d9db 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -370,7 +370,7 @@ class WallpaperController { boolean updateWallpaperOffset(WindowState wallpaperWin, boolean sync) { // Size of the display the wallpaper is rendered on. - final Rect lastWallpaperBounds = wallpaperWin.getLastReportedBounds(); + final Rect lastWallpaperBounds = wallpaperWin.getParentFrame(); // Full size of the wallpaper (usually larger than bounds above to parallax scroll when // swiping through Launcher pages). final Rect wallpaperFrame = wallpaperWin.getFrame(); diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 0ab4faf9d4bd..63bb5c34492d 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -3237,11 +3237,11 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< private Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter, boolean isVoiceInteraction) { - if (isOrganized() + if (AppTransitionController.isTaskViewTask(this) || (isOrganized() // TODO(b/161711458): Clean-up when moved to shell. && getWindowingMode() != WINDOWING_MODE_FULLSCREEN && getWindowingMode() != WINDOWING_MODE_FREEFORM - && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW) { + && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW)) { return null; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index aac5ef6868df..0a5e0b7533ee 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -21,12 +21,20 @@ import static android.app.ActivityManager.isStartResultSuccessful; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_FINISH_ACTIVITY; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_LAUNCH_TASK; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT; @@ -34,19 +42,12 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_SHORTCUT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER; @@ -119,6 +120,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub private static final String TAG = "WindowOrganizerController"; + private static final int TRANSACT_EFFECTS_NONE = 0; /** Flag indicating that an applied transaction may have effected lifecycle */ private static final int TRANSACT_EFFECTS_CLIENT_CONFIG = 1; private static final int TRANSACT_EFFECTS_LIFECYCLE = 1 << 1; @@ -488,7 +490,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub private void applyTransaction(@NonNull WindowContainerTransaction t, int syncId, @Nullable Transition transition, @NonNull CallerInfo caller, @Nullable Transition finishTransition) { - int effects = 0; + int effects = TRANSACT_EFFECTS_NONE; ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Apply window transaction, syncId=%d", syncId); mService.deferWindowLayout(); mService.mTaskSupervisor.setDeferRootVisibilityUpdate(true /* deferUpdate */); @@ -630,7 +632,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // masks here. final int configMask = change.getConfigSetMask() & CONTROLLABLE_CONFIGS; final int windowMask = change.getWindowSetMask() & CONTROLLABLE_WINDOW_CONFIGS; - int effects = 0; + int effects = TRANSACT_EFFECTS_NONE; final int windowingMode = change.getWindowingMode(); if (configMask != 0) { @@ -795,7 +797,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub @NonNull WindowContainerTransaction.Change c, @Nullable IBinder errorCallbackToken) { if (taskFragment.isEmbeddedTaskFragmentInPip()) { // No override from organizer for embedded TaskFragment in a PIP Task. - return 0; + return TRANSACT_EFFECTS_NONE; } // When the TaskFragment is resized, we may want to create a change transition for it, for @@ -861,197 +863,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub effects |= clearAdjacentRootsHierarchyOp(hop); break; } - case HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT: { - final TaskFragmentCreationParams taskFragmentCreationOptions = - hop.getTaskFragmentCreationOptions(); - createTaskFragment(taskFragmentCreationOptions, errorCallbackToken, caller, - transition); - break; - } - case HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT: { - final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer()); - if (wc == null || !wc.isAttached()) { - Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc); - break; - } - final TaskFragment taskFragment = wc.asTaskFragment(); - if (taskFragment == null || taskFragment.asTask() != null) { - throw new IllegalArgumentException( - "Can only delete organized TaskFragment, but not Task."); - } - if (isInLockTaskMode) { - final ActivityRecord bottomActivity = taskFragment.getActivity( - a -> !a.finishing, false /* traverseTopToBottom */); - if (bottomActivity != null - && mService.getLockTaskController().activityBlockedFromFinish( - bottomActivity)) { - Slog.w(TAG, "Skip removing TaskFragment due in lock task mode."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, - taskFragment, type, new IllegalStateException( - "Not allow to delete task fragment in lock task mode.")); - break; - } - } - effects |= deleteTaskFragment(taskFragment, organizer, errorCallbackToken, - transition); - break; - } - case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: { - final IBinder fragmentToken = hop.getContainer(); - final TaskFragment tf = mLaunchTaskFragments.get(fragmentToken); - if (tf == null) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to operate with invalid fragment token"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, tf, type, - exception); - break; - } - if (tf.isEmbeddedTaskFragmentInPip()) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to start activity in PIP TaskFragment"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, tf, type, - exception); - break; - } - final Intent activityIntent = hop.getActivityIntent(); - final Bundle activityOptions = hop.getLaunchOptions(); - final int result = mService.getActivityStartController() - .startActivityInTaskFragment(tf, activityIntent, activityOptions, - hop.getCallingActivity(), caller.mUid, caller.mPid, - errorCallbackToken); - if (!isStartResultSuccessful(result)) { - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, tf, type, - convertStartFailureToThrowable(result, activityIntent)); - } else { - effects |= TRANSACT_EFFECTS_LIFECYCLE; - } - break; - } - case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: { - final IBinder fragmentToken = hop.getNewParent(); - final IBinder activityToken = hop.getContainer(); - ActivityRecord activity = ActivityRecord.forTokenLocked(activityToken); - if (activity == null) { - // The token may be a temporary token if the activity doesn't belong to - // the organizer process. - activity = mTaskFragmentOrganizerController - .getReparentActivityFromTemporaryToken(organizer, activityToken); - } - final TaskFragment parent = mLaunchTaskFragments.get(fragmentToken); - if (parent == null || activity == null) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to operate with invalid fragment token or activity."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, parent, type, - exception); - break; - } - if (parent.isEmbeddedTaskFragmentInPip()) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to reparent activity to PIP TaskFragment"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, parent, type, - exception); - break; - } - if (parent.isAllowedToEmbedActivity(activity) != EMBEDDING_ALLOWED) { - final Throwable exception = new SecurityException( - "The task fragment is not allowed to embed the given activity."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, parent, type, - exception); - break; - } - if (parent.getTask() != activity.getTask()) { - final Throwable exception = new SecurityException("The reparented activity is" - + " not in the same Task as the target TaskFragment."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, parent, type, - exception); - break; - } - - if (transition != null) { - transition.collect(activity); - if (activity.getParent() != null) { - // Collect the current parent. Its visibility may change as a result of - // this reparenting. - transition.collect(activity.getParent()); - } - transition.collect(parent); - } - activity.reparent(parent, POSITION_TOP); - effects |= TRANSACT_EFFECTS_LIFECYCLE; - break; - } - case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: { - final IBinder fragmentToken = hop.getContainer(); - final IBinder adjacentFragmentToken = hop.getAdjacentRoot(); - final TaskFragment tf1 = mLaunchTaskFragments.get(fragmentToken); - final TaskFragment tf2 = adjacentFragmentToken != null - ? mLaunchTaskFragments.get(adjacentFragmentToken) - : null; - if (tf1 == null || (adjacentFragmentToken != null && tf2 == null)) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to set adjacent on invalid fragment tokens"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, tf1, type, - exception); - break; - } - if (tf1.isEmbeddedTaskFragmentInPip() - || (tf2 != null && tf2.isEmbeddedTaskFragmentInPip())) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to set adjacent on TaskFragment in PIP Task"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, tf1, type, - exception); - break; - } - tf1.setAdjacentTaskFragment(tf2); - effects |= TRANSACT_EFFECTS_LIFECYCLE; - - // Clear the focused app if the focused app is no longer visible after reset the - // adjacent TaskFragments. - if (tf2 == null && tf1.getDisplayContent().mFocusedApp != null - && tf1.hasChild(tf1.getDisplayContent().mFocusedApp) - && !tf1.shouldBeVisible(null /* starting */)) { - tf1.getDisplayContent().setFocusedApp(null); - } - - final Bundle bundle = hop.getLaunchOptions(); - final WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = - bundle != null ? new WindowContainerTransaction.TaskFragmentAdjacentParams( - bundle) : null; - if (adjacentParams == null) { - break; - } - - tf1.setDelayLastActivityRemoval( - adjacentParams.shouldDelayPrimaryLastActivityRemoval()); - if (tf2 != null) { - tf2.setDelayLastActivityRemoval( - adjacentParams.shouldDelaySecondaryLastActivityRemoval()); - } - break; - } - case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: { - final TaskFragment tf = mLaunchTaskFragments.get(hop.getContainer()); - if (tf == null || !tf.isAttached()) { - Slog.e(TAG, "Attempt to operate on detached container: " + tf); - break; - } - final ActivityRecord curFocus = tf.getDisplayContent().mFocusedApp; - if (curFocus != null && curFocus.getTaskFragment() == tf) { - Slog.d(TAG, "The requested TaskFragment already has the focus."); - break; - } - if (curFocus != null && curFocus.getTask() != tf.getTask()) { - Slog.d(TAG, "The Task of the requested TaskFragment doesn't have focus."); - break; - } - final ActivityRecord targetFocus = tf.getTopResumedActivity(); - if (targetFocus == null) { - Slog.d(TAG, "There is no resumed activity in the requested TaskFragment."); - break; - } - tf.getDisplayContent().setFocusedApp(targetFocus); - break; - } case HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT: { effects |= reparentChildrenTasksHierarchyOp(hop, transition, syncId, isInLockTaskMode); @@ -1126,24 +937,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub effects |= sanitizeAndApplyHierarchyOp(wc, hop); break; } - case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: { - final IBinder fragmentToken = hop.getContainer(); - final IBinder companionToken = hop.getCompanionContainer(); - final TaskFragment fragment = mLaunchTaskFragments.get(fragmentToken); - final TaskFragment companion = companionToken != null ? mLaunchTaskFragments.get( - companionToken) : null; - if (fragment == null || !fragment.isAttached()) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to set companion on invalid fragment tokens"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, fragment, type, - exception); - break; - } - fragment.setCompanionTaskFragment(companion); - break; - } - case HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION: { - effects |= applyTaskFragmentOperation(hop, errorCallbackToken, organizer); + case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: { + effects |= applyTaskFragmentOperation(hop, transition, isInLockTaskMode, caller, + errorCallbackToken, organizer); break; } default: { @@ -1203,22 +999,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } break; } - case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: { - final WindowContainer oldParent = WindowContainer.fromBinder(hop.getContainer()); - final WindowContainer newParent = hop.getNewParent() != null - ? WindowContainer.fromBinder(hop.getNewParent()) - : null; - if (oldParent == null || oldParent.asTaskFragment() == null - || !oldParent.isAttached()) { - Slog.e(TAG, "Attempt to operate on unknown or detached container: " - + oldParent); - break; - } - reparentTaskFragment(oldParent.asTaskFragment(), newParent, organizer, - errorCallbackToken, transition); - effects |= TRANSACT_EFFECTS_LIFECYCLE; - break; - } case HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER: { if (finishTransition == null) break; final WindowContainer container = WindowContainer.fromBinder(hop.getContainer()); @@ -1278,45 +1058,257 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return effects; } - /** Applies change set through {@link WindowContainerTransaction#setTaskFragmentOperation}. */ + /** + * Applies change set through {@link WindowContainerTransaction#addTaskFragmentOperation}. + * @return an int to represent the transaction effects, such as {@link #TRANSACT_EFFECTS_NONE}, + * {@link #TRANSACT_EFFECTS_LIFECYCLE} or {@link #TRANSACT_EFFECTS_CLIENT_CONFIG}. + */ private int applyTaskFragmentOperation(@NonNull WindowContainerTransaction.HierarchyOp hop, + @Nullable Transition transition, boolean isInLockTaskMode, @NonNull CallerInfo caller, @Nullable IBinder errorCallbackToken, @Nullable ITaskFragmentOrganizer organizer) { + if (!validateTaskFragmentOperation(hop, errorCallbackToken, organizer)) { + return TRANSACT_EFFECTS_NONE; + } final IBinder fragmentToken = hop.getContainer(); final TaskFragment taskFragment = mLaunchTaskFragments.get(fragmentToken); final TaskFragmentOperation operation = hop.getTaskFragmentOperation(); - if (operation == null) { - final Throwable exception = new IllegalArgumentException( - "TaskFragmentOperation must be non-null"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION, exception); - return 0; - } final int opType = operation.getOpType(); - if (taskFragment == null || !taskFragment.isAttached()) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to apply operation on invalid fragment tokens opType=" + opType); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION, exception); - return 0; - } - int effect = 0; + int effects = TRANSACT_EFFECTS_NONE; switch (opType) { + case OP_TYPE_CREATE_TASK_FRAGMENT: { + final TaskFragmentCreationParams taskFragmentCreationParams = + operation.getTaskFragmentCreationParams(); + if (taskFragmentCreationParams == null) { + final Throwable exception = new IllegalArgumentException( + "TaskFragmentCreationParams must be non-null"); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + break; + } + createTaskFragment(taskFragmentCreationParams, errorCallbackToken, caller, + transition); + break; + } + case OP_TYPE_DELETE_TASK_FRAGMENT: { + if (isInLockTaskMode) { + final ActivityRecord bottomActivity = taskFragment.getActivity( + a -> !a.finishing, false /* traverseTopToBottom */); + if (bottomActivity != null + && mService.getLockTaskController().activityBlockedFromFinish( + bottomActivity)) { + Slog.w(TAG, "Skip removing TaskFragment due in lock task mode."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, + taskFragment, opType, new IllegalStateException( + "Not allow to delete task fragment in lock task mode.")); + break; + } + } + effects |= deleteTaskFragment(taskFragment, transition); + break; + } + case OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: { + final IBinder callerActivityToken = operation.getActivityToken(); + final Intent activityIntent = operation.getActivityIntent(); + final Bundle activityOptions = operation.getBundle(); + final int result = mService.getActivityStartController() + .startActivityInTaskFragment(taskFragment, activityIntent, activityOptions, + callerActivityToken, caller.mUid, caller.mPid, + errorCallbackToken); + if (!isStartResultSuccessful(result)) { + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, convertStartFailureToThrowable(result, activityIntent)); + } else { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } + break; + } + case OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: { + final IBinder activityToken = operation.getActivityToken(); + ActivityRecord activity = ActivityRecord.forTokenLocked(activityToken); + if (activity == null) { + // The token may be a temporary token if the activity doesn't belong to + // the organizer process. + activity = mTaskFragmentOrganizerController + .getReparentActivityFromTemporaryToken(organizer, activityToken); + } + if (activity == null) { + final Throwable exception = new IllegalArgumentException( + "Not allowed to operate with invalid activity."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + break; + } + if (taskFragment.isAllowedToEmbedActivity(activity) != EMBEDDING_ALLOWED) { + final Throwable exception = new SecurityException( + "The task fragment is not allowed to embed the given activity."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + break; + } + if (taskFragment.getTask() != activity.getTask()) { + final Throwable exception = new SecurityException("The reparented activity is" + + " not in the same Task as the target TaskFragment."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + break; + } + if (transition != null) { + transition.collect(activity); + if (activity.getParent() != null) { + // Collect the current parent. Its visibility may change as a result of + // this reparenting. + transition.collect(activity.getParent()); + } + transition.collect(taskFragment); + } + activity.reparent(taskFragment, POSITION_TOP); + effects |= TRANSACT_EFFECTS_LIFECYCLE; + break; + } + case OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: { + final IBinder secondaryFragmentToken = operation.getSecondaryFragmentToken(); + final TaskFragment secondaryTaskFragment = + mLaunchTaskFragments.get(secondaryFragmentToken); + if (secondaryTaskFragment == null) { + final Throwable exception = new IllegalArgumentException( + "SecondaryFragmentToken must be set for setAdjacentTaskFragments."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + break; + } + if (taskFragment.getAdjacentTaskFragment() != secondaryTaskFragment) { + // Only have lifecycle effect if the adjacent changed. + taskFragment.setAdjacentTaskFragment(secondaryTaskFragment); + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } + + final Bundle bundle = hop.getLaunchOptions(); + final WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = + bundle != null + ? new WindowContainerTransaction.TaskFragmentAdjacentParams(bundle) + : null; + taskFragment.setDelayLastActivityRemoval(adjacentParams != null + && adjacentParams.shouldDelayPrimaryLastActivityRemoval()); + secondaryTaskFragment.setDelayLastActivityRemoval(adjacentParams != null + && adjacentParams.shouldDelaySecondaryLastActivityRemoval()); + break; + } + case OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS: { + final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); + if (adjacentTaskFragment == null) { + break; + } + taskFragment.resetAdjacentTaskFragment(); + effects |= TRANSACT_EFFECTS_LIFECYCLE; + + // Clear the focused app if the focused app is no longer visible after reset the + // adjacent TaskFragments. + final ActivityRecord focusedApp = taskFragment.getDisplayContent().mFocusedApp; + final TaskFragment focusedTaskFragment = focusedApp != null + ? focusedApp.getTaskFragment() + : null; + if ((focusedTaskFragment == taskFragment + || focusedTaskFragment == adjacentTaskFragment) + && !focusedTaskFragment.shouldBeVisible(null /* starting */)) { + focusedTaskFragment.getDisplayContent().setFocusedApp(null /* newFocus */); + } + break; + } + case OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: { + final ActivityRecord curFocus = taskFragment.getDisplayContent().mFocusedApp; + if (curFocus != null && curFocus.getTaskFragment() == taskFragment) { + Slog.d(TAG, "The requested TaskFragment already has the focus."); + break; + } + if (curFocus != null && curFocus.getTask() != taskFragment.getTask()) { + Slog.d(TAG, "The Task of the requested TaskFragment doesn't have focus."); + break; + } + final ActivityRecord targetFocus = taskFragment.getTopResumedActivity(); + if (targetFocus == null) { + Slog.d(TAG, "There is no resumed activity in the requested TaskFragment."); + break; + } + taskFragment.getDisplayContent().setFocusedApp(targetFocus); + break; + } + case OP_TYPE_SET_COMPANION_TASK_FRAGMENT: { + final IBinder companionFragmentToken = operation.getSecondaryFragmentToken(); + final TaskFragment companionTaskFragment = companionFragmentToken != null + ? mLaunchTaskFragments.get(companionFragmentToken) + : null; + taskFragment.setCompanionTaskFragment(companionTaskFragment); + break; + } case OP_TYPE_SET_ANIMATION_PARAMS: { final TaskFragmentAnimationParams animationParams = operation.getAnimationParams(); if (animationParams == null) { final Throwable exception = new IllegalArgumentException( "TaskFragmentAnimationParams must be non-null"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION, exception); + opType, exception); break; } taskFragment.setAnimationParams(animationParams); break; } - // TODO(b/263436063): move other TaskFragment related operation here. } - return effect; + return effects; + } + + private boolean validateTaskFragmentOperation( + @NonNull WindowContainerTransaction.HierarchyOp hop, + @Nullable IBinder errorCallbackToken, @Nullable ITaskFragmentOrganizer organizer) { + final TaskFragmentOperation operation = hop.getTaskFragmentOperation(); + final IBinder fragmentToken = hop.getContainer(); + final TaskFragment taskFragment = mLaunchTaskFragments.get(fragmentToken); + if (operation == null) { + final Throwable exception = new IllegalArgumentException( + "TaskFragmentOperation must be non-null"); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + OP_TYPE_UNKNOWN, exception); + return false; + } + final int opType = operation.getOpType(); + if (opType == OP_TYPE_CREATE_TASK_FRAGMENT) { + // No need to check TaskFragment. + return true; + } + + if (!validateTaskFragment(taskFragment, opType, errorCallbackToken, organizer)) { + return false; + } + + final IBinder secondaryFragmentToken = operation.getSecondaryFragmentToken(); + return secondaryFragmentToken == null + || validateTaskFragment(mLaunchTaskFragments.get(secondaryFragmentToken), opType, + errorCallbackToken, organizer); + } + + private boolean validateTaskFragment(@Nullable TaskFragment taskFragment, + @TaskFragmentOperation.OperationType int opType, @Nullable IBinder errorCallbackToken, + @Nullable ITaskFragmentOrganizer organizer) { + if (taskFragment == null || !taskFragment.isAttached()) { + // TaskFragment doesn't exist. + final Throwable exception = new IllegalArgumentException( + "Not allowed to apply operation on invalid fragment tokens opType=" + opType); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + return false; + } + if (taskFragment.isEmbeddedTaskFragmentInPip() + && (opType != OP_TYPE_DELETE_TASK_FRAGMENT + // When the Task enters PiP before the organizer removes the empty TaskFragment, we + // should allow it to delete the TaskFragment for cleanup. + || taskFragment.getTopNonFinishingActivity() != null)) { + final Throwable exception = new IllegalArgumentException( + "Not allowed to apply operation on PIP TaskFragment"); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + return false; + } + return true; } /** A helper method to send minimum dimension violation error to the client. */ @@ -1329,7 +1321,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub + taskFragment.getBounds() + " does not satisfy minimum dimensions:" + minDimensions + " " + reason); sendTaskFragmentOperationFailure(taskFragment.getTaskFragmentOrganizer(), - errorCallbackToken, taskFragment, -1 /* opType */, exception); + errorCallbackToken, taskFragment, OP_TYPE_UNKNOWN, exception); } /** @@ -1366,7 +1358,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final DisplayContent dc = task.getDisplayContent(); if (dc == null) { Slog.w(TAG, "Container is no longer attached: " + task); - return 0; + return TRANSACT_EFFECTS_NONE; } final Task as = task; @@ -1379,7 +1371,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub : WindowContainer.fromBinder(hop.getNewParent()); if (newParent == null) { Slog.e(TAG, "Can't resolve parent window from token"); - return 0; + return TRANSACT_EFFECTS_NONE; } if (task.getParent() != newParent) { if (newParent.asTaskDisplayArea() != null) { @@ -1390,14 +1382,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (newParent.inPinnedWindowingMode()) { Slog.w(TAG, "Can't support moving a task to another PIP window..." + " newParent=" + newParent + " task=" + task); - return 0; + return TRANSACT_EFFECTS_NONE; } if (!task.supportsMultiWindowInDisplayArea( newParent.asTask().getDisplayArea())) { Slog.w(TAG, "Can't support task that doesn't support multi-window" + " mode in multi-window mode... newParent=" + newParent + " task=" + task); - return 0; + return TRANSACT_EFFECTS_NONE; } } task.reparent((Task) newParent, @@ -1459,22 +1451,22 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (currentParent == newParent) { Slog.e(TAG, "reparentChildrenTasksHierarchyOp parent not changing: " + hop); - return 0; + return TRANSACT_EFFECTS_NONE; } if (!currentParent.isAttached()) { Slog.e(TAG, "reparentChildrenTasksHierarchyOp currentParent detached=" + currentParent + " hop=" + hop); - return 0; + return TRANSACT_EFFECTS_NONE; } if (!newParent.isAttached()) { Slog.e(TAG, "reparentChildrenTasksHierarchyOp newParent detached=" + newParent + " hop=" + hop); - return 0; + return TRANSACT_EFFECTS_NONE; } if (newParent.inPinnedWindowingMode()) { Slog.e(TAG, "reparentChildrenTasksHierarchyOp newParent in PIP=" + newParent + " hop=" + hop); - return 0; + return TRANSACT_EFFECTS_NONE; } final boolean newParentInMultiWindow = newParent.inMultiWindowMode(); @@ -1553,9 +1545,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by" + " organizer root1=" + root1 + " root2=" + root2); } - if (root1.isEmbeddedTaskFragmentInPip() || root2.isEmbeddedTaskFragmentInPip()) { - Slog.e(TAG, "Attempt to set adjacent TaskFragment in PIP Task"); - return 0; + if (root1.getAdjacentTaskFragment() == root2) { + return TRANSACT_EFFECTS_NONE; } root1.setAdjacentTaskFragment(root2); return TRANSACT_EFFECTS_LIFECYCLE; @@ -1567,7 +1558,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub throw new IllegalArgumentException("clearAdjacentRootsHierarchyOp: Not created by" + " organizer root=" + root); } - + if (root.getAdjacentTaskFragment() == null) { + return TRANSACT_EFFECTS_NONE; + } root.resetAdjacentTaskFragment(); return TRANSACT_EFFECTS_LIFECYCLE; } @@ -1725,52 +1718,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final int type = hop.getType(); // Check for each type of the operations that are allowed for TaskFragmentOrganizer. switch (type) { - case HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT: - enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getContainer()), organizer); - break; - case HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS: - enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getContainer()), organizer); - enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getAdjacentRoot()), - organizer); - break; - case HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS: - enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getContainer()), organizer); - break; - case HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT: - // We are allowing organizer to create TaskFragment. We will check the - // ownerToken in #createTaskFragment, and trigger error callback if that is not - // valid. - break; - case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: - case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: - case HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION: - enforceTaskFragmentOrganized(func, hop.getContainer(), organizer); - break; - case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: - enforceTaskFragmentOrganized(func, hop.getNewParent(), organizer); - break; - case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: + case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: enforceTaskFragmentOrganized(func, hop.getContainer(), organizer); - if (hop.getCompanionContainer() != null) { - enforceTaskFragmentOrganized(func, hop.getCompanionContainer(), organizer); - } - break; - case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: - enforceTaskFragmentOrganized(func, hop.getContainer(), organizer); - if (hop.getAdjacentRoot() != null) { - enforceTaskFragmentOrganized(func, hop.getAdjacentRoot(), organizer); - } - break; - case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: - enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getContainer()), organizer); - if (hop.getNewParent() != null) { + if (hop.getTaskFragmentOperation() != null + && hop.getTaskFragmentOperation().getSecondaryFragmentToken() != null) { enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getNewParent()), + hop.getTaskFragmentOperation().getSecondaryFragmentToken(), organizer); } break; @@ -1917,21 +1870,21 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final Throwable exception = new IllegalArgumentException("TaskFragment token must be unique"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, - HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + OP_TYPE_CREATE_TASK_FRAGMENT, exception); return; } if (ownerActivity == null || ownerActivity.getTask() == null) { final Throwable exception = new IllegalArgumentException("Not allowed to operate with invalid ownerToken"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, - HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + OP_TYPE_CREATE_TASK_FRAGMENT, exception); return; } if (!ownerActivity.isResizeable()) { final IllegalArgumentException exception = new IllegalArgumentException("Not allowed" + " to operate with non-resizable owner Activity"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, - HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + OP_TYPE_CREATE_TASK_FRAGMENT, exception); return; } // The ownerActivity has to belong to the same app as the target Task. @@ -1942,14 +1895,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub new SecurityException("Not allowed to operate with the ownerToken while " + "the root activity of the target task belong to the different app"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, - HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + OP_TYPE_CREATE_TASK_FRAGMENT, exception); return; } if (ownerTask.inPinnedWindowingMode()) { final Throwable exception = new IllegalArgumentException( "Not allowed to create TaskFragment in PIP Task"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, - HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + OP_TYPE_CREATE_TASK_FRAGMENT, exception); return; } final TaskFragment taskFragment = new TaskFragment(mService, @@ -1984,91 +1937,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (transition != null) transition.collectExistenceChange(taskFragment); } - private void reparentTaskFragment(@NonNull TaskFragment oldParent, - @Nullable WindowContainer<?> newParent, @Nullable ITaskFragmentOrganizer organizer, - @Nullable IBinder errorCallbackToken, @Nullable Transition transition) { - final TaskFragment newParentTF; - if (newParent == null) { - // Use the old parent's parent if the caller doesn't specify the new parent. - newParentTF = oldParent.getTask(); - } else { - newParentTF = newParent.asTaskFragment(); - } - if (newParentTF == null) { - final Throwable exception = - new IllegalArgumentException("Not allowed to operate with invalid container"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, newParentTF, - HIERARCHY_OP_TYPE_REPARENT_CHILDREN, exception); - return; - } - if (newParentTF.getTaskFragmentOrganizer() != null) { - // We are reparenting activities to a new embedded TaskFragment, this operation is only - // allowed if the new parent is trusted by all reparent activities. - final boolean isEmbeddingDisallowed = oldParent.forAllActivities(activity -> - newParentTF.isAllowedToEmbedActivity(activity) != EMBEDDING_ALLOWED); - if (isEmbeddingDisallowed) { - final Throwable exception = new SecurityException( - "The new parent is not allowed to embed the activities."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, newParentTF, - HIERARCHY_OP_TYPE_REPARENT_CHILDREN, exception); - return; - } - } - if (newParentTF.isEmbeddedTaskFragmentInPip() || oldParent.isEmbeddedTaskFragmentInPip()) { - final Throwable exception = new SecurityException( - "Not allow to reparent in TaskFragment in PIP Task."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, newParentTF, - HIERARCHY_OP_TYPE_REPARENT_CHILDREN, exception); - return; - } - if (newParentTF.getTask() != oldParent.getTask()) { - final Throwable exception = new SecurityException( - "The new parent is not in the same Task as the old parent."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, newParentTF, - HIERARCHY_OP_TYPE_REPARENT_CHILDREN, exception); - return; - } - if (transition != null) { - // Collect the current parent. It's visibility may change as a result of this - // reparenting. - transition.collect(oldParent); - transition.collect(newParentTF); - } - while (oldParent.hasChild()) { - final WindowContainer child = oldParent.getChildAt(0); - if (transition != null) { - transition.collect(child); - } - child.reparent(newParentTF, POSITION_TOP); - } - } - private int deleteTaskFragment(@NonNull TaskFragment taskFragment, - @Nullable ITaskFragmentOrganizer organizer, @Nullable IBinder errorCallbackToken, @Nullable Transition transition) { - final int index = mLaunchTaskFragments.indexOfValue(taskFragment); - if (index < 0) { - final Throwable exception = - new IllegalArgumentException("Not allowed to operate with invalid " - + "taskFragment"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT, exception); - return 0; - } - if (taskFragment.isEmbeddedTaskFragmentInPip() - // When the Task enters PiP before the organizer removes the empty TaskFragment, we - // should allow it to do the cleanup. - && taskFragment.getTopNonFinishingActivity() != null) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to delete TaskFragment in PIP Task"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT, exception); - return 0; - } - if (transition != null) transition.collectExistenceChange(taskFragment); - mLaunchTaskFragments.removeAt(index); + mLaunchTaskFragments.remove(taskFragment.getFragmentToken()); taskFragment.remove(true /* withTransition */, "deleteTaskFragment"); return TRANSACT_EFFECTS_LIFECYCLE; } @@ -2093,8 +1966,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } void sendTaskFragmentOperationFailure(@NonNull ITaskFragmentOrganizer organizer, - @Nullable IBinder errorCallbackToken, @Nullable TaskFragment taskFragment, int opType, - @NonNull Throwable exception) { + @Nullable IBinder errorCallbackToken, @Nullable TaskFragment taskFragment, + @TaskFragmentOperation.OperationType int opType, @NonNull Throwable exception) { if (organizer == null) { throw new IllegalArgumentException("Not allowed to operate with invalid organizer"); } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 096f8f67d494..e08baccc2da8 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -3068,12 +3068,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mLastReportedConfiguration.getMergedConfiguration(); } - /** Returns the last window configuration bounds reported to the client. */ - Rect getLastReportedBounds() { - final Rect bounds = getLastReportedConfiguration().windowConfiguration.getBounds(); - return !bounds.isEmpty() ? bounds : getBounds(); - } - void adjustStartingWindowFlags() { if (mAttrs.type == TYPE_BASE_APPLICATION && mActivityRecord != null && mActivityRecord.mStartingWindow != null) { @@ -4390,6 +4384,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP pw.print("null"); } + if (mXOffset != 0 || mYOffset != 0) { + pw.println(prefix + "mXOffset=" + mXOffset + " mYOffset=" + mYOffset); + } if (mHScale != 1 || mVScale != 1) { pw.println(prefix + "mHScale=" + mHScale + " mVScale=" + mVScale); @@ -5527,7 +5524,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mSurfacePosition); if (mWallpaperScale != 1f) { - final Rect bounds = getLastReportedBounds(); + final Rect bounds = getParentFrame(); Matrix matrix = mTmpMatrix; matrix.setTranslate(mXOffset, mYOffset); matrix.postScale(mWallpaperScale, mWallpaperScale, bounds.exactCenterX(), @@ -5640,6 +5637,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP && imeTarget.compareTo(this) <= 0; return inTokenWithAndAboveImeTarget; } + + // The condition is for the system dialog not belonging to any Activity. + // (^FLAG_NOT_FOCUSABLE & FLAG_ALT_FOCUSABLE_IM) means the dialog is still focusable but + // should be placed above the IME window. + if ((mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)) + == FLAG_ALT_FOCUSABLE_IM && isTrustedOverlay() && canAddInternalSystemWindow()) { + return true; + } return false; } @@ -6262,13 +6267,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } @Override - public void unfreezeInsetsAfterStartInput() { - if (mActivityRecord != null) { - mActivityRecord.mImeInsetsFrozenUntilStartInput = false; - } - } - - @Override public boolean isInputMethodClientFocus(int uid, int pid) { return getDisplayContent().isInputMethodClientFocus(uid, pid); } diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp index b7a4fd1be261..4cb7a8fc04de 100644 --- a/services/core/jni/com_android_server_companion_virtual_InputController.cpp +++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp @@ -81,7 +81,7 @@ static std::map<int, int> TOOL_TYPE_MAPPING = { static std::map<int, int> DPAD_KEY_CODE_MAPPING = { {AKEYCODE_DPAD_DOWN, KEY_DOWN}, {AKEYCODE_DPAD_UP, KEY_UP}, {AKEYCODE_DPAD_LEFT, KEY_LEFT}, {AKEYCODE_DPAD_RIGHT, KEY_RIGHT}, - {AKEYCODE_DPAD_CENTER, KEY_SELECT}, + {AKEYCODE_DPAD_CENTER, KEY_SELECT}, {AKEYCODE_BACK, KEY_BACK}, }; // Keycode mapping from https://source.android.com/devices/input/keyboard-devices @@ -378,7 +378,7 @@ static bool writeKeyEvent(jint fd, jint androidKeyCode, jint action, const std::map<int, int>& keyCodeMapping) { auto keyCodeIterator = keyCodeMapping.find(androidKeyCode); if (keyCodeIterator == keyCodeMapping.end()) { - ALOGE("No supportive native keycode for androidKeyCode %d", androidKeyCode); + ALOGE("Unsupported native keycode for androidKeyCode %d", androidKeyCode); return false; } auto actionIterator = KEY_ACTION_MAPPING.find(action); @@ -512,4 +512,4 @@ int register_android_server_companion_virtual_InputController(JNIEnv* env) { methods, NELEM(methods)); } -} // namespace android
\ No newline at end of file +} // namespace android diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java index 2141d5188f70..595d03d9845d 100644 --- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java @@ -84,14 +84,12 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta respondToClientWithResponseAndFinish(); } - @Override protected void onProviderResponseComplete(ComponentName componentName) { if (!isAnyProviderPending()) { onFinalResponseReceived(componentName, null); } } - @Override protected void onProviderTerminated(ComponentName componentName) { if (!isAnyProviderPending()) { processResponses(); @@ -138,6 +136,6 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta } } // TODO: Replace with properly defined error type - respondToClientWithErrorAndFinish("unknown", "All providers failed"); + respondToClientWithErrorAndFinish("UNKNOWN", "All providers failed"); } } diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index 3091c0a116c9..82c235820f69 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; +import android.credentials.CreateCredentialException; import android.credentials.CreateCredentialRequest; import android.credentials.CreateCredentialResponse; import android.credentials.CredentialManager; @@ -86,20 +87,13 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR } @Override - public void onProviderStatusChanged(ProviderSession.Status status, - ComponentName componentName) { - super.onProviderStatusChanged(status, componentName); - } - - @Override public void onFinalResponseReceived(ComponentName componentName, @Nullable CreateCredentialResponse response) { Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); if (response != null) { respondToClientWithResponseAndFinish(response); } else { - // TODO("Replace with properly defined error type) - respondToClientWithErrorAndFinish("unknown_type", + respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREDENTIAL, "Invalid response"); } } @@ -113,7 +107,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR @Override public void onUiCancellation() { // TODO("Replace with properly defined error type") - respondToClientWithErrorAndFinish("user_cancelled", + respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREDENTIAL, "User cancelled the selector"); } @@ -136,4 +130,22 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR } finishSession(); } + + @Override + public void onProviderStatusChanged(ProviderSession.Status status, + ComponentName componentName) { + Log.i(TAG, "in onProviderStatusChanged with status: " + status); + // If all provider responses have been received, we can either need the UI, + // or we need to respond with error. The only other case is the entry being + // selected after the UI has been invoked which has a separate code path. + if (!isAnyProviderPending()) { + if (isUiInvocationNeeded()) { + Log.i(TAG, "in onProviderStatusChanged - isUiInvocationNeeded"); + getProviderDataAndInitiateUi(); + } else { + respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREDENTIAL, + "No credentials available"); + } + } + } } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 50ce1c3dafc5..aefd300956d3 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -26,7 +26,9 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.credentials.ClearCredentialStateRequest; +import android.credentials.CreateCredentialException; import android.credentials.CreateCredentialRequest; +import android.credentials.GetCredentialException; import android.credentials.GetCredentialOption; import android.credentials.GetCredentialRequest; import android.credentials.IClearCredentialStateCallback; @@ -50,6 +52,7 @@ import android.service.credentials.CredentialProviderInfo; import android.text.TextUtils; import android.util.Log; import android.util.Slog; +import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.server.infra.AbstractMasterSystemService; @@ -73,6 +76,16 @@ public final class CredentialManagerService private static final String TAG = "CredManSysService"; + private final Context mContext; + + /** + * Cache of system service list per user id. + */ + @GuardedBy("mLock") + private final SparseArray<List<CredentialManagerServiceImpl>> mSystemServicesCacheList = + new SparseArray<>(); + + public CredentialManagerService(@NonNull Context context) { super( context, @@ -80,6 +93,20 @@ public final class CredentialManagerService context, Settings.Secure.CREDENTIAL_SERVICE, /* isMultipleMode= */ true), null, PACKAGE_UPDATE_POLICY_REFRESH_EAGER); + mContext = context; + } + + @NonNull + @GuardedBy("mLock") + private List<CredentialManagerServiceImpl> constructSystemServiceListLocked( + int resolvedUserId) { + List<CredentialManagerServiceImpl> services = new ArrayList<>(); + List<CredentialProviderInfo> credentialProviderInfos = + CredentialProviderInfo.getAvailableSystemServices(mContext, resolvedUserId); + credentialProviderInfos.forEach(info -> { + services.add(new CredentialManagerServiceImpl(this, mLock, resolvedUserId, info)); + }); + return services; } @Override @@ -105,8 +132,10 @@ public final class CredentialManagerService } @Override // from AbstractMasterSystemService + @GuardedBy("mLock") protected List<CredentialManagerServiceImpl> newServiceListLocked( int resolvedUserId, boolean disabled, String[] serviceNames) { + getOrConstructSystemServiceListLock(resolvedUserId); if (serviceNames == null || serviceNames.length == 0) { Slog.i(TAG, "serviceNames sent in newServiceListLocked is null, or empty"); return new ArrayList<>(); @@ -155,13 +184,24 @@ public final class CredentialManagerService // TODO("Iterate over system services and remove if needed") } + @GuardedBy("mLock") + private List<CredentialManagerServiceImpl> getOrConstructSystemServiceListLock( + int resolvedUserId) { + List<CredentialManagerServiceImpl> services = mSystemServicesCacheList.get(resolvedUserId); + if (services == null || services.size() == 0) { + services = constructSystemServiceListLocked(resolvedUserId); + mSystemServicesCacheList.put(resolvedUserId, services); + } + return services; + } + private void runForUser(@NonNull final Consumer<CredentialManagerServiceImpl> c) { final int userId = UserHandle.getCallingUserId(); final long origId = Binder.clearCallingIdentity(); try { synchronized (mLock) { final List<CredentialManagerServiceImpl> services = - getServiceListForUserLocked(userId); + getAllCredentialProviderServicesLocked(userId); for (CredentialManagerServiceImpl s : services) { c.accept(s); } @@ -171,6 +211,19 @@ public final class CredentialManagerService } } + @GuardedBy("mLock") + private List<CredentialManagerServiceImpl> getAllCredentialProviderServicesLocked( + int userId) { + List<CredentialManagerServiceImpl> concatenatedServices = new ArrayList<>(); + List<CredentialManagerServiceImpl> userConfigurableServices = + getServiceListForUserLocked(userId); + if (userConfigurableServices != null && !userConfigurableServices.isEmpty()) { + concatenatedServices.addAll(userConfigurableServices); + } + concatenatedServices.addAll(getOrConstructSystemServiceListLock(userId)); + return concatenatedServices; + } + @SuppressWarnings("GuardedBy") // ErrorProne requires initiateProviderSessionForRequestLocked // to be guarded by 'service.mLock', which is the same as mLock. private List<ProviderSession> initiateProviderSessions( @@ -235,8 +288,8 @@ public final class CredentialManagerService if (providerSessions.isEmpty()) { try { - // TODO("Replace with properly defined error type") - callback.onError("unknown_type", "No providers available to fulfill request."); + callback.onError(GetCredentialException.TYPE_NO_CREDENTIAL, + "No credentials available on this device."); } catch (RemoteException e) { Log.i( TAG, @@ -248,14 +301,10 @@ public final class CredentialManagerService // Iterate over all provider sessions and invoke the request providerSessions.forEach( - providerGetSession -> { - providerGetSession - .getRemoteCredentialService() - .onBeginGetCredential( - (BeginGetCredentialRequest) - providerGetSession.getProviderRequest(), - /* callback= */ providerGetSession); - }); + providerGetSession -> providerGetSession + .getRemoteCredentialService().onBeginGetCredential( + (BeginGetCredentialRequest) providerGetSession.getProviderRequest(), + /*callback=*/providerGetSession)); return cancelTransport; } @@ -284,8 +333,8 @@ public final class CredentialManagerService if (providerSessions.isEmpty()) { try { - // TODO("Replace with properly defined error type") - callback.onError("unknown_type", "No providers available to fulfill request."); + callback.onError(CreateCredentialException.TYPE_NO_CREDENTIAL, + "No credentials available on this device."); } catch (RemoteException e) { Log.i( TAG, @@ -297,14 +346,12 @@ public final class CredentialManagerService // Iterate over all provider sessions and invoke the request providerSessions.forEach( - providerCreateSession -> { - providerCreateSession - .getRemoteCredentialService() - .onCreateCredential( - (BeginCreateCredentialRequest) - providerCreateSession.getProviderRequest(), - /* callback= */ providerCreateSession); - }); + providerCreateSession -> providerCreateSession + .getRemoteCredentialService() + .onCreateCredential( + (BeginCreateCredentialRequest) + providerCreateSession.getProviderRequest(), + /* callback= */ providerCreateSession)); return cancelTransport; } @@ -402,7 +449,8 @@ public final class CredentialManagerService if (providerSessions.isEmpty()) { try { // TODO("Replace with properly defined error type") - callback.onError("unknown_type", "No providers available to fulfill request."); + callback.onError("UNKNOWN", "No crdentials available on this " + + "device"); } catch (RemoteException e) { Log.i( TAG, diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index 0fd1f1929cae..546c48fe05f4 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -58,7 +58,18 @@ public final class CredentialManagerServiceImpl extends return mInfo.getServiceInfo().getComponentName(); } - @Override // from PerUserSystemService + CredentialManagerServiceImpl( + @NonNull CredentialManagerService master, + @NonNull Object lock, int userId, CredentialProviderInfo providerInfo) { + super(master, lock, userId); + Log.i(TAG, "in CredentialManagerServiceImpl constructed with system constructor: " + + providerInfo.isSystemProvider() + + " , " + providerInfo.getServiceInfo() == null ? "" : + providerInfo.getServiceInfo().getComponentName().flattenToString()); + mInfo = providerInfo; + } + + @Override // from PerUserSystemService when a new setting based service is to be created @GuardedBy("mLock") protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) throws PackageManager.NameNotFoundException { @@ -71,7 +82,9 @@ public final class CredentialManagerServiceImpl extends Log.i(TAG, "newServiceInfoLocked with null mInfo , " + serviceComponent.getPackageName()); } - mInfo = new CredentialProviderInfo(getContext(), serviceComponent, mUserId); + mInfo = new CredentialProviderInfo( + getContext(), serviceComponent, + mUserId, /*isSystemProvider=*/false); return mInfo.getServiceInfo(); } diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index 06396e003745..c7fa72c27abb 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -19,6 +19,7 @@ package com.android.server.credentials; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; +import android.credentials.GetCredentialException; import android.credentials.GetCredentialRequest; import android.credentials.GetCredentialResponse; import android.credentials.IGetCredentialCallback; @@ -80,12 +81,6 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest } } - @Override // from provider session - public void onProviderStatusChanged(ProviderSession.Status status, - ComponentName componentName) { - super.onProviderStatusChanged(status, componentName); - } - @Override public void onFinalResponseReceived(ComponentName componentName, @Nullable GetCredentialResponse response) { @@ -93,8 +88,7 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest if (response != null) { respondToClientWithResponseAndFinish(response); } else { - // TODO("Replace with no credentials/unknown type when ready) - respondToClientWithErrorAndFinish("unknown_type", + respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, "Invalid response from provider"); } } @@ -108,29 +102,46 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest } private void respondToClientWithResponseAndFinish(GetCredentialResponse response) { - Log.i(TAG, "respondToClientWithResponseAndFinish"); try { mClientCallback.onResponse(response); } catch (RemoteException e) { - e.printStackTrace(); + Log.i(TAG, "Issue while responding to client with a response : " + e.getMessage()); } finishSession(); } private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { - Log.i(TAG, "respondToClientWithErrorAndFinish"); try { mClientCallback.onError(errorType, errorMsg); } catch (RemoteException e) { - e.printStackTrace(); + Log.i(TAG, "Issue while responding to client with error : " + e.getMessage()); + } finishSession(); } @Override public void onUiCancellation() { - // TODO("Replace with properly defined error type") - respondToClientWithErrorAndFinish("user_canceled", + // TODO("Replace with user cancelled error type when ready") + respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, "User cancelled the selector"); } + + @Override + public void onProviderStatusChanged(ProviderSession.Status status, + ComponentName componentName) { + Log.i(TAG, "in onStatusChanged with status: " + status); + if (!isAnyProviderPending()) { + // If all provider responses have been received, we can either need the UI, + // or we need to respond with error. The only other case is the entry being + // selected after the UI has been invoked which has a separate code path. + if (isUiInvocationNeeded()) { + Log.i(TAG, "in onProviderStatusChanged - isUiInvocationNeeded"); + getProviderDataAndInitiateUi(); + } else { + respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, + "No credentials available"); + } + } + } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 9163b8e65ba8..27eaa0b26773 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -22,6 +22,7 @@ import android.annotation.UserIdInt; import android.content.Context; import android.content.Intent; import android.credentials.CreateCredentialException; +import android.credentials.CreateCredentialResponse; import android.credentials.ui.CreateCredentialProviderData; import android.credentials.ui.Entry; import android.credentials.ui.ProviderPendingIntentResponse; @@ -98,7 +99,7 @@ public final class ProviderCreateSession extends ProviderSession< private ProviderCreateSession( @NonNull Context context, @NonNull CredentialProviderInfo info, - @NonNull ProviderInternalCallback callbacks, + @NonNull ProviderInternalCallback<CreateCredentialResponse> callbacks, @UserIdInt int userId, @NonNull RemoteCredentialService remoteCredentialService, @NonNull BeginCreateCredentialRequest beginCreateRequest, @@ -180,9 +181,7 @@ public final class ProviderCreateSession extends ProviderSession< onSaveEntrySelected(providerPendingIntentResponse); } else { Log.i(TAG, "Unexpected save entry key"); - // TODO("Replace with no credentials error type"); - invokeCallbackWithError("unknown_type", - "Issue while retrieving credential"); + invokeCallbackOnInternalInvalidState(); } break; case REMOTE_ENTRY_KEY: @@ -190,9 +189,7 @@ public final class ProviderCreateSession extends ProviderSession< onRemoteEntrySelected(providerPendingIntentResponse); } else { Log.i(TAG, "Unexpected remote entry key"); - // TODO("Replace with unknown/no credentials exception") - invokeCallbackWithError("unknown_type", - "Issue while retrieving credential"); + invokeCallbackOnInternalInvalidState(); } break; default: @@ -248,23 +245,16 @@ public final class ProviderCreateSession extends ProviderSession< } else { Log.i(TAG, "onSaveEntrySelected - no response or error found in pending " + "intent response"); - invokeCallbackWithError( - // TODO("Replace with unknown/no credentials exception") - "unknown", - "Issue encountered while retrieving the credential"); + invokeCallbackOnInternalInvalidState(); } } - private void invokeCallbackWithError(String errorType, @Nullable String message) { - mCallbacks.onFinalErrorReceived(mComponentName, errorType, message); - } - @Nullable private CreateCredentialException maybeGetPendingIntentException( ProviderPendingIntentResponse pendingIntentResponse) { if (pendingIntentResponse == null) { Log.i(TAG, "pendingIntentResponse is null"); - return null; + return new CreateCredentialException(CreateCredentialException.TYPE_NO_CREDENTIAL); } if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) { CreateCredentialException exception = PendingIntentResultHandler @@ -276,8 +266,19 @@ public final class ProviderCreateSession extends ProviderSession< } else { Log.i(TAG, "Pending intent result code not Activity.RESULT_OK"); // TODO("Update with unknown exception when ready") - return new CreateCredentialException("unknown"); + return new CreateCredentialException(CreateCredentialException.TYPE_NO_CREDENTIAL); } return null; } + + /** + * When an invalid state occurs, e.g. entry mismatch or no response from provider, + * we send back a TYPE_NO_CREDENTIAL error as to the developer, it is the same as not + * getting any credentials back. + */ + private void invokeCallbackOnInternalInvalidState() { + mCallbacks.onFinalErrorReceived(mComponentName, + CreateCredentialException.TYPE_NO_CREDENTIAL, + null); + } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 9846d83a83b3..de93af41671d 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -144,7 +144,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential public ProviderGetSession(Context context, CredentialProviderInfo info, - ProviderInternalCallback callbacks, + ProviderInternalCallback<GetCredentialResponse> callbacks, int userId, RemoteCredentialService remoteCredentialService, BeginGetCredentialRequest beginGetRequest, android.credentials.GetCredentialRequest completeGetRequest) { @@ -195,9 +195,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential CredentialEntry credentialEntry = mUiCredentialEntries.get(entryKey); if (credentialEntry == null) { Log.i(TAG, "Unexpected credential entry key"); - // TODO("Replace with no credentials/unknown exception") - invokeCallbackWithError("unknown_type", - "Issue while retrieving credential"); + invokeCallbackOnInternalInvalidState(); return; } onCredentialEntrySelected(credentialEntry, providerPendingIntentResponse); @@ -206,9 +204,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential Action actionEntry = mUiActionsEntries.get(entryKey); if (actionEntry == null) { Log.i(TAG, "Unexpected action entry key"); - // TODO("Replace with no credentials/unknown exception") - invokeCallbackWithError("unknown_type", - "Issue while retrieving credential"); + invokeCallbackOnInternalInvalidState(); return; } onActionEntrySelected(providerPendingIntentResponse); @@ -218,9 +214,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential onAuthenticationEntrySelected(providerPendingIntentResponse); } else { Log.i(TAG, "Unexpected authentication entry key"); - // TODO("Replace with no credentials/unknown exception") - invokeCallbackWithError("unknown_type", - "Issue while retrieving credential"); + invokeCallbackOnInternalInvalidState(); } break; case REMOTE_ENTRY_KEY: @@ -228,9 +222,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential onRemoteEntrySelected(providerPendingIntentResponse); } else { Log.i(TAG, "Unexpected remote entry key"); - // TODO("Replace with no credentials/unknown exception") - invokeCallbackWithError("unknown_type", - "Issue while retrieving credential"); + invokeCallbackOnInternalInvalidState(); } break; default: @@ -238,11 +230,6 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential } } - private void invokeCallbackWithError(String errorType, @Nullable String errorMessage) { - // TODO: Determine what the error message should be - mCallbacks.onFinalErrorReceived(mComponentName, errorType, errorMessage); - } - @Override // Call from request session to data to be shown on the UI @Nullable protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException { Log.i(TAG, "In prepareUiData"); @@ -288,7 +275,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential String entryId = generateEntryId(); Entry authEntry = new Entry( AUTHENTICATION_ACTION_ENTRY_KEY, entryId, - authenticationAction.getSlice()); + authenticationAction.getSlice(), + setUpFillInIntentForAuthentication()); mUiAuthenticationAction = new Pair<>(entryId, authenticationAction); return authEntry; } @@ -324,6 +312,15 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential return intent; } + private Intent setUpFillInIntentForAuthentication() { + Intent intent = new Intent(); + intent.putExtra( + CredentialProviderService + .EXTRA_BEGIN_GET_CREDENTIAL_REQUEST, + mProviderRequest); + return intent; + } + private List<Entry> prepareUiActionEntries(@Nullable List<Action> actions) { List<Entry> actionEntries = new ArrayList<>(); for (Action action : actions) { @@ -369,20 +366,15 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential } Log.i(TAG, "Pending intent response contains no credential, or error"); - // TODO("Replace with no credentials/unknown error when ready) - invokeCallbackWithError("unknown_type", - "Issue while retrieving credential"); + invokeCallbackOnInternalInvalidState(); } Log.i(TAG, "CredentialEntry does not have a credential or a pending intent result"); - // TODO("Replace with no credentials/unknown error when ready) - invokeCallbackWithError("unknown_type", - "Error encountered while retrieving the credential"); + invokeCallbackOnInternalInvalidState(); } private void onAuthenticationEntrySelected( @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) { //TODO: Other provider intent statuses - // Check if pending intent has an error GetCredentialException exception = maybeGetPendingIntentException( providerPendingIntentResponse); if (exception != null) { @@ -401,9 +393,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential } Log.i(TAG, "No error or respond found in pending intent response"); - // TODO("Replace with no credentials/unknown error when ready) - invokeCallbackWithError("unknown type", "Issue" - + " while retrieving credential"); + invokeCallbackOnInternalInvalidState(); } private void onActionEntrySelected(ProviderPendingIntentResponse @@ -430,7 +420,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential ProviderPendingIntentResponse pendingIntentResponse) { if (pendingIntentResponse == null) { Log.i(TAG, "pendingIntentResponse is null"); - return null; + return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL); } if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) { GetCredentialException exception = PendingIntentResultHandler @@ -441,9 +431,19 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential } } else { Log.i(TAG, "Pending intent result code not Activity.RESULT_OK"); - // TODO("Update with unknown exception when ready") - return new GetCredentialException("unknown"); + return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL); } return null; } + + /** + * When an invalid state occurs, e.g. entry mismatch or no response from provider, + * we send back a TYPE_NO_CREDENTIAL error as to the developer, it is the same as not + * getting any credentials back. + */ + private void invokeCallbackOnInternalInvalidState() { + mCallbacks.onFinalErrorReceived(mComponentName, + GetCredentialException.TYPE_NO_CREDENTIAL, + null); + } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index 93e816a6e88b..7036dfb94163 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -191,6 +191,11 @@ public abstract class ProviderSession<T, R> return mProviderResponse != null || mProviderResponseSet; } + protected void invokeCallbackWithError(String errorType, @Nullable String errorMessage) { + // TODO: Determine what the error message should be + mCallbacks.onFinalErrorReceived(mComponentName, errorType, errorMessage); + } + /** Update the response state stored with the provider session. */ @Nullable protected R getProviderResponse() { return mProviderResponse; diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index f59a0efa9b72..0c3c34e52d0f 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -18,7 +18,6 @@ package com.android.server.credentials; import android.annotation.NonNull; import android.annotation.UserIdInt; -import android.content.ComponentName; import android.content.Context; import android.credentials.ui.ProviderData; import android.credentials.ui.UserSelectionDialogResult; @@ -98,41 +97,6 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan finishSession(); } - protected void onProviderStatusChanged(ProviderSession.Status status, - ComponentName componentName) { - Log.i(TAG, "in onStatusChanged with status: " + status); - if (ProviderSession.isTerminatingStatus(status)) { - Log.i(TAG, "in onStatusChanged terminating status"); - onProviderTerminated(componentName); - //TODO: Check if this was the provider we were waiting for and can invoke the UI now - } else if (ProviderSession.isCompletionStatus(status)) { - Log.i(TAG, "in onStatusChanged isCompletionStatus status"); - onProviderResponseComplete(componentName); - } else if (ProviderSession.isUiInvokingStatus(status)) { - Log.i(TAG, "in onStatusChanged isUiInvokingStatus status"); - onProviderResponseRequiresUi(); - } - } - - protected void onProviderTerminated(ComponentName componentName) { - //TODO: Implement - } - - protected void onProviderResponseComplete(ComponentName componentName) { - //TODO: Implement - } - - protected void onProviderResponseRequiresUi() { - Log.i(TAG, "in onProviderResponseComplete"); - // TODO: Determine whether UI has already been invoked, and deal accordingly - if (!isAnyProviderPending()) { - Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders"); - getProviderDataAndInitiateUi(); - } else { - Log.i(TAG, "Can't invoke UI - waiting on some providers"); - } - } - protected void finishSession() { Log.i(TAG, "finishing session"); clearProviderSessions(); @@ -144,7 +108,7 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan mProviders.clear(); } - boolean isAnyProviderPending() { + protected boolean isAnyProviderPending() { for (ProviderSession session : mProviders.values()) { if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) { return true; @@ -153,7 +117,22 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan return false; } - private void getProviderDataAndInitiateUi() { + /** + * Returns true if at least one provider is ready for UI invocation, and no + * provider is pending a response. + */ + boolean isUiInvocationNeeded() { + for (ProviderSession session : mProviders.values()) { + if (ProviderSession.isUiInvokingStatus(session.getStatus())) { + return true; + } else if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) { + return false; + } + } + return false; + } + + void getProviderDataAndInitiateUi() { Log.i(TAG, "In getProviderDataAndInitiateUi"); Log.i(TAG, "In getProviderDataAndInitiateUi providers size: " + mProviders.size()); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index cdb2e08e80e3..8be3df4fbe82 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -8742,21 +8742,6 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } - private boolean isDeviceOwnerPackage(String packageName, int userId) { - synchronized (getLockObject()) { - return mOwners.hasDeviceOwner() - && mOwners.getDeviceOwnerUserId() == userId - && mOwners.getDeviceOwnerPackageName().equals(packageName); - } - } - - private boolean isProfileOwnerPackage(String packageName, int userId) { - synchronized (getLockObject()) { - return mOwners.hasProfileOwner(userId) - && mOwners.getProfileOwnerPackage(userId).equals(packageName); - } - } - public boolean isProfileOwner(ComponentName who, int userId) { final ComponentName profileOwner = mInjector.binderWithCleanCallingIdentity(() -> getProfileOwnerAsUser(userId)); @@ -9315,7 +9300,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { boolean hasProfileOwner = mOwners.hasProfileOwner(userId); if (!hasProfileOwner) { int managedUserId = getManagedUserId(userId); - if (managedUserId == -1 && newState != STATE_USER_UNMANAGED) { + if (managedUserId < 0 && newState != STATE_USER_UNMANAGED) { // No managed device, user or profile, so setting provisioning state makes // no sense. String error = "Not allowed to change provisioning state unless a " @@ -12524,7 +12509,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { /** * @return the user ID of the managed user that is linked to the current user, if any. - * Otherwise -1. + * Otherwise UserHandle.USER_NULL (-10000). */ public int getManagedUserId(@UserIdInt int callingUserId) { if (VERBOSE_LOG) Slogf.v(LOG_TAG, "getManagedUserId: callingUserId=%d", callingUserId); @@ -12537,7 +12522,26 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return ui.id; } if (VERBOSE_LOG) Slogf.v(LOG_TAG, "Managed user not found."); - return -1; + return UserHandle.USER_NULL; + } + + /** + * Returns the userId of the managed profile on the device. + * If none exists, return {@link UserHandle#USER_NULL}. + * + * We assume there is only one managed profile across all users + * on the device, which is true for now (HSUM or not) but could + * change in future. + */ + private @UserIdInt int getManagedUserId() { + // On HSUM, there is only one main user and only the main user + // can have a managed profile (for now). On non-HSUM, only user 0 + // can host the managed profile and user 0 is the main user. + // So in both cases, we could just get the main user and + // search for the profile user under it. + UserHandle mainUser = mUserManager.getMainUser(); + if (mainUser == null) return UserHandle.USER_NULL; + return getManagedUserId(mainUser.getIdentifier()); } @Override @@ -16187,7 +16191,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return mOwners.getDeviceOwnerUserId(); } else { return mInjector.binderWithCleanCallingIdentity( - () -> getManagedUserId(UserHandle.USER_SYSTEM)); + () -> getManagedUserId()); } } } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 5ebf6cec4d5c..5c5442d0b9c3 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -108,7 +108,6 @@ import com.android.internal.widget.LockSettingsInternal; import com.android.server.am.ActivityManagerService; import com.android.server.ambientcontext.AmbientContextManagerService; import com.android.server.appbinding.AppBindingService; -import com.android.server.art.ArtManagerLocal; import com.android.server.art.ArtModuleServiceInitializer; import com.android.server.art.DexUseManagerLocal; import com.android.server.attention.AttentionManagerService; @@ -132,8 +131,8 @@ import com.android.server.display.DisplayManagerService; import com.android.server.display.color.ColorDisplayService; import com.android.server.dreams.DreamManagerService; import com.android.server.emergency.EmergencyAffordanceService; -import com.android.server.grammaticalinflection.GrammaticalInflectionService; import com.android.server.gpu.GpuService; +import com.android.server.grammaticalinflection.GrammaticalInflectionService; import com.android.server.graphics.fonts.FontManagerService; import com.android.server.hdmi.HdmiControlService; import com.android.server.incident.IncidentCompanionService; @@ -147,6 +146,7 @@ import com.android.server.logcat.LogcatManagerService; import com.android.server.media.MediaRouterService; import com.android.server.media.metrics.MediaMetricsManagerService; import com.android.server.media.projection.MediaProjectionManagerService; +import com.android.server.net.NetworkManagementService; import com.android.server.net.NetworkPolicyManagerService; import com.android.server.net.watchlist.NetworkWatchlistService; import com.android.server.notification.NotificationManagerService; @@ -163,6 +163,7 @@ import com.android.server.pm.ApexSystemServiceInfo; import com.android.server.pm.BackgroundInstallControlService; import com.android.server.pm.CrossProfileAppsService; import com.android.server.pm.DataLoaderManagerService; +import com.android.server.pm.DexOptHelper; import com.android.server.pm.DynamicCodeLoggingService; import com.android.server.pm.Installer; import com.android.server.pm.LauncherAppsService; @@ -2770,7 +2771,7 @@ public final class SystemServer implements Dumpable { t.traceEnd(); t.traceBegin("ArtManagerLocal"); - LocalManagerRegistry.addManager(ArtManagerLocal.class, new ArtManagerLocal(context)); + DexOptHelper.initializeArtManagerLocal(context, mPackageManagerService); t.traceEnd(); if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB)) { diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt index f4e362ceb2c7..998d2067e070 100644 --- a/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt +++ b/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt @@ -148,6 +148,13 @@ inline fun <K, V> IndexedMap<K, V>.retainAllIndexed(predicate: (Int, K, V) -> Bo return isChanged } +inline fun <K, V, R> IndexedMap<K, V>.mapIndexed(transform: (Int, K, V) -> R): IndexedList<R> = + IndexedList<R>().also { destination -> + forEachIndexed { index, key, value -> + transform(index, key, value).let { destination += it } + } + } + inline fun <K, V, R> IndexedMap<K, V>.mapNotNullIndexed( transform: (Int, K, V) -> R? ): IndexedList<R> = diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index acd0a3cbbb98..903fad33055f 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -73,6 +73,7 @@ import com.android.server.pm.UserManagerInternal import com.android.server.pm.UserManagerService import com.android.server.pm.parsing.pkg.AndroidPackageUtils import com.android.server.pm.permission.LegacyPermission +import com.android.server.pm.permission.Permission as LegacyPermission2 import com.android.server.pm.permission.LegacyPermissionSettings import com.android.server.pm.permission.LegacyPermissionState import com.android.server.pm.permission.PermissionManagerServiceInterface @@ -1673,40 +1674,77 @@ class PermissionService( context.getSystemService(PermissionControllerManager::class.java)!!.dump(fd, args) } - override fun getPermissionTEMP( - permissionName: String - ): com.android.server.pm.permission.Permission? { - // TODO("Not yet implemented") - return null - } + override fun getPermissionTEMP(permissionName: String): LegacyPermission2? { + val permission = service.getState { + with(policy) { getPermissions()[permissionName] } + } ?: return null - override fun getLegacyPermissions(): List<LegacyPermission> { - // TODO("Not yet implemented") - return emptyList() + return LegacyPermission2( + permission.permissionInfo, permission.type, permission.isReconciled, permission.appId, + permission.gids, permission.areGidsPerUser + ) } + override fun getLegacyPermissions(): List<LegacyPermission> = + service.getState { + with(policy) { getPermissions() } + }.mapIndexed { _, _, permission -> + LegacyPermission( + permission.permissionInfo, permission.type, permission.appId, permission.gids + ) + } + override fun readLegacyPermissionsTEMP(legacyPermissionSettings: LegacyPermissionSettings) { // Package settings has been read when this method is called. service.initialize() - // TODO("Not yet implemented") } override fun writeLegacyPermissionsTEMP(legacyPermissionSettings: LegacyPermissionSettings) { - // TODO("Not yet implemented") + service.getState { + val permissions = with(policy) { getPermissions() } + legacyPermissionSettings.replacePermissions(toLegacyPermissions(permissions)) + val permissionTrees = with(policy) { getPermissionTrees() } + legacyPermissionSettings.replacePermissionTrees(toLegacyPermissions(permissionTrees)) + } } + private fun toLegacyPermissions( + permissions: IndexedMap<String, Permission> + ): List<LegacyPermission> = + permissions.mapIndexed { _, _, permission -> + // We don't need to provide UID and GIDs, which are only retrieved when dumping. + LegacyPermission( + permission.permissionInfo, permission.type, 0, EmptyArray.INT + ) + } + override fun getLegacyPermissionState(appId: Int): LegacyPermissionState { - // TODO("Not yet implemented") - return LegacyPermissionState() - } + val legacyState = LegacyPermissionState() + val userIds = userManagerService.userIdsIncludingPreCreated + service.getState { + val permissions = with(policy) { getPermissions() } + userIds.forEachIndexed { _, userId -> + val permissionFlags = with(policy) { getUidPermissionFlags(appId, userId) } + ?: return@forEachIndexed - override fun readLegacyPermissionStateTEMP() { - // TODO("Not yet implemented") + permissionFlags.forEachIndexed permissionFlags@{ _, permissionName, flags -> + val permission = permissions[permissionName] ?: return@permissionFlags + val legacyPermissionState = LegacyPermissionState.PermissionState( + permissionName, + permission.isRuntime, + PermissionFlags.isPermissionGranted(flags), + PermissionFlags.toApiFlags(flags) + ) + legacyState.putPermissionState(legacyPermissionState, userId) + } + } + } + return legacyState } - override fun writeLegacyPermissionStateTEMP() { - // TODO("Not yet implemented") - } + override fun readLegacyPermissionStateTEMP() {} + + override fun writeLegacyPermissionStateTEMP() {} override fun onSystemReady() { // TODO STOPSHIP privappPermissionsViolationsfix check diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java index 5e5e7e3a98a9..7909ba444d85 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java @@ -283,7 +283,7 @@ public class AppsFilterImplTest { assertFalse( appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, calling, target, SYSTEM_USER)); - watcher.verifyNoChangeReported("shouldFilterAplication"); + watcher.verifyNoChangeReported("shouldFilterApplication"); } @Test @@ -1024,7 +1024,10 @@ public class AppsFilterImplTest { DUMMY_TARGET_APPID); PackageSetting calling = simulateAddPackage(appsFilter, pkg("com.some.other.package"), DUMMY_CALLING_APPID, - withInstallSource(target.getPackageName(), null, null, INVALID_UID, null, false)); + withInstallSource(target.getPackageName(), null /* originatingPackageName */, + null /* installerPackageName */, INVALID_UID, + null /* updateOwnerPackageName */, null /* installerAttributionTag */, + false /* isInitiatingPackageUninstalled */)); assertFalse( appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, calling, target, @@ -1043,7 +1046,10 @@ public class AppsFilterImplTest { DUMMY_TARGET_APPID); PackageSetting calling = simulateAddPackage(appsFilter, pkg("com.some.other.package"), DUMMY_CALLING_APPID, - withInstallSource(target.getPackageName(), null, null, INVALID_UID, null, true)); + withInstallSource(target.getPackageName(), null /* originatingPackageName */, + null /* installerPackageName */, INVALID_UID, + null /* updateOwnerPackageName */, null /* installerAttributionTag */, + true /* isInitiatingPackageUninstalled */)); assertTrue( appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, calling, target, @@ -1066,14 +1072,16 @@ public class AppsFilterImplTest { DUMMY_TARGET_APPID); watcher.verifyChangeReported("add package"); PackageSetting calling = simulateAddPackage(appsFilter, pkg("com.some.other.package"), - DUMMY_CALLING_APPID, withInstallSource(null, target.getPackageName(), null, - INVALID_UID, null, false)); + DUMMY_CALLING_APPID, withInstallSource(null /* initiatingPackageName */, + target.getPackageName(), null /* installerPackageName */, INVALID_UID, + null /* updateOwnerPackageName */, null /* installerAttributionTag */, + false /* isInitiatingPackageUninstalled */)); watcher.verifyChangeReported("add package"); assertTrue( appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, calling, target, SYSTEM_USER)); - watcher.verifyNoChangeReported("shouldFilterAplication"); + watcher.verifyNoChangeReported("shouldFilterApplication"); } @Test @@ -1092,14 +1100,46 @@ public class AppsFilterImplTest { DUMMY_TARGET_APPID); watcher.verifyChangeReported("add package"); PackageSetting calling = simulateAddPackage(appsFilter, pkg("com.some.other.package"), - DUMMY_CALLING_APPID, withInstallSource(null, null, target.getPackageName(), - DUMMY_TARGET_APPID, null, false)); + DUMMY_CALLING_APPID, withInstallSource(null /* initiatingPackageName */, + null /* originatingPackageName */, target.getPackageName(), + DUMMY_TARGET_APPID, null /* updateOwnerPackageName */, + null /* installerAttributionTag */, + false /* isInitiatingPackageUninstalled */)); watcher.verifyChangeReported("add package"); assertFalse( appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, calling, target, SYSTEM_USER)); - watcher.verifyNoChangeReported("shouldFilterAplication"); + watcher.verifyNoChangeReported("shouldFilterApplication"); + } + + @Test + public void testUpdateOwner_DoesntFilter() throws Exception { + final AppsFilterImpl appsFilter = + new AppsFilterImpl(mFeatureConfigMock, new String[]{}, /* systemAppsQueryable */ + false, /* overlayProvider */ null, mMockHandler); + final WatchableTester watcher = new WatchableTester(appsFilter, "onChange"); + watcher.register(); + simulateAddBasicAndroid(appsFilter); + watcher.verifyChangeReported("addBasicAndroid"); + appsFilter.onSystemReady(mPmInternal); + watcher.verifyChangeReported("systemReady"); + + PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"), + DUMMY_TARGET_APPID); + watcher.verifyChangeReported("add package"); + PackageSetting calling = simulateAddPackage(appsFilter, pkg("com.some.other.package"), + DUMMY_CALLING_APPID, withInstallSource(null /* initiatingPackageName */, + null /* originatingPackageName */, null /* installerPackageName */, + INVALID_UID, target.getPackageName(), + null /* installerAttributionTag */, + false /* isInitiatingPackageUninstalled */)); + watcher.verifyChangeReported("add package"); + + assertFalse( + appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, calling, target, + SYSTEM_USER)); + watcher.verifyNoChangeReported("shouldFilterApplication"); } @Test @@ -1128,7 +1168,7 @@ public class AppsFilterImplTest { assertFalse( appsFilter.shouldFilterApplication(mSnapshot, DUMMY_TARGET_APPID, target, instrumentation, SYSTEM_USER)); - watcher.verifyNoChangeReported("shouldFilterAplication"); + watcher.verifyNoChangeReported("shouldFilterApplication"); } @Test @@ -1679,10 +1719,12 @@ public class AppsFilterImplTest { private WithSettingBuilder withInstallSource(String initiatingPackageName, String originatingPackageName, String installerPackageName, int installerPackageUid, - String installerAttributionTag, boolean isInitiatingPackageUninstalled) { + String updateOwnerPackageName, String installerAttributionTag, + boolean isInitiatingPackageUninstalled) { final InstallSource installSource = InstallSource.create(initiatingPackageName, originatingPackageName, installerPackageName, installerPackageUid, - installerAttributionTag, /* isOrphaned= */ false, isInitiatingPackageUninstalled); + updateOwnerPackageName, installerAttributionTag, /* isOrphaned= */ false, + isInitiatingPackageUninstalled); return setting -> setting.setInstallSource(installSource); } } diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java index 4da082e0370b..98655c895aff 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java @@ -167,8 +167,8 @@ public class PackageInstallerSessionTest { params.isMultiPackage = true; } InstallSource installSource = InstallSource.create("testInstallInitiator", - "testInstallOriginator", "testInstaller", -1, "testAttributionTag", - PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); + "testInstallOriginator", "testInstaller", -1, "testUpdateOwner", + "testAttributionTag", PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); return new PackageInstallerSession( /* callback */ null, /* context */null, diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt index 8e1ca3c02264..0b7020c74f66 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt @@ -218,6 +218,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::isAllowClearUserDataOnFailedRestore, AndroidPackage::isAllowNativeHeapPointerTagging, AndroidPackage::isAllowTaskReparenting, + AndroidPackage::isAllowUpdateOwnership, AndroidPackage::isBackupInForeground, AndroidPackage::isHardwareAccelerated, AndroidPackage::isCantSaveState, diff --git a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningRegistrationTest.java b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningRegistrationTest.java index 470f2bec684c..7b361d3e0832 100644 --- a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningRegistrationTest.java +++ b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningRegistrationTest.java @@ -34,7 +34,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.OutcomeReceiver; +import android.os.RemoteException; import android.security.rkp.IGetKeyCallback; +import android.security.rkp.IStoreUpgradedKeyCallback; import android.security.rkp.service.RegistrationProxy; import android.security.rkp.service.RemotelyProvisionedKey; @@ -72,6 +74,12 @@ public class RemoteProvisioningRegistrationTest { return answerVoid(answer); } + // answerVoid wrapper for mocking storeUpgradeKeyAsync. + static Answer<Void> answerStoreUpgradedKeyAsync( + VoidAnswer4<byte[], byte[], Executor, OutcomeReceiver<Void, Exception>> answer) { + return answerVoid(answer); + } + // matcher helper, making it easier to match the different key types private android.security.rkp.RemotelyProvisionedKey matches( RemotelyProvisionedKey expectedKey) { @@ -178,16 +186,63 @@ public class RemoteProvisioningRegistrationTest { @Test public void storeUpgradedKeySuccess() throws Exception { - // TODO(b/262748535) + doAnswer( + answerStoreUpgradedKeyAsync((oldBlob, newBlob, executor, receiver) -> + executor.execute(() -> receiver.onResult(null)))) + .when(mRegistrationProxy) + .storeUpgradedKeyAsync(any(), any(), any(), any()); + + IStoreUpgradedKeyCallback callback = mock(IStoreUpgradedKeyCallback.class); + mRegistration.storeUpgradedKeyAsync(new byte[0], new byte[0], callback); + verify(callback).onSuccess(); + verifyNoMoreInteractions(callback); } @Test public void storeUpgradedKeyFails() throws Exception { - // TODO(b/262748535) + final String errorString = "this is a failure"; + doAnswer( + answerStoreUpgradedKeyAsync((oldBlob, newBlob, executor, receiver) -> + executor.execute(() -> receiver.onError(new RemoteException(errorString))))) + .when(mRegistrationProxy) + .storeUpgradedKeyAsync(any(), any(), any(), any()); + + IStoreUpgradedKeyCallback callback = mock(IStoreUpgradedKeyCallback.class); + mRegistration.storeUpgradedKeyAsync(new byte[0], new byte[0], callback); + verify(callback).onError(errorString); + verifyNoMoreInteractions(callback); + } + + @Test + public void storeUpgradedKeyHandlesException() throws Exception { + final String errorString = "all aboard the failboat, toot toot"; + doThrow(new IllegalArgumentException(errorString)) + .when(mRegistrationProxy) + .storeUpgradedKeyAsync(any(), any(), any(), any()); + + IStoreUpgradedKeyCallback callback = mock(IStoreUpgradedKeyCallback.class); + mRegistration.storeUpgradedKeyAsync(new byte[0], new byte[0], callback); + verify(callback).onError(errorString); + verifyNoMoreInteractions(callback); } @Test - public void storeUpgradedCatchesExceptionFromProxy() throws Exception { - // TODO(b/262748535) + public void storeUpgradedKeyDuplicateCallback() throws Exception { + IStoreUpgradedKeyCallback callback = mock(IStoreUpgradedKeyCallback.class); + + doAnswer( + answerStoreUpgradedKeyAsync((oldBlob, newBlob, executor, receiver) -> { + assertThrows(IllegalArgumentException.class, + () -> mRegistration.storeUpgradedKeyAsync(new byte[0], new byte[0], + callback)); + executor.execute(() -> receiver.onResult(null)); + })) + .when(mRegistrationProxy) + .storeUpgradedKeyAsync(any(), any(), any(), any()); + + mRegistration.storeUpgradedKeyAsync(new byte[0], new byte[0], callback); + verify(callback).onSuccess(); + verifyNoMoreInteractions(callback); } + } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 77127c536d6d..92570aa8847e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -22,7 +22,6 @@ import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_INTERA import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_MANIFEST; import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_ORDERED; import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_PRIORITIZED; -import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_RESULT_TO; import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList; import static com.android.server.am.BroadcastProcessQueue.removeFromRunnableList; import static com.android.server.am.BroadcastQueueTest.CLASS_BLUE; @@ -115,8 +114,29 @@ public class BroadcastQueueModernImplTest { mConstants.DELAY_NORMAL_MILLIS = 10_000; mConstants.DELAY_CACHED_MILLIS = 120_000; + final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) { + public boolean shouldSkip(BroadcastRecord r, Object o) { + // Ignored + return false; + } + public String shouldSkipMessage(BroadcastRecord r, Object o) { + // Ignored + return null; + } + public boolean disallowBackgroundStart(BroadcastRecord r) { + // Ignored + return false; + } + }; + final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) { + public void addBroadcastToHistoryLocked(BroadcastRecord original) { + // Ignored + } + }; + + mImpl = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(), - mConstants, mConstants); + mConstants, mConstants, emptySkipPolicy, emptyHistory); doReturn(1L).when(mQueue1).getRunnableAt(); doReturn(2L).when(mQueue2).getRunnableAt(); @@ -200,7 +220,8 @@ public class BroadcastQueueModernImplTest { private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue, BroadcastRecord record, int recordIndex, long enqueueTime) { - queue.enqueueOrReplaceBroadcast(record, recordIndex, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(record, recordIndex, + null /* replacedBroadcastConsumer */, false); record.enqueueTime = enqueueTime; } @@ -330,7 +351,8 @@ public class BroadcastQueueModernImplTest { final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, List.of(makeMockRegisteredReceiver())); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, + null /* replacedBroadcastConsumer */, false); queue.setProcessCached(false); final long notCachedRunnableAt = queue.getRunnableAt(); @@ -352,12 +374,14 @@ public class BroadcastQueueModernImplTest { // enqueue a bg-priority broadcast then a fg-priority one final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone); - queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, + null /* replacedBroadcastConsumer */, false); final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, + null /* replacedBroadcastConsumer */, false); // verify that: // (a) the queue is immediately runnable by existence of a fg-priority broadcast @@ -388,7 +412,8 @@ public class BroadcastQueueModernImplTest { final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, null, List.of(withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10), withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 0)), true); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 1, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 1, + null /* replacedBroadcastConsumer */, false); assertFalse(queue.isRunnable()); assertEquals(BroadcastProcessQueue.REASON_BLOCKED, queue.getRunnableAtReason()); @@ -411,7 +436,8 @@ public class BroadcastQueueModernImplTest { final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, List.of(makeMockRegisteredReceiver())); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, + null /* replacedBroadcastConsumer */, false); mConstants.MAX_PENDING_BROADCASTS = 128; queue.invalidateRunnableAt(); @@ -437,11 +463,13 @@ public class BroadcastQueueModernImplTest { new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED), List.of(makeMockRegisteredReceiver())); - queue.enqueueOrReplaceBroadcast(lazyRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(lazyRecord, 0, + null /* replacedBroadcastConsumer */, false); assertThat(queue.getRunnableAt()).isGreaterThan(lazyRecord.enqueueTime); assertThat(queue.getRunnableAtReason()).isNotEqualTo(testRunnableAtReason); - queue.enqueueOrReplaceBroadcast(testRecord, 0, null /* replacedBroadcastConsumer */); + queue.enqueueOrReplaceBroadcast(testRecord, 0, + null /* replacedBroadcastConsumer */, false); assertThat(queue.getRunnableAt()).isAtMost(testRecord.enqueueTime); assertThat(queue.getRunnableAtReason()).isEqualTo(testRunnableAtReason); } @@ -459,13 +487,6 @@ public class BroadcastQueueModernImplTest { } @Test - public void testRunnableAt_Cached_ResultTo() { - final IIntentReceiver resultTo = mock(IIntentReceiver.class); - doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), null, - List.of(makeMockRegisteredReceiver()), resultTo, false), REASON_CONTAINS_RESULT_TO); - } - - @Test public void testRunnableAt_Cached_Foreground() { final Intent foregroundIntent = new Intent(); foregroundIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); @@ -511,25 +532,25 @@ public class BroadcastQueueModernImplTest { queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED) .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED) .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, - null /* replacedBroadcastConsumer */); + null /* replacedBroadcastConsumer */, false); queue.makeActiveNextPending(); assertEquals(Intent.ACTION_LOCKED_BOOT_COMPLETED, queue.getActive().intent.getAction()); 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 8d9dda0863e9..64be0f7c34d2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -357,6 +357,11 @@ public class BroadcastQueueTest { } receiversToSkip.add(o); } + public boolean disallowBackgroundStart(BroadcastRecord r) { + // Ignored + return false; + } + } private class TestInjector extends Injector { diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java index 79fbc877835c..7e1a42b67922 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java @@ -213,7 +213,7 @@ public final class JobConcurrencyManagerTest { mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( idle, preferredUidOnly, stoppable, assignmentInfo); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, idle.size()); assertEquals(0, preferredUidOnly.size()); assertEquals(0, stoppable.size()); assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); @@ -222,7 +222,7 @@ public final class JobConcurrencyManagerTest { @Test public void testPrepareForAssignmentDetermination_onlyPendingJobs() { - for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + for (int i = 0; i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT; ++i) { JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i); mPendingJobQueue.add(job); } @@ -235,7 +235,7 @@ public final class JobConcurrencyManagerTest { mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( idle, preferredUidOnly, stoppable, assignmentInfo); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, idle.size()); assertEquals(0, preferredUidOnly.size()); assertEquals(0, stoppable.size()); assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); @@ -244,7 +244,7 @@ public final class JobConcurrencyManagerTest { @Test public void testPrepareForAssignmentDetermination_onlyPreferredUidOnly() { - for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + for (int i = 0; i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT; ++i) { JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i); mJobConcurrencyManager.addRunningJobForTesting(job); } @@ -262,7 +262,7 @@ public final class JobConcurrencyManagerTest { idle, preferredUidOnly, stoppable, assignmentInfo); assertEquals(0, idle.size()); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, preferredUidOnly.size()); assertEquals(0, stoppable.size()); assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); assertEquals(0, assignmentInfo.numRunningImmediacyPrivileged); @@ -270,7 +270,7 @@ public final class JobConcurrencyManagerTest { @Test public void testPrepareForAssignmentDetermination_onlyStartedWithImmediacyPrivilege() { - for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + for (int i = 0; i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT; ++i) { JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i); job.startedWithImmediacyPrivilege = true; mJobConcurrencyManager.addRunningJobForTesting(job); @@ -289,19 +289,19 @@ public final class JobConcurrencyManagerTest { idle, preferredUidOnly, stoppable, assignmentInfo); assertEquals(0, idle.size()); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT / 2, preferredUidOnly.size()); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT / 2, stoppable.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 2, preferredUidOnly.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 2, stoppable.size()); assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, assignmentInfo.numRunningImmediacyPrivileged); } @Test public void testDetermineAssignments_allRegular() throws Exception { - setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, - new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT)); + setConcurrencyConfig(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, + new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT)); final ArraySet<JobStatus> jobs = new ArraySet<>(); - for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + for (int i = 0; i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT; ++i) { final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i; final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid); setPackageUid(sourcePkgName, uid); @@ -322,7 +322,7 @@ public final class JobConcurrencyManagerTest { .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, assignmentInfo); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, changed.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, changed.size()); for (int i = changed.size() - 1; i >= 0; --i) { jobs.remove(changed.valueAt(i).newJob); } @@ -332,16 +332,16 @@ public final class JobConcurrencyManagerTest { @Test public void testDetermineAssignments_allPreferredUidOnly_shortTimeLeft() throws Exception { mConfigBuilder.setBoolean(JobConcurrencyManager.KEY_ENABLE_MAX_WAIT_TIME_BYPASS, true); - setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, - new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT)); - for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT * 2; ++i) { + setConcurrencyConfig(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, + new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT)); + for (int i = 0; i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT * 2; ++i) { final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i; final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid); setPackageUid(sourcePkgName, uid); final JobStatus job = createJob(uid, sourcePkgName); spyOn(job); doReturn(i % 2 == 0).when(job).shouldTreatAsExpeditedJob(); - if (i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT) { + if (i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT) { mJobConcurrencyManager.addRunningJobForTesting(job); } else { mPendingJobQueue.add(job); @@ -366,30 +366,30 @@ public final class JobConcurrencyManagerTest { mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( idle, preferredUidOnly, stoppable, assignmentInfo); assertEquals(remainingTimeMs, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, preferredUidOnly.size()); mJobConcurrencyManager .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, assignmentInfo); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, preferredUidOnly.size()); assertEquals(0, changed.size()); } @Test public void testDetermineAssignments_allPreferredUidOnly_mediumTimeLeft() throws Exception { mConfigBuilder.setBoolean(JobConcurrencyManager.KEY_ENABLE_MAX_WAIT_TIME_BYPASS, true); - setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, - new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT)); + setConcurrencyConfig(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, + new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT)); final ArraySet<JobStatus> jobs = new ArraySet<>(); - for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT * 2; ++i) { + for (int i = 0; i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT * 2; ++i) { final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i; final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid); setPackageUid(sourcePkgName, uid); final JobStatus job = createJob(uid, sourcePkgName); spyOn(job); doReturn(i % 2 == 0).when(job).shouldTreatAsExpeditedJob(); - if (i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT) { + if (i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT) { mJobConcurrencyManager.addRunningJobForTesting(job); } else { mPendingJobQueue.add(job); @@ -417,17 +417,17 @@ public final class JobConcurrencyManagerTest { mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( idle, preferredUidOnly, stoppable, assignmentInfo); assertEquals(remainingTimeMs, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, preferredUidOnly.size()); mJobConcurrencyManager .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, assignmentInfo); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, preferredUidOnly.size()); for (int i = changed.size() - 1; i >= 0; --i) { jobs.remove(changed.valueAt(i).newJob); } - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT - 1, jobs.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT - 1, jobs.size()); assertEquals(1, changed.size()); JobStatus assignedJob = changed.valueAt(0).newJob; assertTrue(assignedJob.shouldTreatAsExpeditedJob()); @@ -436,17 +436,17 @@ public final class JobConcurrencyManagerTest { @Test public void testDetermineAssignments_allPreferredUidOnly_longTimeLeft() throws Exception { mConfigBuilder.setBoolean(JobConcurrencyManager.KEY_ENABLE_MAX_WAIT_TIME_BYPASS, true); - setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, - new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT)); + setConcurrencyConfig(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, + new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT)); final ArraySet<JobStatus> jobs = new ArraySet<>(); - for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT * 2; ++i) { + for (int i = 0; i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT * 2; ++i) { final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i; final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid); setPackageUid(sourcePkgName, uid); final JobStatus job = createJob(uid, sourcePkgName); spyOn(job); doReturn(i % 2 == 0).when(job).shouldTreatAsExpeditedJob(); - if (i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT) { + if (i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT) { mJobConcurrencyManager.addRunningJobForTesting(job); } else { mPendingJobQueue.add(job); @@ -473,13 +473,13 @@ public final class JobConcurrencyManagerTest { mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( idle, preferredUidOnly, stoppable, assignmentInfo); assertEquals(remainingTimeMs, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, preferredUidOnly.size()); mJobConcurrencyManager .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, assignmentInfo); - assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); + assertEquals(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT, preferredUidOnly.size()); // Depending on iteration order, we may create 1 or 2 contexts. final long numAssignedJobs = changed.size(); assertTrue(numAssignedJobs > 0); @@ -488,7 +488,7 @@ public final class JobConcurrencyManagerTest { jobs.remove(changed.valueAt(i).newJob); } assertEquals(numAssignedJobs, - JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT - jobs.size()); + JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT - jobs.size()); JobStatus firstAssignedJob = changed.valueAt(0).newJob; if (!firstAssignedJob.shouldTreatAsExpeditedJob()) { assertEquals(2, numAssignedJobs); @@ -538,14 +538,14 @@ public final class JobConcurrencyManagerTest { assertFalse(mJobConcurrencyManager.isPkgConcurrencyLimitedLocked(topJob)); // Pending jobs shouldn't affect TOP job's status. - for (int i = 1; i <= JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + for (int i = 1; i <= JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT; ++i) { final JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i); mPendingJobQueue.add(job); } assertFalse(mJobConcurrencyManager.isPkgConcurrencyLimitedLocked(topJob)); // Already running jobs shouldn't affect TOP job's status. - for (int i = 1; i <= JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + for (int i = 1; i <= JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT; ++i) { final JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE, i); mJobConcurrencyManager.addRunningJobForTesting(job); } @@ -605,9 +605,9 @@ public final class JobConcurrencyManagerTest { spyOn(testEj); doReturn(true).when(testEj).shouldTreatAsExpeditedJob(); - setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT); + setConcurrencyConfig(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT); - for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + for (int i = 0; i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT; ++i) { final JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i, i + 1); mPendingJobQueue.add(job); } @@ -887,12 +887,14 @@ public final class JobConcurrencyManagerTest { mConfigBuilder .setInt(WorkTypeConfig.KEY_PREFIX_MAX_TOTAL + identifier, total); for (TypeConfig config : typeConfigs) { - mConfigBuilder.setInt( - WorkTypeConfig.KEY_PREFIX_MAX + config.workTypeString + "_" + identifier, - config.max); - mConfigBuilder.setInt( - WorkTypeConfig.KEY_PREFIX_MIN + config.workTypeString + "_" + identifier, - config.min); + mConfigBuilder.setFloat( + WorkTypeConfig.KEY_PREFIX_MAX_RATIO + config.workTypeString + "_" + + identifier, + (float) config.max / total); + mConfigBuilder.setFloat( + WorkTypeConfig.KEY_PREFIX_MIN_RATIO + config.workTypeString + "_" + + identifier, + (float) config.min / total); } } updateDeviceConfig(); 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 9935a2f2a0ba..06ba5dd6069b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt @@ -63,6 +63,7 @@ import com.android.server.SystemServerInitThreadPool import com.android.server.compat.PlatformCompat import com.android.server.extendedtestutils.wheneverStatic import com.android.server.pm.dex.DexManager +import com.android.server.pm.dex.DynamicCodeLogger import com.android.server.pm.parsing.PackageParser2 import com.android.server.pm.parsing.pkg.PackageImpl import com.android.server.pm.parsing.pkg.ParsedPackage @@ -208,6 +209,7 @@ class MockSystem(withSession: (StaticMockitoSessionBuilder) -> Unit = {}) { whenever(snapshot()) { appsFilterSnapshot } } val dexManager: DexManager = mock() + val dynamicCodeLogger: DynamicCodeLogger = mock() val installer: Installer = mock() val displayMetrics: DisplayMetrics = mock() val domainVerificationManagerInternal: DomainVerificationManagerInternal = mock() @@ -285,6 +287,7 @@ class MockSystem(withSession: (StaticMockitoSessionBuilder) -> Unit = {}) { whenever(mocks.injector.crossProfileIntentFilterHelper) .thenReturn(mocks.crossProfileIntentFilterHelper) whenever(mocks.injector.dexManager).thenReturn(mocks.dexManager) + whenever(mocks.injector.dynamicCodeLogger).thenReturn(mocks.dynamicCodeLogger) whenever(mocks.injector.systemConfig).thenReturn(mocks.systemConfig) whenever(mocks.injector.apexManager).thenReturn(mocks.apexManager) whenever(mocks.injector.scanningCachingPackageParser).thenReturn(mocks.packageParser) diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java index cfd5279a0fa9..d2547a3ff336 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java @@ -723,8 +723,8 @@ public class StagingManagerTest { params.isStaged = true; InstallSource installSource = InstallSource.create("testInstallInitiator", - "testInstallOriginator", "testInstaller", 100, "testAttributionTag", - PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); + "testInstallOriginator", "testInstaller", 100, "testUpdateOwner", + "testAttributionTag", PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); PackageInstallerSession session = new PackageInstallerSession( /* callback */ null, diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java index 88709e164d79..b9ba780ff685 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java @@ -49,6 +49,7 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(USER_ID, DEFAULT_DISPLAY); expectUserIsVisible(USER_ID); expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY); @@ -80,6 +81,7 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(currentUserId, DEFAULT_DISPLAY); expectUserIsVisible(currentUserId); expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY); @@ -110,6 +112,7 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG_VISIBLE, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY); expectUserIsVisible(PROFILE_USER_ID); expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java index e4664d2c2c46..c59834bea6ca 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java @@ -165,12 +165,16 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(USER_ID); expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @Test public final void testStartVisibleBgUser_onDefaultDisplay() throws Exception { visibleBgUserCannotBeStartedOnDefaultDisplayTest(); + + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); } protected final void visibleBgUserCannotBeStartedOnDefaultDisplayTest() throws Exception { @@ -180,8 +184,8 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); - expectUserIsNotVisibleAtAll(PROFILE_USER_ID); - expectNoDisplayAssignedToUser(PROFILE_USER_ID); + expectUserIsNotVisibleAtAll(USER_ID); + expectNoDisplayAssignedToUser(USER_ID); listener.verify(); } @@ -194,8 +198,11 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { SECONDARY_DISPLAY_ID); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); - expectUserIsNotVisibleAtAll(PROFILE_USER_ID); - expectNoDisplayAssignedToUser(PROFILE_USER_ID); + expectUserIsNotVisibleAtAll(USER_ID); + expectNoDisplayAssignedToUser(USER_ID); + + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID); listener.verify(); } @@ -217,6 +224,9 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(USER_SYSTEM); expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID); + assertUserCannotBeAssignedExtraDisplay(USER_SYSTEM, SECONDARY_DISPLAY_ID); + assertUserCannotBeAssignedExtraDisplay(USER_SYSTEM, OTHER_SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -256,6 +266,8 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectUserAssignedToDisplay(DEFAULT_DISPLAY, OTHER_USER_ID); + assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -289,6 +301,8 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY); + assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -305,6 +319,10 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, + OTHER_SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -320,6 +338,10 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, + OTHER_SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -336,6 +358,9 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -351,6 +376,10 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, + OTHER_SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -481,6 +510,63 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { .that(actualResult).isEqualTo(expectedResult); } + protected void assertBgUserBecomesInvisibleOnStop(@UserIdInt int userId) { + Log.d(TAG, "Stopping user " + userId); + mMediator.unassignUserFromDisplayOnStop(userId); + expectUserIsNotVisibleAtAll(userId); + } + + /** + * Assigns and unassigns the user to / from an extra display, asserting the visibility state in + * between. + * + * <p>It assumes the user was not visible in the display beforehand. + */ + protected void assertUserCanBeAssignedExtraDisplay(@UserIdInt int userId, int displayId) { + assertUserCanBeAssignedExtraDisplay(userId, displayId, /* unassign= */ true); + } + + protected void assertUserCanBeAssignedExtraDisplay(@UserIdInt int userId, int displayId, + boolean unassign) { + + expectUserIsNotVisibleOnDisplay(userId, displayId); + + Log.d(TAG, "Calling assignUserToExtraDisplay(" + userId + ", " + displayId + ")"); + assertWithMessage("assignUserToExtraDisplay(%s, %s)", userId, displayId) + .that(mMediator.assignUserToExtraDisplay(userId, displayId)) + .isTrue(); + expectUserIsVisibleOnDisplay(userId, displayId); + + if (unassign) { + Log.d(TAG, "Calling unassignUserFromExtraDisplay(" + userId + ", " + displayId + ")"); + assertWithMessage("unassignUserFromExtraDisplay(%s, %s)", userId, displayId) + .that(mMediator.unassignUserFromExtraDisplay(userId, displayId)) + .isTrue(); + expectUserIsNotVisibleOnDisplay(userId, displayId); + } + } + + /** + * Asserts that a user (already visible or not) cannot be assigned to an extra display (and + * hence won't be visible on that display). + */ + protected void assertUserCannotBeAssignedExtraDisplay(@UserIdInt int userId, int displayId) { + expectWithMessage("assignUserToExtraDisplay(%s, %s)", userId, displayId) + .that(mMediator.assignUserToExtraDisplay(userId, displayId)) + .isFalse(); + expectUserIsNotVisibleOnDisplay(userId, displayId); + } + + /** + * Asserts that an invisible user cannot be assigned to an extra display. + */ + protected void assertInvisibleUserCannotBeAssignedExtraDisplay(@UserIdInt int userId, + int displayId) { + assertUserCannotBeAssignedExtraDisplay(userId, displayId); + expectNoDisplayAssignedToUser(userId); + expectInitialCurrentUserAssignedToDisplay(displayId); + } + protected void expectUserIsVisible(@UserIdInt int userId) { expectWithMessage("isUserVisible(%s)", userId) .that(mMediator.isUserVisible(userId)) @@ -534,6 +620,11 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { .that(mMediator.getDisplayAssignedToUser(userId)).isEqualTo(INVALID_DISPLAY); } + protected void expectUserCannotBeUnassignedFromDisplay(@UserIdInt int userId, int displayId) { + expectWithMessage("unassignUserFromExtraDisplay(%s, %s)", userId, displayId) + .that(mMediator.unassignUserFromExtraDisplay(userId, displayId)).isFalse(); + } + protected void expectUserAssignedToDisplay(int displayId, @UserIdInt int userId) { expectWithMessage("getUserAssignedToDisplay(%s)", displayId) .that(mMediator.getUserAssignedToDisplay(displayId)).isEqualTo(userId); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java index 66d7eb6e603e..627553bcfa18 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java @@ -52,6 +52,7 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(USER_ID, DEFAULT_DISPLAY); expectUserIsVisible(USER_ID); expectUserIsVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY); @@ -64,7 +65,9 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID); expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID); - expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY); + expectNoDisplayAssignedToUser(USER_NULL); + + assertUserCanBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); listener.verify(); } @@ -83,6 +86,7 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(currentUserId, DEFAULT_DISPLAY); expectUserIsVisible(currentUserId); expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY); @@ -98,6 +102,8 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectUserIsNotVisibleAtAll(previousCurrentUserId); expectNoDisplayAssignedToUser(previousCurrentUserId); + assertUserCanBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -113,6 +119,7 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG_VISIBLE, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY); expectUserIsVisible(PROFILE_USER_ID); expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY); @@ -123,6 +130,8 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY); expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID); + assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -134,6 +143,9 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, DEFAULT_DISPLAY); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -148,6 +160,9 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectUserIsNotVisibleAtAll(USER_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, DEFAULT_DISPLAY); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -159,6 +174,7 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE, SECONDARY_DISPLAY_ID); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(USER_ID, SECONDARY_DISPLAY_ID); expectUserIsVisible(USER_ID); expectUserIsVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID); @@ -169,7 +185,16 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectDisplayAssignedToUser(USER_ID, SECONDARY_DISPLAY_ID); expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID); - listener.verify(); + assertUserCanBeAssignedExtraDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID); + + // Assign again, without unassigning (to make sure it becomes invisible on stop) + AsyncUserVisibilityListener listener2 = addListenerForEvents(onInvisible(USER_ID)); + assertUserCanBeAssignedExtraDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID, + /* unassign= */ false); + + assertBgUserBecomesInvisibleOnStop(USER_ID); + + listener2.verify(); } @Test @@ -203,6 +228,8 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectNoDisplayAssignedToUser(USER_ID); expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, OTHER_USER_ID); + assertUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -226,7 +253,18 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectDisplayAssignedToUser(USER_ID, OTHER_SECONDARY_DISPLAY_ID); expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, USER_ID); + assertUserCanBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); + + // Assign again, without unassigning (to make sure it becomes invisible on stop) + AsyncUserVisibilityListener listener2 = addListenerForEvents(onInvisible(USER_ID)); + assertUserCanBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID, + /* unassign= */ false); + + assertBgUserBecomesInvisibleOnStop(USER_ID); + + listener2.verify(); } @Test @@ -244,12 +282,14 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, PARENT_USER_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } + // Conditions below are asserted on other tests, but they're explicitly checked in the 2 + // tests below (which call this method) as well private void currentUserVisibilityWhenNoDisplayIsAssignedTest(@UserIdInt int currentUserId) { - // Conditions below are asserted on other tests, but they're explicitly checked in the 2 - // tests below as well expectUserIsVisible(currentUserId); expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY); expectUserIsNotVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID); @@ -277,4 +317,13 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectUserIsNotVisibleAtAll(INITIAL_CURRENT_USER_ID); expectDisplayAssignedToUser(INITIAL_CURRENT_USER_ID, INVALID_DISPLAY); } + + @Test + public final void testAssignUserToExtraDisplay_invalidDisplays() throws Exception { + expectWithMessage("assignUserToExtraDisplay(%s, %s)", USER_ID, INVALID_DISPLAY) + .that(mMediator.assignUserToExtraDisplay(USER_ID, INVALID_DISPLAY)).isFalse(); + // DEFAULT_DISPLAY is always assigned to the current user + expectWithMessage("assignUserToExtraDisplay(%s, %s)", USER_ID, DEFAULT_DISPLAY) + .that(mMediator.assignUserToExtraDisplay(USER_ID, DEFAULT_DISPLAY)).isFalse(); + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/mockingservicestests/src/com/android/server/pm/dex/DexManagerTests.java index fb9cbb00255c..7dae23529fc6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/dex/DexManagerTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/dex/DexManagerTests.java @@ -85,6 +85,7 @@ public class DexManagerTests { private final Object mInstallLock = new Object(); + private DynamicCodeLogger mDynamicCodeLogger; private DexManager mDexManager; private TestData mFooUser0; @@ -158,8 +159,9 @@ public class DexManagerTests { .when(mockContext) .getSystemService(PowerManager.class); - mDexManager = new DexManager(mockContext, /*PackageDexOptimizer*/ null, - mInstaller, mInstallLock, mPM); + mDynamicCodeLogger = new DynamicCodeLogger(mInstaller); + mDexManager = new DexManager(mockContext, /*PackageDexOptimizer*/ null, mInstaller, + mInstallLock, mDynamicCodeLogger, mPM); // Foo and Bar are available to user0. // Only Bar is available to user1; @@ -452,6 +454,7 @@ public class DexManagerTests { notifyDexLoad(mBarUser1, mBarUser1.getSecondaryDexPaths(), mUser1); mDexManager.notifyPackageDataDestroyed(mBarUser0.getPackageName(), mUser0); + mDynamicCodeLogger.notifyPackageDataDestroyed(mBarUser0.getPackageName(), mUser0); // Data for user 1 should still be present PackageUseInfo pui = getPackageUseInfo(mBarUser1); @@ -474,6 +477,7 @@ public class DexManagerTests { notifyDexLoad(mBarUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser0); mDexManager.notifyPackageDataDestroyed(mFooUser0.getPackageName(), mUser0); + mDynamicCodeLogger.notifyPackageDataDestroyed(mFooUser0.getPackageName(), mUser0); // Foo should still be around since it's used by other apps but with no // secondary dex info. @@ -491,6 +495,7 @@ public class DexManagerTests { notifyDexLoad(mFooUser0, fooSecondaries, mUser0); mDexManager.notifyPackageDataDestroyed(mFooUser0.getPackageName(), mUser0); + mDynamicCodeLogger.notifyPackageDataDestroyed(mFooUser0.getPackageName(), mUser0); // Foo should not be around since all its secondary dex info were deleted // and it is not used by other apps. @@ -505,6 +510,8 @@ public class DexManagerTests { notifyDexLoad(mBarUser1, mBarUser1.getSecondaryDexPaths(), mUser1); mDexManager.notifyPackageDataDestroyed(mBarUser0.getPackageName(), UserHandle.USER_ALL); + mDynamicCodeLogger.notifyPackageDataDestroyed( + mBarUser0.getPackageName(), UserHandle.USER_ALL); // Bar should not be around since it was removed for all users. assertNoUseInfo(mBarUser0); @@ -906,8 +913,7 @@ public class DexManagerTests { } private PackageDynamicCode getPackageDynamicCodeInfo(TestData testData) { - return mDexManager.getDynamicCodeLogger() - .getPackageDynamicCodeInfo(testData.getPackageName()); + return mDynamicCodeLogger.getPackageDynamicCodeInfo(testData.getPackageName()); } private void assertNoUseInfo(TestData testData) { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index d056348318a5..448ffe538d00 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -55,6 +55,7 @@ import android.graphics.drawable.Icon; import android.hardware.display.DisplayManagerGlobal; import android.os.Bundle; import android.os.IBinder; +import android.os.LocaleList; import android.os.UserHandle; import android.provider.Settings; import android.testing.TestableContext; @@ -106,7 +107,8 @@ public class AccessibilityManagerServiceTest { private static final String INTENT_ACTION = "TESTACTION"; private static final String DESCRIPTION = "description"; private static final PendingIntent TEST_PENDING_INTENT = PendingIntent.getBroadcast( - ApplicationProvider.getApplicationContext(), 0, new Intent(INTENT_ACTION), + ApplicationProvider.getApplicationContext(), 0, new Intent(INTENT_ACTION) + .setPackage(ApplicationProvider.getApplicationContext().getPackageName()), PendingIntent.FLAG_MUTABLE_UNAUDITED); private static final RemoteAction TEST_ACTION = new RemoteAction( Icon.createWithContentUri("content://test"), @@ -487,7 +489,7 @@ public class AccessibilityManagerServiceTest { final int userid = 10; final int windowId = 100; final AccessibilityWindowAttributes attributes = new AccessibilityWindowAttributes( - new WindowManager.LayoutParams()); + new WindowManager.LayoutParams(), LocaleList.getEmptyLocaleList()); mA11yms.setAccessibilityWindowAttributes(displayId, windowId, userid, attributes); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java index 7b7e1e0c9aff..2dfabd0fbefb 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.when; import android.graphics.Region; import android.os.IBinder; +import android.os.LocaleList; import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; @@ -909,7 +910,7 @@ public class AccessibilityWindowManagerTest { final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.accessibilityTitle = "accessibility window title"; final AccessibilityWindowAttributes attributes = new AccessibilityWindowAttributes( - layoutParams); + layoutParams, new LocaleList()); mA11yWindowManager.setAccessibilityWindowAttributes(Display.DEFAULT_DISPLAY, windowId, USER_SYSTEM_ID, attributes); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java index bbcf77bf15d7..13d93cbbfde4 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java @@ -76,14 +76,18 @@ public class SystemActionPerformerTest { private static final String DESCRIPTION1 = "description1"; private static final String DESCRIPTION2 = "description2"; private static final PendingIntent TEST_PENDING_INTENT_1 = PendingIntent.getBroadcast( - InstrumentationRegistry.getTargetContext(), 0, new Intent(INTENT_ACTION1), PendingIntent.FLAG_MUTABLE_UNAUDITED); + InstrumentationRegistry.getTargetContext(), 0, new Intent(INTENT_ACTION1) + .setPackage(InstrumentationRegistry.getTargetContext().getPackageName()), + PendingIntent.FLAG_MUTABLE_UNAUDITED); private static final RemoteAction NEW_TEST_ACTION_1 = new RemoteAction( Icon.createWithContentUri("content://test"), LABEL_1, DESCRIPTION1, TEST_PENDING_INTENT_1); private static final PendingIntent TEST_PENDING_INTENT_2 = PendingIntent.getBroadcast( - InstrumentationRegistry.getTargetContext(), 0, new Intent(INTENT_ACTION2), PendingIntent.FLAG_MUTABLE_UNAUDITED); + InstrumentationRegistry.getTargetContext(), 0, new Intent(INTENT_ACTION2) + .setPackage(InstrumentationRegistry.getTargetContext().getPackageName()), + PendingIntent.FLAG_MUTABLE_UNAUDITED); private static final RemoteAction NEW_TEST_ACTION_2 = new RemoteAction( Icon.createWithContentUri("content://test"), LABEL_2, diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java index eac86715e4c7..2a80ce05d4e0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java @@ -26,13 +26,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -40,26 +40,33 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.accessibilityservice.MagnificationConfig; +import android.animation.ValueAnimator; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; +import android.hardware.display.DisplayManagerInternal; +import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.test.mock.MockContentResolver; import android.testing.DexmakerShareClassLoaderRule; import android.view.Display; +import android.view.DisplayInfo; import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.view.accessibility.MagnificationAnimationCallback; +import androidx.annotation.NonNull; +import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityManagerService; import com.android.server.accessibility.AccessibilityTraceManager; +import com.android.server.accessibility.test.MessageCapturingHandler; import com.android.server.wm.WindowManagerInternal; import org.junit.After; @@ -70,7 +77,6 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; @@ -82,6 +88,8 @@ public class MagnificationControllerTest { private static final int TEST_DISPLAY = Display.DEFAULT_DISPLAY; private static final int TEST_SERVICE_ID = 1; + private static final Region INITIAL_SCREEN_MAGNIFICATION_REGION = + new Region(0, 0, 500, 600); private static final Rect TEST_RECT = new Rect(0, 50, 100, 51); private static final float MAGNIFIED_CENTER_X = 100; private static final float MAGNIFIED_CENTER_Y = 200; @@ -101,9 +109,20 @@ public class MagnificationControllerTest { @Mock private Context mContext; @Mock - PackageManager mPackageManager; + private PackageManager mPackageManager; + + @Mock + private FullScreenMagnificationController.ControllerContext mControllerCtx; + @Mock + private ValueAnimator mValueAnimator; @Mock + private MessageCapturingHandler mMessageCapturingHandler; + private FullScreenMagnificationController mScreenMagnificationController; + private final FullScreenMagnificationCtrInfoChangedCallbackDelegate + mScreenMagnificationInfoChangedCallbackDelegate = + new FullScreenMagnificationCtrInfoChangedCallbackDelegate(); + private MagnificationScaleProvider mScaleProvider; @Captor private ArgumentCaptor<MagnificationAnimationCallback> mCallbackArgumentCaptor; @@ -112,13 +131,17 @@ public class MagnificationControllerTest { private WindowMagnificationManager mWindowMagnificationManager; private MockContentResolver mMockResolver; private MagnificationController mMagnificationController; - private final WindowMagnificationMgrCallbackDelegate mCallbackDelegate = + private final WindowMagnificationMgrCallbackDelegate + mWindowMagnificationCallbackDelegate = new WindowMagnificationMgrCallbackDelegate(); @Mock - private WindowManagerInternal mMockWindowManagerInternal; + private WindowManagerInternal mWindowManagerInternal; + @Mock + private WindowManagerInternal.AccessibilityControllerInternal mA11yController; + @Mock - private WindowManagerInternal.AccessibilityControllerInternal mMockA11yController; + private DisplayManagerInternal mDisplayManagerInternal; // To mock package-private class @Rule @@ -132,31 +155,60 @@ public class MagnificationControllerTest { final Object globalLock = new Object(); LocalServices.removeServiceForTest(WindowManagerInternal.class); - LocalServices.addService(WindowManagerInternal.class, mMockWindowManagerInternal); - when(mMockWindowManagerInternal.getAccessibilityController()).thenReturn( - mMockA11yController); + LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal); + when(mWindowManagerInternal.getAccessibilityController()).thenReturn( + mA11yController); + when(mWindowManagerInternal.setMagnificationCallbacks(eq(TEST_DISPLAY), any())) + .thenReturn(true); + doAnswer((Answer<Void>) invocationOnMock -> { + Object[] args = invocationOnMock.getArguments(); + Region regionArg = (Region) args[1]; + regionArg.set(INITIAL_SCREEN_MAGNIFICATION_REGION); + return null; + }).when(mWindowManagerInternal).getMagnificationRegion(anyInt(), any(Region.class)); mMockResolver = new MockContentResolver(); mMockResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + Looper looper = InstrumentationRegistry.getContext().getMainLooper(); + // Pretending ID of the Thread associated with looper as main thread ID in controller + when(mContext.getMainLooper()).thenReturn(looper); when(mContext.getContentResolver()).thenReturn(mMockResolver); when(mContext.getPackageManager()).thenReturn(mPackageManager); Settings.Secure.putFloatForUser(mMockResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, DEFAULT_SCALE, CURRENT_USER_ID); mScaleProvider = spy(new MagnificationScaleProvider(mContext)); - mWindowMagnificationManager = Mockito.spy( - new WindowMagnificationManager(mContext, globalLock, - mCallbackDelegate, mTraceManager, mScaleProvider)); + + when(mControllerCtx.getContext()).thenReturn(mContext); + when(mControllerCtx.getTraceManager()).thenReturn(mTraceManager); + when(mControllerCtx.getWindowManager()).thenReturn(mWindowManagerInternal); + when(mControllerCtx.getHandler()).thenReturn(mMessageCapturingHandler); + when(mControllerCtx.getAnimationDuration()).thenReturn(1000L); + when(mControllerCtx.newValueAnimator()).thenReturn(mValueAnimator); + + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalDensityDpi = 300; + doReturn(displayInfo).when(mDisplayManagerInternal).getDisplayInfo(anyInt()); + LocalServices.removeServiceForTest(DisplayManagerInternal.class); + LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternal); + + mScreenMagnificationController = spy(new FullScreenMagnificationController( + mControllerCtx, new Object(), + mScreenMagnificationInfoChangedCallbackDelegate, mScaleProvider)); + mScreenMagnificationController.register(TEST_DISPLAY); + + mWindowMagnificationManager = spy(new WindowMagnificationManager(mContext, globalLock, + mWindowMagnificationCallbackDelegate, mTraceManager, mScaleProvider)); mMockConnection = new MockWindowMagnificationConnection(true); mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); + mMagnificationController = new MagnificationController(mService, globalLock, mContext, mScreenMagnificationController, mWindowMagnificationManager, mScaleProvider); - new FullScreenMagnificationControllerStubber(mScreenMagnificationController, - mMagnificationController); - mMagnificationController.setMagnificationCapabilities( Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL); - mCallbackDelegate.setDelegate(mMagnificationController); + + mScreenMagnificationInfoChangedCallbackDelegate.setDelegate(mMagnificationController); + mWindowMagnificationCallbackDelegate.setDelegate(mMagnificationController); } @After @@ -213,8 +265,8 @@ public class MagnificationControllerTest { verify(mTransitionCallBack).onResult(TEST_DISPLAY, false); final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - // The first time is for notifying full-screen enabled and the second time is for notifying - // the target mode transitions failed. + // The first time is for notifying full-screen enabled. + // The second time is for notifying the target mode transitions failed. verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), any(Region.class), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); @@ -252,9 +304,9 @@ public class MagnificationControllerTest { MODE_WINDOW, mTransitionCallBack); - // The first time is triggered when window mode is activated, the second time is triggered - // when activating the window mode again. The third time is triggered when the transition is - // completed. + // The first time is triggered when window mode is activated. + // The second time is triggered when activating the window mode again. + // The third time is triggered when the transition is completed. verify(mWindowMagnificationManager, times(3)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_WINDOW)); } @@ -279,8 +331,7 @@ public class MagnificationControllerTest { @Test public void transitionToFullScreen_centerNotInTheBounds_magnifyBoundsCenter() throws RemoteException { - final Rect magnificationBounds = - FullScreenMagnificationControllerStubber.MAGNIFICATION_REGION.getBounds(); + final Rect magnificationBounds = INITIAL_SCREEN_MAGNIFICATION_REGION.getBounds(); final PointF magnifiedCenter = new PointF(magnificationBounds.right + 100, magnificationBounds.bottom + 100); setMagnificationEnabled(MODE_WINDOW, magnifiedCenter.x, magnifiedCenter.y); @@ -436,17 +487,19 @@ public class MagnificationControllerTest { public void magnifyThroughExternalRequest_showMagnificationButton() { mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY, DEFAULT_SCALE, MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y, false, TEST_SERVICE_ID); - mMagnificationController.onRequestMagnificationSpec(TEST_DISPLAY, TEST_SERVICE_ID); - verify(mWindowMagnificationManager).showMagnificationButton(eq(TEST_DISPLAY), + // The first time is trigger when fullscreen mode is activated. + // The second time is triggered when magnification spec is changed. + verify(mWindowMagnificationManager, times(2)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_FULLSCREEN)); } @Test - public void setScaleOneThroughExternalRequest_removeMagnificationButton() { + public void setScaleOneThroughExternalRequest_fullScreenEnabled_removeMagnificationButton() + throws RemoteException { + setMagnificationEnabled(MODE_FULLSCREEN); mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY, 1.0f, MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y, false, TEST_SERVICE_ID); - mMagnificationController.onRequestMagnificationSpec(TEST_DISPLAY, TEST_SERVICE_ID); verify(mWindowMagnificationManager).removeMagnificationButton(eq(TEST_DISPLAY)); } @@ -490,12 +543,12 @@ public class MagnificationControllerTest { config.getScale(), config.getCenterX(), config.getCenterY(), true, TEST_SERVICE_ID); - // The first time is triggered when setting magnification enabled. And the second time is - // triggered when calling setScaleAndCenter. + // The notify method is triggered when setting magnification enabled. + // The setScaleAndCenter call should not trigger notify method due to same scale and center. final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), - eq(FullScreenMagnificationControllerStubber.MAGNIFICATION_REGION), + verify(mService).notifyMagnificationChanged(eq(TEST_DISPLAY), + eq(INITIAL_SCREEN_MAGNIFICATION_REGION), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); assertEquals(config.getCenterX(), actualConfig.getCenterX(), 0); @@ -514,8 +567,8 @@ public class MagnificationControllerTest { final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - // The first time is for notifying window enabled and the second time is for notifying - // the target mode transitions. + // The first time is for notifying window enabled. + // The second time is for notifying the target mode transitions. verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), any(Region.class), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); @@ -534,8 +587,8 @@ public class MagnificationControllerTest { final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - // The first time is for notifying window enabled and the second time is for notifying - // the target mode transitions. + // The first time is for notifying window enabled. + // The second time is for notifying the target mode transitions. verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), any(Region.class), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); @@ -558,8 +611,8 @@ public class MagnificationControllerTest { final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - // The first time is for notifying full-screen enabled and the second time is for notifying - // the target mode transitions. + // The first time is for notifying full-screen enabled. + // The second time is for notifying the target mode transitions. verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), any(Region.class), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); @@ -578,8 +631,8 @@ public class MagnificationControllerTest { final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - // The first time is for notifying full-screen enabled and the second time is for notifying - // the target mode transitions. + // The first time is for notifying full-screen enabled. + // The second time is for notifying the target mode transitions. verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), any(Region.class), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); @@ -597,6 +650,7 @@ public class MagnificationControllerTest { mMagnificationController.onAccessibilityActionPerformed(TEST_DISPLAY); // The first time is triggered when window mode is activated. + // The second time is triggered when accessibility action performed. verify(mWindowMagnificationManager, times(2)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_WINDOW)); } @@ -611,6 +665,7 @@ public class MagnificationControllerTest { mMagnificationController.onAccessibilityActionPerformed(TEST_DISPLAY); // The first time is triggered when window mode is activated. + // The second time is triggered when accessibility action performed. verify(mWindowMagnificationManager, times(2)).removeMagnificationButton(eq(TEST_DISPLAY)); } @@ -767,7 +822,10 @@ public class MagnificationControllerTest { mMagnificationController.onTouchInteractionStart(TEST_DISPLAY, MODE_FULLSCREEN); - verify(mWindowMagnificationManager).showMagnificationButton(eq(TEST_DISPLAY), + // The first time is triggered when fullscreen mode is activated. + // The second time is triggered when magnification spec is changed. + // The third time is triggered when user interaction changed. + verify(mWindowMagnificationManager, times(3)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_FULLSCREEN)); } @@ -778,7 +836,10 @@ public class MagnificationControllerTest { mMagnificationController.onTouchInteractionEnd(TEST_DISPLAY, MODE_FULLSCREEN); - verify(mWindowMagnificationManager).showMagnificationButton(eq(TEST_DISPLAY), + // The first time is triggered when fullscreen mode is activated. + // The second time is triggered when magnification spec is changed. + // The third time is triggered when user interaction changed. + verify(mWindowMagnificationManager, times(3)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_FULLSCREEN)); } @@ -790,6 +851,7 @@ public class MagnificationControllerTest { mMagnificationController.onTouchInteractionStart(TEST_DISPLAY, MODE_WINDOW); // The first time is triggered when the window mode is activated. + // The second time is triggered when user interaction changed. verify(mWindowMagnificationManager, times(2)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_WINDOW)); } @@ -802,6 +864,7 @@ public class MagnificationControllerTest { mMagnificationController.onTouchInteractionEnd(TEST_DISPLAY, MODE_WINDOW); // The first time is triggered when the window mode is activated. + // The second time is triggered when user interaction changed. verify(mWindowMagnificationManager, times(2)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_WINDOW)); } @@ -849,7 +912,10 @@ public class MagnificationControllerTest { mMagnificationController.onFullScreenMagnificationActivationState(TEST_DISPLAY, true); - verify(mWindowMagnificationManager).showMagnificationButton(eq(TEST_DISPLAY), + // The first time is triggered when fullscreen mode is activated. + // The second time is triggered when magnification spec is changed. + // The third time is triggered when fullscreen mode activation state is updated. + verify(mWindowMagnificationManager, times(3)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_FULLSCREEN)); } @@ -867,11 +933,7 @@ public class MagnificationControllerTest { public void onFullScreenDeactivated_fullScreenEnabled_removeMagnificationButton() throws RemoteException { setMagnificationEnabled(MODE_FULLSCREEN); - mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY, - /* scale= */ 1, MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y, - true, TEST_SERVICE_ID); - - mMagnificationController.onFullScreenMagnificationActivationState(TEST_DISPLAY, false); + mScreenMagnificationController.reset(TEST_DISPLAY, /* animate= */ true); verify(mWindowMagnificationManager).removeMagnificationButton(eq(TEST_DISPLAY)); } @@ -885,7 +947,10 @@ public class MagnificationControllerTest { MODE_FULLSCREEN, mTransitionCallBack); mMockConnection.invokeCallbacks(); - verify(mWindowMagnificationManager).showMagnificationButton(eq(TEST_DISPLAY), + // The first time is triggered when fullscreen mode is activated. + // The second time is triggered when magnification spec is changed. + // The third time is triggered when the disable-magnification callback is triggered. + verify(mWindowMagnificationManager, times(3)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_FULLSCREEN)); } @@ -902,8 +967,8 @@ public class MagnificationControllerTest { mCallbackArgumentCaptor.getValue().onResult(true); mMockConnection.invokeCallbacks(); - // The first time is triggered when window mode is activated, the second time is triggered - // when the disable-magnification callback is triggered. + // The first time is triggered when window mode is activated. + // The second time is triggered when the disable-magnification callback is triggered. verify(mWindowMagnificationManager, times(2)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_WINDOW)); } @@ -1023,8 +1088,7 @@ public class MagnificationControllerTest { .UiChangesForAccessibilityCallbacks> captor = ArgumentCaptor.forClass( WindowManagerInternal.AccessibilityControllerInternal .UiChangesForAccessibilityCallbacks.class); - verify(mMockWindowManagerInternal.getAccessibilityController()) - .setUiChangesForAccessibilityCallbacks(captor.capture()); + verify(mA11yController).setUiChangesForAccessibilityCallbacks(captor.capture()); return captor.getValue(); } @@ -1072,95 +1136,42 @@ public class MagnificationControllerTest { } } - /** - * Stubs public methods to simulate the real behaviours. - */ - private static class FullScreenMagnificationControllerStubber { - private static final Region MAGNIFICATION_REGION = new Region(0, 0, 500, 600); - private final FullScreenMagnificationController mScreenMagnificationController; - private final FullScreenMagnificationController.MagnificationInfoChangedCallback - mMagnificationChangedCallback; - private boolean mIsMagnifying = false; - private float mScale = 1.0f; - private float mCenterX = MAGNIFICATION_REGION.getBounds().exactCenterX(); - private float mCenterY = MAGNIFICATION_REGION.getBounds().exactCenterY(); - private int mServiceId = -1; - - FullScreenMagnificationControllerStubber( - FullScreenMagnificationController screenMagnificationController, + private static class FullScreenMagnificationCtrInfoChangedCallbackDelegate implements + FullScreenMagnificationController.MagnificationInfoChangedCallback { + private FullScreenMagnificationController.MagnificationInfoChangedCallback mCallback; + + public void setDelegate( FullScreenMagnificationController.MagnificationInfoChangedCallback callback) { - mScreenMagnificationController = screenMagnificationController; - mMagnificationChangedCallback = callback; - stubMethods(); + mCallback = callback; + } + + @Override + public void onRequestMagnificationSpec(int displayId, int serviceId) { + if (mCallback != null) { + mCallback.onRequestMagnificationSpec(displayId, serviceId); + } + } + + @Override + public void onFullScreenMagnificationActivationState(int displayId, boolean activated) { + if (mCallback != null) { + mCallback.onFullScreenMagnificationActivationState(displayId, activated); + } } - private void stubMethods() { - doAnswer(invocation -> mIsMagnifying).when(mScreenMagnificationController).isMagnifying( - TEST_DISPLAY); - doAnswer(invocation -> mIsMagnifying).when( - mScreenMagnificationController).isForceShowMagnifiableBounds(TEST_DISPLAY); - doAnswer(invocation -> mScale).when(mScreenMagnificationController).getPersistedScale( - TEST_DISPLAY); - doAnswer(invocation -> mScale).when(mScreenMagnificationController).getScale( - TEST_DISPLAY); - doAnswer(invocation -> mCenterX).when(mScreenMagnificationController).getCenterX( - TEST_DISPLAY); - doAnswer(invocation -> mCenterY).when(mScreenMagnificationController).getCenterY( - TEST_DISPLAY); - doAnswer(invocation -> mServiceId).when( - mScreenMagnificationController).getIdOfLastServiceToMagnify(TEST_DISPLAY); - - doAnswer(invocation -> { - final Region outRegion = invocation.getArgument(1); - outRegion.set(MAGNIFICATION_REGION); - return null; - }).when(mScreenMagnificationController).getMagnificationRegion(anyInt(), - any(Region.class)); - - Answer setScaleAndCenterStubAnswer = invocation -> { - final float scale = invocation.getArgument(1); - mScale = Float.isNaN(scale) ? mScale : scale; - mIsMagnifying = mScale > 1.0f; - if (mIsMagnifying) { - mCenterX = invocation.getArgument(2); - mCenterY = invocation.getArgument(3); - mServiceId = invocation.getArgument(5); - } else { - reset(); - } - - final MagnificationConfig config = new MagnificationConfig.Builder().setMode( - MODE_FULLSCREEN).setScale(mScale).setCenterX(mCenterX).setCenterY( - mCenterY).build(); - mMagnificationChangedCallback.onFullScreenMagnificationChanged(TEST_DISPLAY, - FullScreenMagnificationControllerStubber.MAGNIFICATION_REGION, - config); - return true; - }; - doAnswer(setScaleAndCenterStubAnswer).when( - mScreenMagnificationController).setScaleAndCenter(eq(TEST_DISPLAY), - anyFloat(), anyFloat(), anyFloat(), any(), anyInt()); - - doAnswer(setScaleAndCenterStubAnswer).when( - mScreenMagnificationController).setScaleAndCenter(eq(TEST_DISPLAY), - anyFloat(), anyFloat(), anyFloat(), anyBoolean(), anyInt()); - - Answer resetStubAnswer = invocation -> { - reset(); - return true; - }; - doAnswer(resetStubAnswer).when(mScreenMagnificationController).reset(eq(TEST_DISPLAY), - any(MagnificationAnimationCallback.class)); - doAnswer(resetStubAnswer).when(mScreenMagnificationController).reset(eq(TEST_DISPLAY), - anyBoolean()); + @Override + public void onImeWindowVisibilityChanged(int displayId, boolean shown) { + if (mCallback != null) { + mCallback.onImeWindowVisibilityChanged(displayId, shown); + } } - private void reset() { - mScale = 1.0f; - mIsMagnifying = false; - mServiceId = -1; - mCenterX = MAGNIFICATION_REGION.getBounds().exactCenterX(); - mCenterY = MAGNIFICATION_REGION.getBounds().exactCenterY(); + @Override + public void onFullScreenMagnificationChanged(int displayId, @NonNull Region region, + @NonNull MagnificationConfig config) { + if (mCallback != null) { + mCallback.onFullScreenMagnificationChanged(displayId, region, config); + } } } } diff --git a/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java b/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java index 574aaf00e460..5f55f0914d14 100644 --- a/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java +++ b/services/tests/servicestests/src/com/android/server/am/CoreSettingsObserverTest.java @@ -97,6 +97,8 @@ public class CoreSettingsObserverTest { mContentResolver = new MockContentResolver(mContext); mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); when(mContext.getContentResolver()).thenReturn(mContentResolver); + when(mContext.getCacheDir()).thenReturn(originalContext.getCacheDir()); + when(mContext.getAttributionSource()).thenReturn(originalContext.getAttributionSource()); when(mContext.getResources()).thenReturn(mResources); // To prevent NullPointerException at the constructor of ActivityManagerConstants. when(mResources.getStringArray(anyInt())).thenReturn(new String[0]); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 759b0497044f..eb99e30b58ec 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -369,6 +369,21 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void isDeviceIdValid_defaultDeviceId_returnsFalse() { + assertThat(mVdm.isValidVirtualDeviceId(DEVICE_ID_DEFAULT)).isFalse(); + } + + @Test + public void isDeviceIdValid_validVirtualDeviceId_returnsTrue() { + assertThat(mVdm.isValidVirtualDeviceId(mDeviceImpl.getDeviceId())).isTrue(); + } + + @Test + public void isDeviceIdValid_nonExistentDeviceId_returnsFalse() { + assertThat(mVdm.isValidVirtualDeviceId(mDeviceImpl.getDeviceId() + 1)).isFalse(); + } + + @Test public void getDevicePolicy_invalidDeviceId_returnsDefault() { assertThat(mVdm.getDevicePolicy(DEVICE_ID_INVALID, POLICY_TYPE_SENSORS)) .isEqualTo(DEVICE_POLICY_DEFAULT); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java index 2a6a97991135..4163f33e94e9 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java @@ -219,8 +219,10 @@ public class MockSystemServices { // Add the system user with a fake profile group already set up (this can happen in the real // world if a managed profile is added and then removed). - systemUserDataDir = addUser(UserHandle.USER_SYSTEM, UserInfo.FLAG_PRIMARY, + systemUserDataDir = addUser(UserHandle.USER_SYSTEM, + UserInfo.FLAG_PRIMARY | UserInfo.FLAG_MAIN, UserManager.USER_TYPE_FULL_SYSTEM, UserHandle.USER_SYSTEM); + when(userManager.getMainUser()).thenReturn(UserHandle.SYSTEM); // System user is always running. setUserRunning(UserHandle.USER_SYSTEM, true); diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java index f676a3f84c0f..2d252cbbbd9c 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -39,6 +39,8 @@ import static org.mockito.Mockito.when; import android.app.PropertyInvalidatedCache; import android.companion.virtual.IVirtualDevice; +import android.companion.virtual.IVirtualDeviceManager; +import android.companion.virtual.VirtualDeviceManager; import android.compat.testing.PlatformCompatChangeRule; import android.content.Context; import android.content.ContextWrapper; @@ -173,6 +175,7 @@ public class DisplayManagerServiceTest { private final DisplayManagerService.Injector mBasicInjector = new BasicInjector(); + @Mock IVirtualDeviceManager mIVirtualDeviceManager; @Mock InputManagerInternal mMockInputManagerInternal; @Mock VirtualDeviceManagerInternal mMockVirtualDeviceManagerInternal; @Mock IVirtualDisplayCallback.Stub mMockAppToken; @@ -202,6 +205,8 @@ public class DisplayManagerServiceTest { mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); + VirtualDeviceManager vdm = new VirtualDeviceManager(mIVirtualDeviceManager, mContext); + when(mContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm); // Disable binder caches in this process. PropertyInvalidatedCache.disableForTestMode(); setUpDisplay(); @@ -727,10 +732,8 @@ public class DisplayManagerServiceTest { when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); IVirtualDevice virtualDevice = mock(IVirtualDevice.class); - when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) - .thenReturn(true); when(virtualDevice.getDeviceId()).thenReturn(1); - + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); // Create a first virtual display. A display group should be created for this display on the // virtual device. final VirtualDisplayConfig.Builder builder1 = @@ -780,9 +783,8 @@ public class DisplayManagerServiceTest { when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); IVirtualDevice virtualDevice = mock(IVirtualDevice.class); - when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) - .thenReturn(true); when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); // Create a first virtual display. A display group should be created for this display on the // virtual device. @@ -806,6 +808,8 @@ public class DisplayManagerServiceTest { .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) .setUniqueId("uniqueId --- own display group"); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + int displayId2 = localService.createVirtualDisplay( builder2.build(), @@ -832,9 +836,8 @@ public class DisplayManagerServiceTest { when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); IVirtualDevice virtualDevice = mock(IVirtualDevice.class); - when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) - .thenReturn(true); when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); // Allow an ALWAYS_UNLOCKED display to be created. when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)) @@ -1062,7 +1065,7 @@ public class DisplayManagerServiceTest { * a virtual device, even if ADD_TRUSTED_DISPLAY is not granted. */ @Test - public void testOwnDisplayGroup_allowCreationWithVirtualDevice() { + public void testOwnDisplayGroup_allowCreationWithVirtualDevice() throws Exception { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerInternal localService = displayManager.new LocalService(); @@ -1081,8 +1084,8 @@ public class DisplayManagerServiceTest { builder.setUniqueId("uniqueId --- OWN_DISPLAY_GROUP"); IVirtualDevice virtualDevice = mock(IVirtualDevice.class); - when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) - .thenReturn(true); + when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); int displayId = localService.createVirtualDisplay(builder.build(), mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */, diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt index ecd9d893330a..3ce747f145dc 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -16,7 +16,9 @@ package com.android.server.input +import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager import android.content.Context import android.content.ContextWrapper import android.hardware.BatteryState.STATUS_CHARGING @@ -246,6 +248,11 @@ class BatteryControllerTests { notifyDeviceChanged(deviceId, hasBattery, supportsUsi) } + private fun createBluetoothDevice(address: String): BluetoothDevice { + return context.getSystemService(BluetoothManager::class.java)!! + .adapter.getRemoteDevice(address) + } + @After fun tearDown() { InputManager.clearInstance() @@ -656,29 +663,31 @@ class BatteryControllerTests { addInputDevice(SECOND_BT_DEVICE_ID) testLooper.dispatchNext() - // Ensure that a BT battery listener is not added when there are no monitored BT devices. - verify(bluetoothBatteryManager, never()).addListener(any()) + // Listen to a non-Bluetooth device and ensure that the BT battery listener is not added + // when there are no monitored BT devices. + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager, never()).addBatteryListener(any()) val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) - val listener = createMockListener() // The BT battery listener is added when the first BT input device is monitored. batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) // The BT listener is only added once for all BT devices. batteryController.registerBatteryListener(SECOND_BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager, times(1)).addListener(any()) + verify(bluetoothBatteryManager, times(1)).addBatteryListener(any()) // The BT listener is only removed when there are no monitored BT devices. batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager, never()).removeListener(any()) + verify(bluetoothBatteryManager, never()).removeBatteryListener(any()) `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) .thenReturn(null) notifyDeviceChanged(SECOND_BT_DEVICE_ID) testLooper.dispatchNext() - verify(bluetoothBatteryManager).removeListener(bluetoothListener.value) + verify(bluetoothBatteryManager).removeBatteryListener(bluetoothListener.value) } @Test @@ -690,15 +699,14 @@ class BatteryControllerTests { val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) val listener = createMockListener() batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) // When the state has not changed, the listener is not notified again. - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 21) listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(25) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 25) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f) } @@ -717,7 +725,7 @@ class BatteryControllerTests { // When the device is first monitored and both native and BT battery is available, // the latter is used. batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) verify(uEventManager).addListener(uEventListener.capture(), any()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID), @@ -744,25 +752,144 @@ class BatteryControllerTests { val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) verify(uEventManager).addListener(uEventListener.capture(), any()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) // Fall back to the native state when BT is off. - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))) - .thenReturn(BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", + BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.98f) - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(22) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 22) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f) // Fall back to the native state when BT battery is unknown. - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))) - .thenReturn(BluetoothDevice.BATTERY_LEVEL_UNKNOWN) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", + BluetoothDevice.BATTERY_LEVEL_UNKNOWN) listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.98f) } + + @Test + fun testRegisterBluetoothMetadataListenerForMonitoredBluetoothDevices() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn("11:22:33:44:55:66") + addInputDevice(BT_DEVICE_ID) + testLooper.dispatchNext() + addInputDevice(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + + // Listen to a non-Bluetooth device and ensure that the metadata listener is not added when + // there are no monitored BT devices. + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager, never()).addMetadataListener(any(), any()) + + val metadataListener1 = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val metadataListener2 = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + + // The metadata listener is added when the first BT input device is monitored. + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener1.capture()) + + // There is one metadata listener added for each BT device. + batteryController.registerBatteryListener(SECOND_BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("11:22:33:44:55:66"), metadataListener2.capture()) + + // The metadata listener is removed when the device is no longer monitored. + batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .removeMetadataListener("AA:BB:CC:DD:EE:FF", metadataListener1.value) + + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn(null) + notifyDeviceChanged(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + verify(bluetoothBatteryManager) + .removeMetadataListener("11:22:33:44:55:66", metadataListener2.value) + } + + @Test + fun testNotifiesBluetoothMetadataBatteryChanges() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY)) + .thenReturn("21".toByteArray()) + addInputDevice(BT_DEVICE_ID) + val metadataListener = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val listener = createMockListener() + val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f, status = STATUS_UNKNOWN) + + // When the state has not changed, the listener is not notified again. + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "21".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "25".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_UNKNOWN) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "true".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_CHARGING) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "false".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_DISCHARGING) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, null) + listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.25f, + status = STATUS_UNKNOWN) + } + + @Test + fun testBluetoothMetadataBatteryIsPrioritized() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21) + `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY)) + .thenReturn("22".toByteArray()) + addInputDevice(BT_DEVICE_ID) + val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) + val metadataListener = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val listener = createMockListener() + val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f) + + // A change in the Bluetooth battery level has no effect while there is a valid battery + // level obtained through the metadata. + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 23) + listener.verifyNotified(BT_DEVICE_ID, mode = never(), capacity = 0.23f) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "24".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.24f) + + // When the battery level from the metadata is no longer valid, we fall back to using the + // Bluetooth battery level. + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, null) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.23f) + } } diff --git a/services/tests/servicestests/src/com/android/server/job/BiasSchedulingTest.java b/services/tests/servicestests/src/com/android/server/job/BiasSchedulingTest.java index e886e7dc1d60..56d01b0e3a2a 100644 --- a/services/tests/servicestests/src/com/android/server/job/BiasSchedulingTest.java +++ b/services/tests/servicestests/src/com/android/server/job/BiasSchedulingTest.java @@ -57,14 +57,14 @@ public class BiasSchedulingTest extends AndroidTestCase { } public void testLowerBiasJobPreempted() throws Exception { - for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + for (int i = 0; i < JobConcurrencyManager.MAX_CONCURRENCY_LIMIT; ++i) { JobInfo job = new JobInfo.Builder(100 + i, sJobServiceComponent) .setBias(LOW_BIAS) .setOverrideDeadline(0) .build(); mJobScheduler.schedule(job); } - final int higherBiasJobId = 100 + JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; + final int higherBiasJobId = 100 + JobConcurrencyManager.MAX_CONCURRENCY_LIMIT; JobInfo jobHigher = new JobInfo.Builder(higherBiasJobId, sJobServiceComponent) .setBias(HIGH_BIAS) .setMinimumLatency(2000) @@ -88,14 +88,14 @@ public class BiasSchedulingTest extends AndroidTestCase { } public void testHigherBiasJobNotPreempted() throws Exception { - for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + for (int i = 0; i < JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT; ++i) { JobInfo job = new JobInfo.Builder(100 + i, sJobServiceComponent) .setBias(HIGH_BIAS) .setOverrideDeadline(0) .build(); mJobScheduler.schedule(job); } - final int lowerBiasJobId = 100 + JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; + final int lowerBiasJobId = 100 + JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT; JobInfo jobLower = new JobInfo.Builder(lowerBiasJobId, sJobServiceComponent) .setBias(LOW_BIAS) .setMinimumLatency(2000) diff --git a/services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java b/services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java index dd9ae6592f5c..0fd6a9e9248a 100644 --- a/services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java @@ -16,6 +16,7 @@ package com.android.server.job; +import static com.android.server.job.JobConcurrencyManager.MAX_CONCURRENCY_LIMIT; import static com.android.server.job.JobConcurrencyManager.NUM_WORK_TYPES; import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BG; import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER; @@ -191,10 +192,10 @@ public class WorkCountTrackerTest { } private void recount(Jobs jobs, int totalMax, - @NonNull List<Pair<Integer, Integer>> minLimits, - @NonNull List<Pair<Integer, Integer>> maxLimits) { + @NonNull List<Pair<Integer, Float>> minLimitRatios, + @NonNull List<Pair<Integer, Float>> maxLimitRatios) { mWorkCountTracker.setConfig(new JobConcurrencyManager.WorkTypeConfig( - "test", totalMax, minLimits, maxLimits)); + "test", MAX_CONCURRENCY_LIMIT, totalMax, minLimitRatios, maxLimitRatios)); mWorkCountTracker.resetCounts(); for (int i = 0; i < jobs.running.size(); ++i) { @@ -259,18 +260,18 @@ public class WorkCountTrackerTest { * Used by the following testRandom* tests. */ private void checkRandom(Jobs jobs, int numTests, int totalMax, - @NonNull List<Pair<Integer, Integer>> minLimits, - @NonNull List<Pair<Integer, Integer>> maxLimits, + @NonNull List<Pair<Integer, Float>> minLimitRatios, + @NonNull List<Pair<Integer, Float>> maxLimitRatios, double probStart, double[] typeCdf, double[] numTypesCdf, double probStop) { int minExpected = 0; - for (Pair<Integer, Integer> minLimit : minLimits) { - minExpected = Math.min(minLimit.second, minExpected); + for (Pair<Integer, Float> minLimit : minLimitRatios) { + minExpected = Math.min((int) (minLimit.second * MAX_CONCURRENCY_LIMIT), minExpected); } for (int i = 0; i < numTests; i++) { jobs.maybeFinishJobs(probStop); jobs.maybeEnqueueJobs(probStart, typeCdf, numTypesCdf); - recount(jobs, totalMax, minLimits, maxLimits); + recount(jobs, totalMax, minLimitRatios, maxLimitRatios); final int numPending = jobs.pendingMultiTypes.size(); startPendingJobs(jobs); @@ -284,9 +285,11 @@ public class WorkCountTrackerTest { } assertThat(totalRunning).isAtMost(totalMax); assertThat(totalRunning).isAtLeast(Math.min(minExpected, numPending)); - for (Pair<Integer, Integer> maxLimit : maxLimits) { - assertWithMessage("Work type " + maxLimit.first + " is running too many jobs") - .that(jobs.running.get(maxLimit.first)).isAtMost(maxLimit.second); + for (Pair<Integer, Float> maxLimitRatio : maxLimitRatios) { + final int workType = maxLimitRatio.first; + final int maxLimit = (int) (maxLimitRatio.second * MAX_CONCURRENCY_LIMIT); + assertWithMessage("Work type " + workType + " is running too many jobs") + .that(jobs.running.get(workType)).isAtMost(maxLimit); } } } @@ -302,12 +305,14 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 4)); - final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3)); final double probStop = 0.1; final double probStart = 0.1; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, EQUAL_PROBABILITY_CDF, EQUAL_PROBABILITY_CDF, probStop); } @@ -317,15 +322,15 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 2; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_BG, 2), Pair.create(WORK_TYPE_BGUSER, 1)); - final List<Pair<Integer, Integer>> minLimits = List.of(); + final List<Pair<Integer, Float>> minLimitRatios = List.of(); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1f), Pair.create(WORK_TYPE_BGUSER, .5f)); final double probStop = 0.5; final double[] cdf = buildWorkTypeCdf(0.5, 0, 0, 0.5, 0, 0); final double[] numTypesCdf = buildCdf(.5, .3, .15, .05); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -335,15 +340,15 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 2; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_BG, 2), Pair.create(WORK_TYPE_BGUSER, 1)); - final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2)); + final List<Pair<Integer, Float>> minLimitRatios = List.of(Pair.create(WORK_TYPE_BG, .99f)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1f), Pair.create(WORK_TYPE_BGUSER, .5f)); final double probStop = 0.5; final double[] cdf = buildWorkTypeCdf(1.0 / 3, 0, 0, 1.0 / 3, 0, 1.0 / 3); final double[] numTypesCdf = buildCdf(.75, .2, .05); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -353,15 +358,15 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 10; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_BG, 2), Pair.create(WORK_TYPE_BGUSER, 1)); - final List<Pair<Integer, Integer>> minLimits = List.of(); + final List<Pair<Integer, Float>> minLimitRatios = List.of(); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, .2f), Pair.create(WORK_TYPE_BGUSER, .1f)); final double probStop = 0.5; final double[] cdf = buildWorkTypeCdf(1.0 / 3, 0, 0, 1.0 / 3, 0, 1.0 / 3); final double[] numTypesCdf = buildCdf(.05, .95); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -371,15 +376,17 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)); - final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 2.0f / 3)); final double probStop = 0.5; final double[] cdf = buildWorkTypeCdf(0.1, 0, 0, 0.8, 0.02, .08); final double[] numTypesCdf = buildCdf(.5, .3, .15, .05); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -389,15 +396,17 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)); - final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)); final double probStop = 0.5; final double[] cdf = buildWorkTypeCdf(0.85, 0.05, 0, 0.1, 0, 0); final double[] numTypesCdf = buildCdf(1); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -407,15 +416,17 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)); - final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)); final double probStop = 0.4; final double[] cdf = buildWorkTypeCdf(0.1, 0, 0, 0.1, 0.05, .75); final double[] numTypesCdf = buildCdf(0.5, 0.5); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -425,16 +436,18 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)); - final List<Pair<Integer, Integer>> minLimits = - List.of(Pair.create(WORK_TYPE_BG, 2), Pair.create(WORK_TYPE_BGUSER, 1)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 6)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)); final double probStop = 0.4; final double[] cdf = buildWorkTypeCdf(0.8, 0.1, 0, 0.05, 0, 0.05); final double[] numTypesCdf = buildCdf(1); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -444,16 +457,18 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)); - final List<Pair<Integer, Integer>> minLimits = - List.of(Pair.create(WORK_TYPE_BG, 2), Pair.create(WORK_TYPE_BGUSER, 1)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 6)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)); final double probStop = 0.5; final double[] cdf = buildWorkTypeCdf(0, 0, 0, 0.5, 0, 0.5); final double[] numTypesCdf = buildCdf(1); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -463,16 +478,18 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)); - final List<Pair<Integer, Integer>> minLimits = - List.of(Pair.create(WORK_TYPE_BG, 2), Pair.create(WORK_TYPE_BGUSER, 1)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 6)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)); final double probStop = 0.5; final double[] cdf = buildWorkTypeCdf(0, 0, 0, 0.1, 0, 0.9); final double[] numTypesCdf = buildCdf(0.9, 0.1); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -482,16 +499,18 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)); - final List<Pair<Integer, Integer>> minLimits = - List.of(Pair.create(WORK_TYPE_BG, 2), Pair.create(WORK_TYPE_BGUSER, 1)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 6)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)); final double probStop = 0.5; final double[] cdf = buildWorkTypeCdf(0, 0, 0, 0.9, 0, 0.1); final double[] numTypesCdf = buildCdf(1); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -501,15 +520,16 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 4)); - final List<Pair<Integer, Integer>> minLimits = - List.of(Pair.create(WORK_TYPE_EJ, 2), Pair.create(WORK_TYPE_BG, 2)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_EJ, 1.0f / 3), Pair.create(WORK_TYPE_BG, 1.0f / 3)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3)); final double probStop = 0.4; final double[] cdf = buildWorkTypeCdf(0.5, 0, 0.5, 0, 0, 0); final double[] numTypesCdf = buildCdf(0.1, 0.7, 0.2); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -522,16 +542,16 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 13; - final List<Pair<Integer, Integer>> maxLimits = List.of( - Pair.create(WORK_TYPE_EJ, 5), Pair.create(WORK_TYPE_BG, 4), - Pair.create(WORK_TYPE_BGUSER, 3)); - final List<Pair<Integer, Integer>> minLimits = - List.of(Pair.create(WORK_TYPE_EJ, 2), Pair.create(WORK_TYPE_BG, 1)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_EJ, 2.0f / 13), Pair.create(WORK_TYPE_BG, 1.0f / 13)); + final List<Pair<Integer, Float>> maxLimitRatios = List.of( + Pair.create(WORK_TYPE_EJ, 5.0f / 13), Pair.create(WORK_TYPE_BG, 4.0f / 13), + Pair.create(WORK_TYPE_BGUSER, 3.0f / 13)); final double probStop = 0.13; final double[] numTypesCdf = buildCdf(0, 0.05, 0.1, 0.7, 0.1, 0.05); final double probStart = 0.87; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, EQUAL_PROBABILITY_CDF, numTypesCdf, probStop); } @@ -541,15 +561,16 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_EJ, 5), Pair.create(WORK_TYPE_BG, 4)); - final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 2)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_EJ, 5.0f / 6), Pair.create(WORK_TYPE_BG, 2.0f / 3)); final double probStop = 0.4; final double[] cdf = buildWorkTypeCdf(.1, 0, 0.5, 0.35, 0, 0.05); final double[] numTypesCdf = buildCdf(1); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -559,17 +580,17 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 6; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_EJ, 5), Pair.create(WORK_TYPE_BG, 4), - Pair.create(WORK_TYPE_BGUSER, 1)); - final List<Pair<Integer, Integer>> minLimits = - List.of(Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_EJ, .5f), Pair.create(WORK_TYPE_BG, 1.0f / 3)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_EJ, 5.0f / 6), Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 6)); final double probStop = 0.4; final double[] cdf = buildWorkTypeCdf(0.01, 0.09, 0.4, 0.1, 0, 0.4); final double[] numTypesCdf = buildCdf(0.7, 0.3); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } @@ -579,25 +600,25 @@ public class WorkCountTrackerTest { final int numTests = 5000; final int totalMax = 7; - final List<Pair<Integer, Integer>> maxLimits = - List.of(Pair.create(WORK_TYPE_EJ, 5), Pair.create(WORK_TYPE_BG, 4), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1), - Pair.create(WORK_TYPE_BGUSER, 1)); - final List<Pair<Integer, Integer>> minLimits = - List.of(Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_EJ, 3.0f / 7), Pair.create(WORK_TYPE_BG, 2.0f / 7)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_EJ, 5.0f / 7), Pair.create(WORK_TYPE_BG, 4.0f / 7), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1.0f / 7), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 7)); final double probStop = 0.4; final double[] cdf = buildWorkTypeCdf(0.01, 0.09, 0.25, 0.05, 0.3, 0.3); final double[] numTypesCdf = buildCdf(0.7, 0.3); final double probStart = 0.5; - checkRandom(jobs, numTests, totalMax, minLimits, maxLimits, probStart, + checkRandom(jobs, numTests, totalMax, minLimitRatios, maxLimitRatios, probStart, cdf, numTypesCdf, probStop); } /** Used by the following tests */ private void checkSimple(int totalMax, - @NonNull List<Pair<Integer, Integer>> minLimits, - @NonNull List<Pair<Integer, Integer>> maxLimits, + @NonNull List<Pair<Integer, Float>> minLimitRatios, + @NonNull List<Pair<Integer, Float>> maxLimitRatios, @NonNull List<Pair<Integer, Integer>> running, @NonNull List<Pair<Integer, Integer>> pending, @NonNull List<Pair<Integer, Integer>> resultRunning, @@ -610,17 +631,19 @@ public class WorkCountTrackerTest { jobs.addPending(pend.first, pend.second); } - recount(jobs, totalMax, minLimits, maxLimits); + recount(jobs, totalMax, minLimitRatios, maxLimitRatios); startPendingJobs(jobs); for (Pair<Integer, Integer> run : resultRunning) { assertWithMessage( - "Incorrect running result for work type " + workTypeToString(run.first)) + "Incorrect running result for work type " + workTypeToString(run.first) + + " wanted " + run.second + ", got " + jobs.running.get(run.first)) .that(jobs.running.get(run.first)).isEqualTo(run.second); } for (Pair<Integer, Integer> pend : resultPending) { assertWithMessage( - "Incorrect pending result for work type " + workTypeToString(pend.first)) + "Incorrect pending result for work type " + workTypeToString(pend.first) + + " wanted " + pend.second + ", got " + jobs.pending.get(pend.first)) .that(jobs.pending.get(pend.first)).isEqualTo(pend.second); } } @@ -628,16 +651,18 @@ public class WorkCountTrackerTest { @Test public void testBasic() { checkSimple(6, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)), + /* max */List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)), /* run */ List.of(), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 1)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 1)), /* resPen */ List.of()); checkSimple(6, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)), /* run */ List.of(), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 6)), @@ -645,39 +670,40 @@ public class WorkCountTrackerTest { // When there are BG jobs pending, 2 (min-BG) jobs should run. checkSimple(6, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)), /* run */ List.of(), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 1)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 5), Pair.create(WORK_TYPE_BG, 1)), /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 5))); checkSimple(6, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 4)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3)), /* run */ List.of(), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 2)), /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 1))); checkSimple(8, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 6)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .25f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, .75f)), /* run */ List.of(), /* pen */ List.of(Pair.create(WORK_TYPE_BG, 49)), /* resRun */ List.of(Pair.create(WORK_TYPE_BG, 6)), /* resPen */ List.of(Pair.create(WORK_TYPE_BG, 43))); checkSimple(8, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 6)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .25f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, .75f)), /* run */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 4)), /* pen */ List.of(Pair.create(WORK_TYPE_BG, 49)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 6)), /* resPen */ List.of(Pair.create(WORK_TYPE_BG, 47))); checkSimple(8, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 6)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .25f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, .75f)), /* run */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 4)), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 49), Pair.create(WORK_TYPE_BG, 49)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 4)), @@ -686,48 +712,52 @@ public class WorkCountTrackerTest { checkSimple(8, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 6)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .25f)), + /* max */ + List.of(Pair.create(WORK_TYPE_TOP, .75f), Pair.create(WORK_TYPE_BG, .75f)), /* run */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 4)), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 49)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 4)), /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 8), Pair.create(WORK_TYPE_BG, 49))); checkSimple(8, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 6)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .25f)), + /* max */ List.of( + Pair.create(WORK_TYPE_TOP, .75f), Pair.create(WORK_TYPE_BG, .75f)), /* run */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 1)), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 49)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 2)), /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 48))); checkSimple(8, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 1)), - /* max */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 2)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 8)), + /* max */ List.of( + Pair.create(WORK_TYPE_TOP, .75f), Pair.create(WORK_TYPE_BG, .25f)), /* run */ List.of(Pair.create(WORK_TYPE_BG, 6)), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 49)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 6)), /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 8), Pair.create(WORK_TYPE_BG, 49))); checkSimple(8, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 1)), - /* max */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 2)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 8)), + /* max */ + List.of(Pair.create(WORK_TYPE_TOP, .75f), Pair.create(WORK_TYPE_BG, .25f)), /* run */ List.of(Pair.create(WORK_TYPE_BG, 6)), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 49)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 6)), /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 8), Pair.create(WORK_TYPE_BG, 49))); checkSimple(6, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 4)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3)), /* run */ List.of(Pair.create(WORK_TYPE_TOP, 6)), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3)), /* resRun */ List.of(Pair.create(WORK_TYPE_TOP, 6)), /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3))); checkSimple(8, - /* min */ List.of(Pair.create(WORK_TYPE_EJ, 2), Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 4)), + /* min */ List.of(Pair.create(WORK_TYPE_EJ, .25f), Pair.create(WORK_TYPE_BG, .25f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, .75f)), /* run */ List.of(Pair.create(WORK_TYPE_TOP, 6)), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_EJ, 5), Pair.create(WORK_TYPE_BG, 3)), @@ -740,15 +770,16 @@ public class WorkCountTrackerTest { // shouldn't start new ones. checkSimple(5, /* min */ List.of(), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 1)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, .2f)), /* run */ List.of(Pair.create(WORK_TYPE_BG, 6)), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3)), /* resRun */ List.of(Pair.create(WORK_TYPE_BG, 6)), /* resPen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3))); checkSimple(6, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)), /* run */ List.of(Pair.create(WORK_TYPE_BG, 2)), /* pen */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 3), @@ -759,8 +790,9 @@ public class WorkCountTrackerTest { Pair.create(WORK_TYPE_BGUSER, 3))); checkSimple(6, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 3)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)), + /* max */ List.of( + Pair.create(WORK_TYPE_BG, 2.0f / 3), Pair.create(WORK_TYPE_BGUSER, .5f)), /* run */ List.of(Pair.create(WORK_TYPE_BG, 2)), /* pen */ List.of(Pair.create(WORK_TYPE_BG, 3), Pair.create(WORK_TYPE_BGUSER, 3)), /* resRun */ List.of( @@ -769,8 +801,9 @@ public class WorkCountTrackerTest { Pair.create(WORK_TYPE_BG, 1), Pair.create(WORK_TYPE_BGUSER, 1))); checkSimple(6, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 2)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 1)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 6)), /* run */ List.of(Pair.create(WORK_TYPE_BG, 2)), /* pen */ List.of(Pair.create(WORK_TYPE_BG, 3), Pair.create(WORK_TYPE_BGUSER, 3)), /* resRun */ List.of( @@ -778,12 +811,13 @@ public class WorkCountTrackerTest { /* resPen */ List.of( Pair.create(WORK_TYPE_BG, 1), Pair.create(WORK_TYPE_BGUSER, 2))); - Log.d(TAG, "START***#*#*#*#*#*#**#*"); // Test multi-types checkSimple(6, - /* min */ List.of(Pair.create(WORK_TYPE_EJ, 2), Pair.create(WORK_TYPE_BG, 2)), + /* min */ + List.of(Pair.create(WORK_TYPE_EJ, 1.0f / 3), Pair.create(WORK_TYPE_BG, 1.0f / 3)), /* max */ List.of( - Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 1)), + Pair.create(WORK_TYPE_BG, 2.0f / 3), + Pair.create(WORK_TYPE_BGUSER, 1.0f / 6)), /* run */ List.of(), /* pen */ List.of( // 2 of these as TOP, 1 as EJ @@ -809,10 +843,12 @@ public class WorkCountTrackerTest { jobs.addPending(WORK_TYPE_BG, 10); final int totalMax = 6; - final List<Pair<Integer, Integer>> minLimits = List.of(Pair.create(WORK_TYPE_BG, 1)); - final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 5)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 1.0f / totalMax)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 5.0f / totalMax)); - recount(jobs, totalMax, minLimits, maxLimits); + recount(jobs, totalMax, minLimitRatios, maxLimitRatios); startPendingJobs(jobs); @@ -887,11 +923,12 @@ public class WorkCountTrackerTest { jobs.addPending(WORK_TYPE_BG, 10); // c final int totalMax = 8; - final List<Pair<Integer, Integer>> minLimits = - List.of(Pair.create(WORK_TYPE_EJ, 1), Pair.create(WORK_TYPE_BG, 1)); - final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 5)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_EJ, 1.0f / 8), Pair.create(WORK_TYPE_BG, 1.0f / 8)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 5.0f / 8)); - recount(jobs, totalMax, minLimits, maxLimits); + recount(jobs, totalMax, minLimitRatios, maxLimitRatios); assertThat(jobs.pending.get(WORK_TYPE_TOP)).isEqualTo(11); assertThat(jobs.pending.get(WORK_TYPE_EJ)).isEqualTo(5); @@ -966,11 +1003,12 @@ public class WorkCountTrackerTest { } final int totalMax = 8; - final List<Pair<Integer, Integer>> minLimits = - List.of(Pair.create(WORK_TYPE_EJ, 1), Pair.create(WORK_TYPE_BG, 1)); - final List<Pair<Integer, Integer>> maxLimits = List.of(Pair.create(WORK_TYPE_BG, 5)); + final List<Pair<Integer, Float>> minLimitRatios = + List.of(Pair.create(WORK_TYPE_EJ, 1.0f / 8), Pair.create(WORK_TYPE_BG, 1.0f / 8)); + final List<Pair<Integer, Float>> maxLimitRatios = + List.of(Pair.create(WORK_TYPE_BG, 5.0f / 8)); - recount(jobs, totalMax, minLimits, maxLimits); + recount(jobs, totalMax, minLimitRatios, maxLimitRatios); assertThat(jobs.pending.get(WORK_TYPE_TOP)).isEqualTo(11); assertThat(jobs.pending.get(WORK_TYPE_EJ)).isEqualTo(10); diff --git a/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java b/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java index 21d27848bc57..bd5a063b1484 100644 --- a/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java @@ -45,23 +45,26 @@ import java.util.List; @SmallTest public class WorkTypeConfigTest { private static final String KEY_MAX_TOTAL = "concurrency_max_total_test"; - private static final String KEY_MAX_TOP = "concurrency_max_top_test"; - private static final String KEY_MAX_FGS = "concurrency_max_fgs_test"; - private static final String KEY_MAX_EJ = "concurrency_max_ej_test"; - private static final String KEY_MAX_BG = "concurrency_max_bg_test"; - private static final String KEY_MAX_BGUSER_IMPORTANT = "concurrency_max_bguser_important_test"; - private static final String KEY_MAX_BGUSER = "concurrency_max_bguser_test"; - private static final String KEY_MIN_TOP = "concurrency_min_top_test"; - private static final String KEY_MIN_FGS = "concurrency_min_fgs_test"; - private static final String KEY_MIN_EJ = "concurrency_min_ej_test"; - private static final String KEY_MIN_BG = "concurrency_min_bg_test"; - private static final String KEY_MIN_BGUSER_IMPORTANT = "concurrency_min_bguser_important_test"; - private static final String KEY_MIN_BGUSER = "concurrency_min_bguser_test"; + private static final String KEY_MAX_RATIO_TOP = "concurrency_max_ratio_top_test"; + private static final String KEY_MAX_RATIO_FGS = "concurrency_max_ratio_fgs_test"; + private static final String KEY_MAX_RATIO_EJ = "concurrency_max_ratio_ej_test"; + private static final String KEY_MAX_RATIO_BG = "concurrency_max_ratio_bg_test"; + private static final String KEY_MAX_RATIO_BGUSER_IMPORTANT = + "concurrency_max_ratio_bguser_important_test"; + private static final String KEY_MAX_RATIO_BGUSER = "concurrency_max_ratio_bguser_test"; + private static final String KEY_MIN_RATIO_TOP = "concurrency_min_ratio_top_test"; + private static final String KEY_MIN_RATIO_FGS = "concurrency_min_ratio_fgs_test"; + private static final String KEY_MIN_RATIO_EJ = "concurrency_min_ratio_ej_test"; + private static final String KEY_MIN_RATIO_BG = "concurrency_min_ratio_bg_test"; + private static final String KEY_MIN_RATIO_BGUSER_IMPORTANT = + "concurrency_min_ratio_bguser_important_test"; + private static final String KEY_MIN_RATIO_BGUSER = "concurrency_min_ratio_bguser_test"; private void check(@Nullable DeviceConfig.Properties config, + int defaultLimit, int defaultTotal, - @NonNull List<Pair<Integer, Integer>> defaultMin, - @NonNull List<Pair<Integer, Integer>> defaultMax, + @NonNull List<Pair<Integer, Float>> defaultMinRatios, + @NonNull List<Pair<Integer, Float>> defaultMaxRatios, boolean expectedValid, int expectedTotal, @NonNull List<Pair<Integer, Integer>> expectedMinLimits, @NonNull List<Pair<Integer, Integer>> expectedMaxLimits) throws Exception { @@ -69,7 +72,7 @@ public class WorkTypeConfigTest { final WorkTypeConfig counts; try { counts = new WorkTypeConfig("test", - defaultTotal, defaultMin, defaultMax); + defaultLimit, defaultTotal, defaultMinRatios, defaultMaxRatios); if (!expectedValid) { fail("Invalid config successfully created"); return; @@ -84,7 +87,7 @@ public class WorkTypeConfigTest { } if (config != null) { - counts.update(config); + counts.update(config, defaultLimit); } assertEquals(expectedTotal, counts.getMaxTotal()); @@ -101,7 +104,7 @@ public class WorkTypeConfigTest { @Test public void test() throws Exception { // Tests with various combinations. - check(null, /*default*/ 13, + check(null, /* limit */ 16, /*default*/ 13, /* min */ List.of(), /* max */ List.of(), /*expected*/ true, 13, @@ -109,111 +112,141 @@ public class WorkTypeConfigTest { Pair.create(WORK_TYPE_BG, 0), Pair.create(WORK_TYPE_BGUSER, 0)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 13), Pair.create(WORK_TYPE_EJ, 13), Pair.create(WORK_TYPE_BG, 13), Pair.create(WORK_TYPE_BGUSER, 13))); - check(null, /*default*/ 5, - /* min */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 0)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 1)), + check(null, /* limit */ 16, /*default*/ 5, + /* min */ List.of(Pair.create(WORK_TYPE_TOP, .8f), Pair.create(WORK_TYPE_BG, 0f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, .2f)), /*expected*/ true, 5, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 0)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 5), Pair.create(WORK_TYPE_BG, 1))); - check(null, /*default*/ 5, + check(null, /* limit */ 16, /*default*/ 5, + /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1f), + Pair.create(WORK_TYPE_BG, 0f), Pair.create(WORK_TYPE_BGUSER, 0f)), + /* max */ List.of( + Pair.create(WORK_TYPE_BG, 0f), Pair.create(WORK_TYPE_BGUSER, .2f)), + /*expected*/ false, 5, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 5), Pair.create(WORK_TYPE_BG, 0), Pair.create(WORK_TYPE_BGUSER, 0)), + /* max */ List.of(Pair.create(WORK_TYPE_TOP, 5), + Pair.create(WORK_TYPE_BG, 1), Pair.create(WORK_TYPE_BGUSER, 1))); + check(null, /* limit */ 16, /*default*/ 5, + /* min */ List.of(Pair.create(WORK_TYPE_TOP, .99f), + Pair.create(WORK_TYPE_BG, 0f), Pair.create(WORK_TYPE_BGUSER, 0f)), /* max */ List.of( - Pair.create(WORK_TYPE_BG, 0), Pair.create(WORK_TYPE_BGUSER, 1)), + Pair.create(WORK_TYPE_BG, .01f), Pair.create(WORK_TYPE_BGUSER, .2f)), /*expected*/ true, 5, - /* min */ List.of(Pair.create(WORK_TYPE_TOP, 5), + /* min */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 0), Pair.create(WORK_TYPE_BGUSER, 0)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 5), Pair.create(WORK_TYPE_BG, 1), Pair.create(WORK_TYPE_BGUSER, 1))); - check(null, /*default*/ 0, - /* min */ List.of(Pair.create(WORK_TYPE_TOP, 5), Pair.create(WORK_TYPE_BG, 0)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 0)), + check(null, /* limit */ 16, /*default*/ 0, + /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1f), Pair.create(WORK_TYPE_BG, 0f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 0f)), /*expected*/ false, 1, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 0)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 1))); - check(null, /*default*/ -1, - /* min */ List.of(Pair.create(WORK_TYPE_BG, -1)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, -1)), + check(null, /* limit */ 16, /*default*/ -1, + /* min */ List.of(Pair.create(WORK_TYPE_BG, -1f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, -1f)), /*expected*/ false, 1, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 0)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 1))); - check(null, /*default*/ 5, + check(null, /* limit */ 16, /*default*/ 5, /* min */ List.of( - Pair.create(WORK_TYPE_BG, 5), Pair.create(WORK_TYPE_BGUSER, 0)), + Pair.create(WORK_TYPE_BG, 1f), Pair.create(WORK_TYPE_BGUSER, 0f)), /* max */ List.of( - Pair.create(WORK_TYPE_BG, 5), Pair.create(WORK_TYPE_BGUSER, 5)), - /*expected*/ true, 5, + Pair.create(WORK_TYPE_BG, 1f), Pair.create(WORK_TYPE_BGUSER, 1f)), + /*expected*/ false, 5, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 0)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 5), Pair.create(WORK_TYPE_BG, 5), Pair.create(WORK_TYPE_BGUSER, 5))); - check(null, /*default*/ 6, + check(null, /* limit */ 16, /*default*/ 5, + /* min */ List.of( + Pair.create(WORK_TYPE_BG, .99f), Pair.create(WORK_TYPE_BGUSER, 0f)), + /* max */ List.of( + Pair.create(WORK_TYPE_BG, 1f), Pair.create(WORK_TYPE_BGUSER, 1f)), + /*expected*/ true, 5, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), - Pair.create(WORK_TYPE_BG, 6), Pair.create(WORK_TYPE_BGUSER, 2)), + Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 0)), + /* max */ List.of(Pair.create(WORK_TYPE_TOP, 5), + Pair.create(WORK_TYPE_BG, 5), Pair.create(WORK_TYPE_BGUSER, 5))); + check(null, /* limit */ 16, /*default*/ 6, + /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1.0f / 6), + Pair.create(WORK_TYPE_BG, 1f), Pair.create(WORK_TYPE_BGUSER, 1.0f / 3)), /* max */ List.of( - Pair.create(WORK_TYPE_BG, 5), Pair.create(WORK_TYPE_BGUSER, 1)), + Pair.create(WORK_TYPE_BG, 1f), Pair.create(WORK_TYPE_BGUSER, 1.0f / 6)), /*expected*/ false, 6, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 5), Pair.create(WORK_TYPE_BGUSER, 0)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 5), Pair.create(WORK_TYPE_BGUSER, 1))); - check(null, /*default*/ 4, + check(null, /* limit */ 16, /*default*/ 4, /* min */ List.of( - Pair.create(WORK_TYPE_BG, 6), Pair.create(WORK_TYPE_BGUSER, 6)), + Pair.create(WORK_TYPE_BG, 1.5f), Pair.create(WORK_TYPE_BGUSER, 1.5f)), /* max */ List.of( - Pair.create(WORK_TYPE_BG, 5), Pair.create(WORK_TYPE_BGUSER, 5)), + Pair.create(WORK_TYPE_BG, 1.25f), Pair.create(WORK_TYPE_BGUSER, 1.25f)), /*expected*/ false, 4, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 3), Pair.create(WORK_TYPE_BGUSER, 0)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 4))); - check(null, /*default*/ 5, - /* min */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 1)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 1)), + check(null, /* limit */ 16, /*default*/ 5, + /* min */ List.of(Pair.create(WORK_TYPE_TOP, .8f), Pair.create(WORK_TYPE_BG, .2f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, .2f)), /*expected*/ true, 5, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 1)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 5), Pair.create(WORK_TYPE_BG, 1))); - check(null, /*default*/ 10, - /* min */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_EJ, 3), - Pair.create(WORK_TYPE_BG, 1)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 1)), + check(null, /* limit */ 16, /*default*/ 10, + /* min */ List.of(Pair.create(WORK_TYPE_TOP, .4f), Pair.create(WORK_TYPE_EJ, .3f), + Pair.create(WORK_TYPE_BG, .1f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, .1f)), /*expected*/ true, 10, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 1)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 10), Pair.create(WORK_TYPE_BG, 1))); - check(null, /*default*/ 10, - /* min */ List.of(Pair.create(WORK_TYPE_TOP, 3), Pair.create(WORK_TYPE_FGS, 2), - Pair.create(WORK_TYPE_EJ, 1), Pair.create(WORK_TYPE_BG, 1)), - /* max */ List.of(Pair.create(WORK_TYPE_FGS, 3)), + check(null, /* limit */ 16, /*default*/ 10, + /* min */ List.of(Pair.create(WORK_TYPE_TOP, .3f), Pair.create(WORK_TYPE_FGS, .2f), + Pair.create(WORK_TYPE_EJ, .1f), Pair.create(WORK_TYPE_BG, .1f)), + /* max */ List.of(Pair.create(WORK_TYPE_FGS, .3f)), /*expected*/ true, 10, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 3), Pair.create(WORK_TYPE_FGS, 2), Pair.create(WORK_TYPE_EJ, 1), Pair.create(WORK_TYPE_BG, 1)), /* max */ List.of(Pair.create(WORK_TYPE_FGS, 3))); - check(null, /*default*/ 15, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 15)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 15)), + check(null, /* limit */ 16, /*default*/ 15, + /* min */ List.of(Pair.create(WORK_TYPE_BG, .95f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 1f)), /*expected*/ true, 15, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 14)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 15), Pair.create(WORK_TYPE_BG, 15))); - check(null, /*default*/ 16, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 16)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 16)), + check(null, /* limit */ 16, /*default*/ 16, + /* min */ List.of(Pair.create(WORK_TYPE_BG, .99f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 1f)), /*expected*/ true, 16, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 15)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 16), Pair.create(WORK_TYPE_BG, 16))); - check(null, /*default*/ 20, + check(null, /* limit */ 16, /*default*/ 20, /* min */ List.of( - Pair.create(WORK_TYPE_BG, 20), Pair.create(WORK_TYPE_BGUSER, 10)), + Pair.create(WORK_TYPE_BG, .99f), Pair.create(WORK_TYPE_BGUSER, .5f)), /* max */ List.of( - Pair.create(WORK_TYPE_BG, 20), Pair.create(WORK_TYPE_BGUSER, 20)), + Pair.create(WORK_TYPE_BG, 1f), Pair.create(WORK_TYPE_BGUSER, 1f)), /*expected*/ false, 16, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 15), Pair.create(WORK_TYPE_BGUSER, 0)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 16), Pair.create(WORK_TYPE_BG, 16), Pair.create(WORK_TYPE_BGUSER, 16))); - check(null, /*default*/ 20, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 16)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 16)), + check(null, /* limit */ 76, /*default*/ 80, + /* min */ List.of( + Pair.create(WORK_TYPE_BG, .98f), Pair.create(WORK_TYPE_BGUSER, .9f)), + /* max */ List.of( + Pair.create(WORK_TYPE_BG, 1f), Pair.create(WORK_TYPE_BGUSER, 1f)), + /*expected*/ false, 64, + /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), + Pair.create(WORK_TYPE_BG, 63), Pair.create(WORK_TYPE_BGUSER, 0)), + /* max */ List.of(Pair.create(WORK_TYPE_TOP, 64), + Pair.create(WORK_TYPE_BG, 64), Pair.create(WORK_TYPE_BGUSER, 64))); + check(null, /* limit */ 16, /*default*/ 20, + /* min */ List.of(Pair.create(WORK_TYPE_BG, .99f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 1f)), /*expected*/ true, 16, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 15)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 16), Pair.create(WORK_TYPE_BG, 16))); @@ -221,94 +254,101 @@ public class WorkTypeConfigTest { // Test for overriding with a setting string. check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER) .setInt(KEY_MAX_TOTAL, 5) - .setInt(KEY_MAX_BG, 4) - .setInt(KEY_MIN_BG, 3) + .setFloat(KEY_MAX_RATIO_BG, .8f) + .setFloat(KEY_MIN_RATIO_BG, .6f) .build(), + /* limit */ 16, /*default*/ 9, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 9)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .99f)), /* max */ List.of( - Pair.create(WORK_TYPE_BG, 9), Pair.create(WORK_TYPE_BGUSER, 2)), + Pair.create(WORK_TYPE_BG, 1f), Pair.create(WORK_TYPE_BGUSER, .4f)), /*expected*/ true, 5, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 3)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 5), Pair.create(WORK_TYPE_BG, 4), Pair.create(WORK_TYPE_BGUSER, 2))); check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER) .setInt(KEY_MAX_TOTAL, 5).build(), + /* limit */ 16, /*default*/ 9, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 9)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 9)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .99f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 1f)), /*expected*/ true, 5, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 4)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 5), Pair.create(WORK_TYPE_BG, 5))); check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER) - .setInt(KEY_MAX_BG, 4).build(), + .setFloat(KEY_MAX_RATIO_BG, 4.0f / 9).build(), + /* limit */ 16, /*default*/ 9, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 9)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 9)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .99f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 1f)), /*expected*/ true, 9, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 4)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 9), Pair.create(WORK_TYPE_BG, 4))); check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER) - .setInt(KEY_MIN_BG, 3).build(), + .setFloat(KEY_MIN_RATIO_BG, 1.0f / 3).build(), + /* limit */ 16, /*default*/ 9, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 9)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 9)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .99f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 1f)), /*expected*/ true, 9, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 3)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 9), Pair.create(WORK_TYPE_BG, 9))); check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER) .setInt(KEY_MAX_TOTAL, 20) - .setInt(KEY_MAX_EJ, 5) - .setInt(KEY_MIN_EJ, 2) - .setInt(KEY_MAX_BG, 16) - .setInt(KEY_MIN_BG, 8) + .setFloat(KEY_MAX_RATIO_EJ, .25f) + .setFloat(KEY_MIN_RATIO_EJ, .1f) + .setFloat(KEY_MAX_RATIO_BG, .8f) + .setFloat(KEY_MIN_RATIO_BG, .4f) .build(), + /* limit */ 16, /*default*/ 9, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 9)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 9)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .99f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 1f)), /*expected*/ true, 16, - /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_EJ, 2), - Pair.create(WORK_TYPE_BG, 8)), + /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_EJ, 1), + Pair.create(WORK_TYPE_BG, 6)), /* max */ - List.of(Pair.create(WORK_TYPE_TOP, 16), Pair.create(WORK_TYPE_EJ, 5), - Pair.create(WORK_TYPE_BG, 16))); + List.of(Pair.create(WORK_TYPE_TOP, 16), Pair.create(WORK_TYPE_EJ, 4), + Pair.create(WORK_TYPE_BG, 12))); check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER) .setInt(KEY_MAX_TOTAL, 20) - .setInt(KEY_MAX_BG, 20) - .setInt(KEY_MIN_BG, 8) + .setFloat(KEY_MAX_RATIO_BG, 1f) + .setFloat(KEY_MIN_RATIO_BG, .4f) .build(), + /* limit */ 16, /*default*/ 9, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 9)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 9)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .99f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 1f)), /*expected*/ true, 16, - /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 8)), + /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_BG, 6)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 16), Pair.create(WORK_TYPE_BG, 16))); check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER) .setInt(KEY_MAX_TOTAL, 16) - .setInt(KEY_MAX_TOP, 16) - .setInt(KEY_MIN_TOP, 1) - .setInt(KEY_MAX_FGS, 15) - .setInt(KEY_MIN_FGS, 2) - .setInt(KEY_MAX_EJ, 14) - .setInt(KEY_MIN_EJ, 3) - .setInt(KEY_MAX_BG, 13) - .setInt(KEY_MIN_BG, 4) - .setInt(KEY_MAX_BGUSER_IMPORTANT, 12) - .setInt(KEY_MIN_BGUSER_IMPORTANT, 5) - .setInt(KEY_MAX_BGUSER, 11) - .setInt(KEY_MIN_BGUSER, 6) + .setFloat(KEY_MAX_RATIO_TOP, 1f) + .setFloat(KEY_MIN_RATIO_TOP, 1.0f / 16) + .setFloat(KEY_MAX_RATIO_FGS, 15.0f / 16) + .setFloat(KEY_MIN_RATIO_FGS, 2.0f / 16) + .setFloat(KEY_MAX_RATIO_EJ, 14.0f / 16) + .setFloat(KEY_MIN_RATIO_EJ, 3.0f / 16) + .setFloat(KEY_MAX_RATIO_BG, 13.0f / 16) + .setFloat(KEY_MIN_RATIO_BG, 3.0f / 16) + .setFloat(KEY_MAX_RATIO_BGUSER_IMPORTANT, 12.0f / 16) + .setFloat(KEY_MIN_RATIO_BGUSER_IMPORTANT, 2.0f / 16) + .setFloat(KEY_MAX_RATIO_BGUSER, 11.0f / 16) + .setFloat(KEY_MIN_RATIO_BGUSER, 2.0f / 16) .build(), + /* limit */ 16, /*default*/ 9, - /* min */ List.of(Pair.create(WORK_TYPE_BG, 9)), - /* max */ List.of(Pair.create(WORK_TYPE_BG, 9)), + /* min */ List.of(Pair.create(WORK_TYPE_BG, .99f)), + /* max */ List.of(Pair.create(WORK_TYPE_BG, 1f)), /*expected*/ true, 16, /* min */ List.of(Pair.create(WORK_TYPE_TOP, 1), Pair.create(WORK_TYPE_FGS, 2), - Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 4), - Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 5), - Pair.create(WORK_TYPE_BGUSER, 6)), + Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 3), + Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 2), + Pair.create(WORK_TYPE_BGUSER, 2)), /* max */ List.of(Pair.create(WORK_TYPE_TOP, 16), Pair.create(WORK_TYPE_FGS, 15), Pair.create(WORK_TYPE_EJ, 14), Pair.create(WORK_TYPE_BG, 13), diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java index 065aec5b2f64..07fda309f03e 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java @@ -77,6 +77,7 @@ public class LocaleManagerServiceTest { /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null, /* originatingPackageName = */ null, /* installingPackageName = */ DEFAULT_INSTALLER_PACKAGE_NAME, + /* updateOwnerPackageName = */ null, /* packageSource = */ PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); private LocaleManagerService mLocaleManagerService; diff --git a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java index 494796ee48eb..9429462a6723 100644 --- a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java @@ -89,6 +89,7 @@ public class SystemAppUpdateTrackerTest { /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null, /* originatingPackageName = */ null, /* installingPackageName = */ DEFAULT_INSTALLER_PACKAGE_NAME, + /* updateOwnerPackageName = */ null, /* packageSource = */ PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); @Mock diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java index 281195de4b35..1b983f0bfb1b 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java @@ -1073,7 +1073,9 @@ public class RecoverableKeyStoreManagerTest { int uid = Binder.getCallingUid(); PendingIntent intent = PendingIntent.getBroadcast( InstrumentationRegistry.getTargetContext(), /*requestCode=*/1, - new Intent(), /*flags=*/ PendingIntent.FLAG_MUTABLE_UNAUDITED); + new Intent() + .setPackage(InstrumentationRegistry.getTargetContext().getPackageName()), + /*flags=*/ PendingIntent.FLAG_MUTABLE); mRecoverableKeyStoreManager.setSnapshotCreatedPendingIntent(intent); verify(mMockListenersStorage).setSnapshotListener(eq(uid), any(PendingIntent.class)); } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java index d9ebb4c26891..418d47452330 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java @@ -41,7 +41,9 @@ public class RecoverySnapshotListenersStorageTest { int recoveryAgentUid = 1000; PendingIntent intent = PendingIntent.getBroadcast( InstrumentationRegistry.getTargetContext(), /*requestCode=*/ 1, - new Intent(), /*flags=*/ PendingIntent.FLAG_MUTABLE_UNAUDITED); + new Intent() + .setPackage(InstrumentationRegistry.getTargetContext().getPackageName()), + /*flags=*/ PendingIntent.FLAG_MUTABLE); mStorage.setSnapshotListener(recoveryAgentUid, intent); assertTrue(mStorage.hasListener(recoveryAgentUid)); @@ -54,7 +56,9 @@ public class RecoverySnapshotListenersStorageTest { int recoveryAgentUid = 1000; mStorage.recoverySnapshotAvailable(recoveryAgentUid); PendingIntent intent = PendingIntent.getBroadcast( - context, /*requestCode=*/ 0, new Intent(TEST_INTENT_ACTION), /*flags=*/PendingIntent.FLAG_MUTABLE_UNAUDITED); + context, /*requestCode=*/ 0, + new Intent(TEST_INTENT_ACTION).setPackage(context.getPackageName()), + /*flags=*/PendingIntent.FLAG_MUTABLE); CountDownLatch latch = new CountDownLatch(1); context.registerReceiver(new BroadcastReceiver() { @Override @@ -75,7 +79,9 @@ public class RecoverySnapshotListenersStorageTest { int recoveryAgentUid = 1000; mStorage.recoverySnapshotAvailable(recoveryAgentUid); PendingIntent intent = PendingIntent.getBroadcast( - context, /*requestCode=*/ 0, new Intent(TEST_INTENT_ACTION), /*flags=*/PendingIntent.FLAG_MUTABLE_UNAUDITED); + context, /*requestCode=*/ 0, + new Intent(TEST_INTENT_ACTION).setPackage(context.getPackageName()), + /*flags=*/PendingIntent.FLAG_MUTABLE); CountDownLatch latch = new CountDownLatch(2); BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override diff --git a/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java index 17a587603009..d9cd77d8cd7c 100644 --- a/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.net; import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE; import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY; @@ -51,14 +51,13 @@ import android.os.PermissionEnforcer; import android.os.Process; import android.os.RemoteException; import android.permission.PermissionCheckerManager; +import android.platform.test.annotations.Presubmit; import android.test.suitebuilder.annotation.SmallTest; import android.util.ArrayMap; import androidx.test.runner.AndroidJUnit4; import com.android.internal.app.IBatteryStats; -import com.android.server.NetworkManagementService.Dependencies; -import com.android.server.net.BaseNetworkObserver; import org.junit.After; import org.junit.Before; @@ -76,6 +75,7 @@ import java.util.function.BiFunction; */ @RunWith(AndroidJUnit4.class) @SmallTest +@Presubmit public class NetworkManagementServiceTest { private NetworkManagementService mNMService; @Mock private Context mContext; @@ -92,7 +92,7 @@ public class NetworkManagementServiceTest { private final MockDependencies mDeps = new MockDependencies(); private final MockPermissionEnforcer mPermissionEnforcer = new MockPermissionEnforcer(); - private final class MockDependencies extends Dependencies { + private final class MockDependencies extends NetworkManagementService.Dependencies { @Override public IBinder getService(String name) { switch (name) { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java index 2293808a5d64..a85c7227b954 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java @@ -327,7 +327,9 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { } private IntentSender makeResultIntent() { - return PendingIntent.getActivity(getTestContext(), 0, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED).getIntentSender(); + return PendingIntent.getActivity(getTestContext(), 0, + new Intent().setPackage(getTestContext().getPackageName()), + PendingIntent.FLAG_MUTABLE).getIntentSender(); } public void testRequestPinShortcut_withCallback() { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java index a47a8df51c9f..2fca3d07149e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java @@ -150,7 +150,9 @@ public class ShortcutManagerTest9 extends BaseShortcutManagerTest { public void testRequestPinAppWidget_withCallback() { final PendingIntent resultIntent = - PendingIntent.getActivity(getTestContext(), 0, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED); + PendingIntent.getActivity(getTestContext(), 0, + new Intent().setPackage(getTestContext().getPackageName()), + PendingIntent.FLAG_MUTABLE); checkRequestPinAppWidget(resultIntent); } diff --git a/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java b/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java index b00fb92f9c46..bbe89073d34d 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java @@ -29,7 +29,6 @@ import android.content.pm.UserInfo; import android.os.RemoteException; import android.os.UserManager; import android.platform.test.annotations.Postsubmit; -import android.provider.Settings; import android.util.Log; import androidx.test.InstrumentationRegistry; @@ -38,7 +37,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.FunctionalUtils; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -64,22 +62,12 @@ public class UserLifecycleStressTest { private Context mContext; private UserManager mUserManager; private ActivityManager mActivityManager; - private String mRemoveGuestOnExitOriginalValue; @Before public void setup() { mContext = InstrumentationRegistry.getInstrumentation().getContext(); mUserManager = mContext.getSystemService(UserManager.class); mActivityManager = mContext.getSystemService(ActivityManager.class); - mRemoveGuestOnExitOriginalValue = Settings.Global.getString(mContext.getContentResolver(), - Settings.Global.REMOVE_GUEST_ON_EXIT); - - } - - @After - public void tearDown() { - Settings.Global.putString(mContext.getContentResolver(), - Settings.Global.REMOVE_GUEST_ON_EXIT, mRemoveGuestOnExitOriginalValue); } /** @@ -117,9 +105,6 @@ public class UserLifecycleStressTest { **/ @Test public void switchToExistingGuestAndStartOverStressTest() throws Exception { - Settings.Global.putString(mContext.getContentResolver(), - Settings.Global.REMOVE_GUEST_ON_EXIT, "0"); - if (ActivityManager.getCurrentUser() != USER_SYSTEM) { switchUser(USER_SYSTEM); } diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index 34dad0922468..1889d9a07692 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -175,14 +175,14 @@ public final class UserManagerTest { final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference(); // Test that only one clone user can be created - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo = createProfileForUser("Clone user1", UserManager.USER_TYPE_PROFILE_CLONE, - primaryUserId); + mainUserId); assertThat(userInfo).isNotNull(); UserInfo userInfo2 = createProfileForUser("Clone user2", UserManager.USER_TYPE_PROFILE_CLONE, - primaryUserId); + mainUserId); assertThat(userInfo2).isNull(); final Context userContext = mContext.createPackageContextAsUser("system", 0, @@ -212,12 +212,12 @@ public final class UserManagerTest { cloneUserProperties::getCrossProfileIntentResolutionStrategy); // Verify clone user parent - assertThat(mUserManager.getProfileParent(primaryUserId)).isNull(); + assertThat(mUserManager.getProfileParent(mainUserId)).isNull(); UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id); assertThat(parentProfileInfo).isNotNull(); - assertThat(primaryUserId).isEqualTo(parentProfileInfo.id); + assertThat(mainUserId).isEqualTo(parentProfileInfo.id); removeUser(userInfo.id); - assertThat(mUserManager.getProfileParent(primaryUserId)).isNull(); + assertThat(mUserManager.getProfileParent(mainUserId)).isNull(); } @MediumTest @@ -670,17 +670,16 @@ public final class UserManagerTest { @Test public void testGetProfileParent() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; - + int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo = createProfileForUser("Profile", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNotNull(); - assertThat(mUserManager.getProfileParent(primaryUserId)).isNull(); + assertThat(mUserManager.getProfileParent(mainUserId)).isNull(); UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id); assertThat(parentProfileInfo).isNotNull(); - assertThat(primaryUserId).isEqualTo(parentProfileInfo.id); + assertThat(mainUserId).isEqualTo(parentProfileInfo.id); removeUser(userInfo.id); - assertThat(mUserManager.getProfileParent(primaryUserId)).isNull(); + assertThat(mUserManager.getProfileParent(mainUserId)).isNull(); } /** Test that UserManager returns the correct badge information for a managed profile. */ @@ -694,9 +693,9 @@ public final class UserManagerTest { .that(userTypeDetails).isNotNull(); assertThat(userTypeDetails.getName()).isEqualTo(UserManager.USER_TYPE_PROFILE_MANAGED); - final int primaryUserId = mUserManager.getPrimaryUser().id; + int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo = createProfileForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNotNull(); final int userId = userInfo.id; @@ -739,9 +738,9 @@ public final class UserManagerTest { final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference(); // Create an actual user (of this user type) and get its properties. - final int primaryUserId = mUserManager.getPrimaryUser().id; + int mainUserId = mUserManager.getMainUser().getIdentifier(); final UserInfo userInfo = createProfileForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNotNull(); final int userId = userInfo.id; final UserProperties userProps = mUserManager.getUserProperties(UserHandle.of(userId)); @@ -762,11 +761,11 @@ public final class UserManagerTest { @Test public void testAddManagedProfile() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo1 = createProfileForUser("Managed 1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); UserInfo userInfo2 = createProfileForUser("Managed 2", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo1).isNotNull(); assertThat(userInfo2).isNull(); @@ -785,9 +784,9 @@ public final class UserManagerTest { @Test public void testAddManagedProfile_withDisallowedPackages() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo1 = createProfileForUser("Managed1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); // Verify that the packagesToVerify are installed by default. for (String pkg : PACKAGES) { if (!mPackageManager.isPackageAvailable(pkg)) { @@ -801,7 +800,7 @@ public final class UserManagerTest { removeUser(userInfo1.id); UserInfo userInfo2 = createProfileForUser("Managed2", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId, PACKAGES); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId, PACKAGES); // Verify that the packagesToVerify are not installed by default. for (String pkg : PACKAGES) { if (!mPackageManager.isPackageAvailable(pkg)) { @@ -821,9 +820,9 @@ public final class UserManagerTest { @Test public void testAddManagedProfile_disallowedPackagesInstalledLater() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo = createProfileForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId, PACKAGES); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId, PACKAGES); // Verify that the packagesToVerify are not installed by default. for (String pkg : PACKAGES) { if (!mPackageManager.isPackageAvailable(pkg)) { @@ -868,17 +867,17 @@ public final class UserManagerTest { @MediumTest @Test public void testCreateUser_disallowAddClonedUserProfile() throws Exception { - final int primaryUserId = ActivityManager.getCurrentUser(); - final UserHandle primaryUserHandle = asHandle(primaryUserId); + final int mainUserId = ActivityManager.getCurrentUser(); + final UserHandle mainUserHandle = asHandle(mainUserId); mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_CLONE_PROFILE, - true, primaryUserHandle); + true, mainUserHandle); try { UserInfo cloneProfileUserInfo = createProfileForUser("Clone", - UserManager.USER_TYPE_PROFILE_CLONE, primaryUserId); + UserManager.USER_TYPE_PROFILE_CLONE, mainUserId); assertThat(cloneProfileUserInfo).isNull(); } finally { mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_CLONE_PROFILE, false, - primaryUserHandle); + mainUserHandle); } } @@ -887,17 +886,17 @@ public final class UserManagerTest { @Test public void testCreateProfileForUser_disallowAddManagedProfile() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; - final UserHandle primaryUserHandle = asHandle(primaryUserId); + final int mainUserId = mUserManager.getMainUser().getIdentifier(); + final UserHandle mainUserHandle = asHandle(mainUserId); mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_MANAGED_PROFILE, true, - primaryUserHandle); + mainUserHandle); try { UserInfo userInfo = createProfileForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNull(); } finally { mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_MANAGED_PROFILE, false, - primaryUserHandle); + mainUserHandle); } } @@ -906,17 +905,17 @@ public final class UserManagerTest { @Test public void testCreateProfileForUserEvenWhenDisallowed() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; - final UserHandle primaryUserHandle = asHandle(primaryUserId); + final int mainUserId = mUserManager.getMainUser().getIdentifier(); + final UserHandle mainUserHandle = asHandle(mainUserId); mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_MANAGED_PROFILE, true, - primaryUserHandle); + mainUserHandle); try { UserInfo userInfo = createProfileEvenWhenDisallowedForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNotNull(); } finally { mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_MANAGED_PROFILE, false, - primaryUserHandle); + mainUserHandle); } } @@ -925,23 +924,23 @@ public final class UserManagerTest { @Test public void testCreateProfileForUser_disallowAddUser() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; - final UserHandle primaryUserHandle = asHandle(primaryUserId); - mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, true, primaryUserHandle); + final int mainUserId = mUserManager.getMainUser().getIdentifier(); + final UserHandle mainUserHandle = asHandle(mainUserId); + mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, true, mainUserHandle); try { UserInfo userInfo = createProfileForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNotNull(); } finally { mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, false, - primaryUserHandle); + mainUserHandle); } } @MediumTest @Test public void testAddRestrictedProfile() throws Exception { - if (isAutomotive()) return; + if (isAutomotive() || UserManager.isHeadlessSystemUserMode()) return; assertWithMessage("There should be no associated restricted profiles before the test") .that(mUserManager.hasRestrictedProfiles()).isFalse(); UserInfo userInfo = createRestrictedProfile("Profile"); @@ -973,10 +972,10 @@ public final class UserManagerTest { @Test public void testGetManagedProfileCreationTime() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); final long startTime = System.currentTimeMillis(); UserInfo profile = createProfileForUser("Managed 1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); final long endTime = System.currentTimeMillis(); assertThat(profile).isNotNull(); if (System.currentTimeMillis() > EPOCH_PLUS_30_YEARS) { @@ -989,8 +988,8 @@ public final class UserManagerTest { assertThat(mUserManager.getUserCreationTime(asHandle(profile.id))) .isEqualTo(profile.creationTime); - long ownerCreationTime = mUserManager.getUserInfo(primaryUserId).creationTime; - assertThat(mUserManager.getUserCreationTime(asHandle(primaryUserId))) + long ownerCreationTime = mUserManager.getUserInfo(mainUserId).creationTime; + assertThat(mUserManager.getUserCreationTime(asHandle(mainUserId))) .isEqualTo(ownerCreationTime); } @@ -1226,14 +1225,14 @@ public final class UserManagerTest { @Test public void testCreateProfile_withContextUserId() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userProfile = createProfileForUser("Managed 1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userProfile).isNotNull(); UserManager um = (UserManager) mContext.createPackageContextAsUser( - "android", 0, mUserManager.getPrimaryUser().getUserHandle()) + "android", 0, mUserManager.getMainUser()) .getSystemService(Context.USER_SERVICE); List<UserHandle> profiles = um.getAllProfiles(); @@ -1245,10 +1244,10 @@ public final class UserManagerTest { @Test public void testSetUserName_withContextUserId() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo1 = createProfileForUser("Managed 1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo1).isNotNull(); UserManager um = (UserManager) mContext.createPackageContextAsUser( @@ -1294,10 +1293,10 @@ public final class UserManagerTest { @Test public void testGetUserIcon_withContextUserId() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo1 = createProfileForUser("Managed 1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo1).isNotNull(); UserManager um = (UserManager) mContext.createPackageContextAsUser( diff --git a/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java b/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java index 856df359b326..50040b70c47e 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java @@ -62,6 +62,15 @@ public class FakeTimeDetectorStrategy implements TimeDetectorStrategy { } @Override + public NetworkTimeSuggestion getLatestNetworkSuggestion() { + return null; + } + + @Override + public void clearLatestNetworkSuggestion() { + } + + @Override public void suggestGnssTime(GnssTimeSuggestion timeSuggestion) { } diff --git a/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java new file mode 100644 index 000000000000..08d08b65288a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java @@ -0,0 +1,439 @@ +/* + * 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.timedetector; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.time.UnixEpochTime; +import android.net.Network; +import android.util.NtpTrustedTime; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.timedetector.NetworkTimeUpdateService.Engine.RefreshCallbacks; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetSocketAddress; +import java.util.function.Supplier; + +@RunWith(AndroidJUnit4.class) +public class NetworkTimeUpdateServiceTest { + + private static final InetSocketAddress FAKE_SERVER_ADDRESS = + InetSocketAddress.createUnresolved("test", 123); + private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 100000000L; + private static final long ARBITRARY_UNIX_EPOCH_TIME_MILLIS = 5555555555L; + private static final int ARBITRARY_UNCERTAINTY_MILLIS = 999; + + private FakeElapsedRealtimeClock mFakeElapsedRealtimeClock; + private NtpTrustedTime mMockNtpTrustedTime; + private Network mDummyNetwork; + + @Before + public void setUp() { + mFakeElapsedRealtimeClock = new FakeElapsedRealtimeClock(); + mMockNtpTrustedTime = mock(NtpTrustedTime.class); + mDummyNetwork = mock(Network.class); + } + + @Test + public void engineImpl_refreshIfRequiredAndReschedule_success() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + // Simulated NTP client behavior: No cached time value available initially, then a + // successful refresh. + NtpTrustedTime.TimeResult timeResult = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1); + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect the refresh attempt to have been made. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + // Check everything happened that was supposed to. + long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult( + timeResult, normalPollingIntervalMillis); + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + + NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult); + verify(mockCallback).submitSuggestion(expectedSuggestion); + } + + @Test + public void engineImpl_refreshIfRequiredAndReschedule_failThenFailRepeatedly() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + for (int i = 0; i < tryAgainTimesMax + 1; i++) { + // Simulated NTP client behavior: No cached time value available and failure to refresh. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect a refresh attempt each time: there's no currently cached result. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + // Check everything happened that was supposed to. + long expectedDelayMillis; + if (i < tryAgainTimesMax) { + expectedDelayMillis = shortPollingIntervalMillis; + } else { + expectedDelayMillis = normalPollingIntervalMillis; + } + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + verify(mockCallback, never()).submitSuggestion(any()); + + reset(mMockNtpTrustedTime); + } + } + + @Test + public void engineImpl_refreshIfRequiredAndReschedule_successThenFailRepeatedly() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int maxTimeResultAgeMillis = normalPollingIntervalMillis; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + NtpTrustedTime.TimeResult timeResult = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1); + NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult); + + { + // Simulated NTP client behavior: No cached time value available initially, with a + // successful refresh. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect the refresh attempt to have been made: there is no cached network time + // initially. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult( + timeResult, normalPollingIntervalMillis); + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion); + reset(mMockNtpTrustedTime); + } + + // Increment the current time by enough so that an attempt to refresh the time should be + // made every time refreshIfRequiredAndReschedule() is called. + mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis); + + // Test multiple follow-up calls. + for (int i = 0; i < tryAgainTimesMax + 1; i++) { + // Simulated NTP client behavior: (Too old) cached time value available, unsuccessful + // refresh. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect a refresh attempt each time as the cached network time is too old. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + // Check the scheduling. + long expectedDelayMillis; + if (i < tryAgainTimesMax) { + expectedDelayMillis = shortPollingIntervalMillis; + } else { + expectedDelayMillis = normalPollingIntervalMillis; + } + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + + // No valid time, no suggestion. + verify(mockCallback, never()).submitSuggestion(any()); + + reset(mMockNtpTrustedTime); + } + } + + @Test + public void engineImpl_refreshIfRequiredAndReschedule_successFailSuccess() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int maxTimeResultAgeMillis = normalPollingIntervalMillis; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + NtpTrustedTime.TimeResult timeResult1 = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1); + { + // Simulated NTP client behavior: No cached time value available initially, with a + // successful refresh. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult1); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect the refresh attempt to have been made: there is no cached network time + // initially. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult( + timeResult1, normalPollingIntervalMillis); + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult1); + verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion); + reset(mMockNtpTrustedTime); + } + + // Increment the current time by enough so that the cached time result is too old and an + // attempt to refresh the time should be made every time refreshIfRequiredAndReschedule() is + // called. + mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis); + + { + // Simulated NTP client behavior: (Old) cached time value available initially, with an + // unsuccessful refresh. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult1); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect the refresh attempt to have been made: the timeResult is too old. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + long expectedDelayMillis = shortPollingIntervalMillis; + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + + // No valid time, no suggestion. + verify(mockCallback, never()).submitSuggestion(any()); + reset(mMockNtpTrustedTime); + } + + NtpTrustedTime.TimeResult timeResult2 = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1); + + { + // Simulated NTP client behavior: (Old) cached time value available initially, with a + // successful refresh and a new cached time value. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult1, timeResult2); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect the refresh attempt to have been made: the timeResult is too old. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult( + timeResult2, normalPollingIntervalMillis); + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult2); + verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion); + reset(mMockNtpTrustedTime); + } + } + + /** + * Confirms that if a refreshIfRequiredAndReschedule() call is made, e.g. for reasons besides + * scheduled alerts, and the latest time is not too old, then an NTP refresh won't be attempted. + * A suggestion will still be made. + */ + @Test + public void engineImpl_refreshIfRequiredAndReschedule_noRefreshIfLatestIsNotTooOld() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int maxTimeResultAgeMillis = normalPollingIntervalMillis; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + // Simulated NTP client behavior: A cached time value is available, increment the clock, but + // not enough to consider the cached value too old. + NtpTrustedTime.TimeResult timeResult = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis()); + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult); + mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis - 1); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect no refresh attempt to have been made. + verify(mMockNtpTrustedTime, never()).forceRefresh(any()); + + // The next wake-up should be rescheduled for when the cached time value will become too + // old. + long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(timeResult, + normalPollingIntervalMillis); + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + + // Suggestions must be made every time if the cached time value is not too old in case it + // was refreshed by a different component. + NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult); + verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion); + } + + /** + * Confirms that if a refreshIfRequiredAndReschedule() call is made, e.g. for reasons besides + * scheduled alerts, and the latest time is not too old, then an NTP refresh won't be attempted. + * A suggestion will still be made. + */ + @Test + public void engineImpl_refreshIfRequiredAndReschedule_failureHandlingAfterLatestIsTooOld() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int maxTimeResultAgeMillis = normalPollingIntervalMillis; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + // Simulated NTP client behavior: A cached time value is available, increment the clock, + // enough to consider the cached value too old. The refresh attempt will fail. + NtpTrustedTime.TimeResult timeResult = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis()); + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult); + mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect a refresh attempt to have been made. + verify(mMockNtpTrustedTime, times(1)).forceRefresh(mDummyNetwork); + + // The next wake-up should be rescheduled using the short polling interval. + long expectedDelayMillis = shortPollingIntervalMillis; + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + + // Suggestions should not be made if the cached time value is too old. + verify(mockCallback, never()).submitSuggestion(any()); + } + + private long calculateRefreshDelayMillisForTimeResult(NtpTrustedTime.TimeResult timeResult, + int normalPollingIntervalMillis) { + long currentElapsedRealtimeMillis = mFakeElapsedRealtimeClock.getElapsedRealtimeMillis(); + long timeResultAgeMillis = timeResult.getAgeMillis(currentElapsedRealtimeMillis); + return normalPollingIntervalMillis - timeResultAgeMillis; + } + + private static NetworkTimeSuggestion createExpectedSuggestion( + NtpTrustedTime.TimeResult timeResult) { + UnixEpochTime unixEpochTime = new UnixEpochTime( + timeResult.getElapsedRealtimeMillis(), timeResult.getTimeMillis()); + return new NetworkTimeSuggestion(unixEpochTime, timeResult.getUncertaintyMillis()); + } + + private static NtpTrustedTime.TimeResult createNtpTimeResult(long elapsedRealtimeMillis) { + return new NtpTrustedTime.TimeResult( + ARBITRARY_UNIX_EPOCH_TIME_MILLIS, + elapsedRealtimeMillis, + ARBITRARY_UNCERTAINTY_MILLIS, + FAKE_SERVER_ADDRESS); + } + + private static class FakeElapsedRealtimeClock implements Supplier<Long> { + + private long mElapsedRealtimeMillis; + + public void setElapsedRealtimeMillis(long elapsedRealtimeMillis) { + mElapsedRealtimeMillis = elapsedRealtimeMillis; + } + + public long getElapsedRealtimeMillis() { + return mElapsedRealtimeMillis; + } + + public long incrementMillis(int millis) { + mElapsedRealtimeMillis += millis; + return mElapsedRealtimeMillis; + } + + @Override + public Long get() { + return getElapsedRealtimeMillis(); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java index a8572381208d..0b339ad52eda 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java @@ -380,6 +380,28 @@ public class TimeDetectorServiceTest { } @Test + public void testClearNetworkTime_withoutPermission() { + doThrow(new SecurityException("Mock")) + .when(mMockContext).enforceCallingPermission(anyString(), any()); + + assertThrows(SecurityException.class, + () -> mTimeDetectorService.clearNetworkTime()); + verify(mMockContext).enforceCallingPermission( + eq(android.Manifest.permission.SET_TIME), anyString()); + } + + @Test + public void testClearNetworkTime() throws Exception { + doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); + + mTimeDetectorService.clearNetworkTime(); + + verify(mMockContext).enforceCallingPermission( + eq(android.Manifest.permission.SET_TIME), anyString()); + verify(mFakeTimeDetectorStrategySpy).clearLatestNetworkSuggestion(); + } + + @Test public void testLatestNetworkTime() { NtpTrustedTime.TimeResult latestNetworkTime = new NtpTrustedTime.TimeResult( 1234L, 54321L, 999, InetSocketAddress.createUnresolved("test.timeserver", 123)); diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java index caef4943118b..37da2a28a892 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java @@ -874,6 +874,7 @@ public class TimeDetectorStrategyImplTest { long expectedSystemClockMillis = script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime()); script.simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(timeSuggestion) .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); } @@ -891,10 +892,55 @@ public class TimeDetectorStrategyImplTest { script.simulateTimePassing() .simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(timeSuggestion) .verifySystemClockWasNotSetAndResetCallTracking(); } @Test + public void testClearLatestNetworkSuggestion() { + ConfigurationInternal configInternal = + new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) + .setOriginPriorities(ORIGIN_NETWORK, ORIGIN_EXTERNAL) + .build(); + Script script = new Script().simulateConfigurationInternalChange(configInternal); + + // Create two different time suggestions for the current elapsedRealtimeMillis. + ExternalTimeSuggestion externalTimeSuggestion = + script.generateExternalTimeSuggestion(ARBITRARY_TEST_TIME); + NetworkTimeSuggestion networkTimeSuggestion = + script.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME.plus(Duration.ofHours(5))); + script.simulateTimePassing(); + + // Suggest an external time: This should cause the device to change time. + { + long expectedSystemClockMillis = + script.calculateTimeInMillisForNow(externalTimeSuggestion.getUnixEpochTime()); + script.simulateExternalTimeSuggestion(externalTimeSuggestion) + .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); + } + + // Suggest a network time: This should cause the device to change time because + // network > external. + { + long expectedSystemClockMillis = + script.calculateTimeInMillisForNow(networkTimeSuggestion.getUnixEpochTime()); + script.simulateNetworkTimeSuggestion(networkTimeSuggestion) + .assertLatestNetworkSuggestion(networkTimeSuggestion) + .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); + } + + // Clear the network time. This should cause the device to change back to the external time, + // which is now the best time available. + { + long expectedSystemClockMillis = + script.calculateTimeInMillisForNow(externalTimeSuggestion.getUnixEpochTime()); + script.simulateClearLatestNetworkSuggestion() + .assertLatestNetworkSuggestion(null) + .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); + } + } + + @Test public void testSuggestNetworkTime_rejectedBelowLowerBound() { ConfigurationInternal configInternal = new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) @@ -908,6 +954,7 @@ public class TimeDetectorStrategyImplTest { NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(belowLowerBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(null) .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -926,6 +973,7 @@ public class TimeDetectorStrategyImplTest { NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(aboveLowerBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(timeSuggestion) .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli()); } @@ -944,6 +992,7 @@ public class TimeDetectorStrategyImplTest { NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(aboveUpperBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(null) .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -962,6 +1011,7 @@ public class TimeDetectorStrategyImplTest { NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(belowUpperBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(timeSuggestion) .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli()); } @@ -1745,7 +1795,7 @@ public class TimeDetectorStrategyImplTest { } @Test - public void suggestionsFromNetworkOriginNotInPriorityList_areIgnored() { + public void suggestionsFromNetworkOriginNotInPriorityList_areNotUsed() { ConfigurationInternal configInternal = new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) .setOriginPriorities(ORIGIN_TELEPHONY) @@ -1757,11 +1807,12 @@ public class TimeDetectorStrategyImplTest { script.simulateNetworkTimeSuggestion(timeSuggestion) .assertLatestNetworkSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(timeSuggestion) .verifySystemClockWasNotSetAndResetCallTracking(); } @Test - public void suggestionsFromGnssOriginNotInPriorityList_areIgnored() { + public void suggestionsFromGnssOriginNotInPriorityList_areNotUsed() { ConfigurationInternal configInternal = new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) .setOriginPriorities(ORIGIN_TELEPHONY) @@ -1777,7 +1828,7 @@ public class TimeDetectorStrategyImplTest { } @Test - public void suggestionsFromExternalOriginNotInPriorityList_areIgnored() { + public void suggestionsFromExternalOriginNotInPriorityList_areNotUsed() { ConfigurationInternal configInternal = new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) .setOriginPriorities(ORIGIN_TELEPHONY) @@ -2015,6 +2066,11 @@ public class TimeDetectorStrategyImplTest { return this; } + Script simulateClearLatestNetworkSuggestion() { + mTimeDetectorStrategy.clearLatestNetworkSuggestion(); + return this; + } + Script simulateGnssTimeSuggestion(GnssTimeSuggestion timeSuggestion) { mTimeDetectorStrategy.suggestGnssTime(timeSuggestion); return this; @@ -2056,6 +2112,12 @@ public class TimeDetectorStrategyImplTest { return this; } + /** Calls {@link TimeDetectorStrategy#confirmTime(UnixEpochTime)}. */ + Script simulateConfirmTime(UnixEpochTime confirmationTime, boolean expectedReturnValue) { + assertEquals(expectedReturnValue, mTimeDetectorStrategy.confirmTime(confirmationTime)); + return this; + } + Script verifySystemClockWasNotSetAndResetCallTracking() { mFakeEnvironment.verifySystemClockNotSet(); mFakeEnvironment.resetCallTracking(); @@ -2218,11 +2280,6 @@ public class TimeDetectorStrategyImplTest { long calculateTimeInMillisForNow(UnixEpochTime unixEpochTime) { return unixEpochTime.at(peekElapsedRealtimeMillis()).getUnixEpochTimeMillis(); } - - Script simulateConfirmTime(UnixEpochTime confirmationTime, boolean expectedReturnValue) { - assertEquals(expectedReturnValue, mTimeDetectorStrategy.confirmTime(confirmationTime)); - return this; - } } private static TelephonyTimeSuggestion createTelephonyTimeSuggestion(int slotIndex, diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 4ee87d4b57b5..e02863e2c352 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -58,6 +58,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; +import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; import static android.view.WindowManager.TRANSIT_PIP; @@ -3143,46 +3144,6 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); } - @Test - public void testImeInsetsFrozenFlag_resetWhenReparented() { - final ActivityRecord activity = createActivityWithTask(); - final WindowState app = createWindow(null, TYPE_APPLICATION, activity, "app"); - final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow"); - final Task newTask = new TaskBuilder(mSupervisor).build(); - makeWindowVisible(app, imeWindow); - mDisplayContent.mInputMethodWindow = imeWindow; - mDisplayContent.setImeLayeringTarget(app); - mDisplayContent.setImeInputTarget(app); - - // Simulate app is closing and expect the last IME is shown and IME insets is frozen. - app.mActivityRecord.commitVisibility(false, false); - assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Expect IME insets frozen state will reset when the activity is reparent to the new task. - activity.setState(RESUMED, "test"); - activity.reparent(newTask, 0 /* top */, "test"); - assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - } - - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testImeInsetsFrozenFlag_resetWhenResized() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); - makeWindowVisibleAndDrawn(app, mImeWindow); - mDisplayContent.setImeLayeringTarget(app); - mDisplayContent.setImeInputTarget(app); - - // Simulate app is closing and expect the last IME is shown and IME insets is frozen. - app.mActivityRecord.commitVisibility(false, false); - assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Expect IME insets frozen state will reset when the activity is reparent to the new task. - app.mActivityRecord.onResize(); - assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - } - @SetupWindows(addWindows = W_INPUT_METHOD) @Test public void testImeInsetsFrozenFlag_resetWhenNoImeFocusableInActivity() { @@ -3216,6 +3177,10 @@ public class ActivityRecordTests extends WindowTestsBase { public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_IME).setWindowContainer( + mImeWindow, null, null); + mImeWindow.getControllableInsetProvider().setServerVisible(true); + InsetsSource imeSource = new InsetsSource(ITYPE_IME, ime()); app.mAboveInsetsState.addSource(imeSource); mDisplayContent.setImeLayeringTarget(app); @@ -3233,8 +3198,10 @@ public class ActivityRecordTests extends WindowTestsBase { // Simulate app re-start input or turning screen off/on then unlocked by un-secure // keyguard to back to the app, expect IME insets is not frozen - mDisplayContent.updateImeInputAndControlTarget(app); app.mActivityRecord.commitVisibility(true, false); + mDisplayContent.updateImeInputAndControlTarget(app); + mDisplayContent.mWmService.mRoot.performSurfacePlacement(); + assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); imeSource.setVisible(true); @@ -3274,12 +3241,12 @@ public class ActivityRecordTests extends WindowTestsBase { assertTrue(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); // Simulate switching to app2 to make it visible to be IME targets. - makeWindowVisibleAndDrawn(app2); spyOn(app2); spyOn(app2.mClient); ArgumentCaptor<InsetsState> insetsStateCaptor = ArgumentCaptor.forClass(InsetsState.class); doReturn(true).when(app2).isReadyToDispatchInsetsState(); mDisplayContent.setImeLayeringTarget(app2); + app2.mActivityRecord.commitVisibility(true, false); mDisplayContent.updateImeInputAndControlTarget(app2); mDisplayContent.mWmService.mRoot.performSurfacePlacement(); @@ -3293,6 +3260,57 @@ public class ActivityRecordTests extends WindowTestsBase { } @Test + public void testImeInsetsFrozenFlag_multiWindowActivities() { + final WindowToken imeToken = createTestWindowToken(TYPE_INPUT_METHOD, mDisplayContent); + final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, imeToken, "ime"); + makeWindowVisibleAndDrawn(ime); + + // Create a split-screen root task with activity1 and activity 2. + final Task task = new TaskBuilder(mSupervisor) + .setCreateParentTask(true).setCreateActivity(true).build(); + task.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + final ActivityRecord activity1 = task.getTopNonFinishingActivity(); + activity1.getTask().setResumedActivity(activity1, "testApp1"); + + final ActivityRecord activity2 = new TaskBuilder(mSupervisor) + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) + .setCreateActivity(true).build().getTopMostActivity(); + activity2.getTask().setResumedActivity(activity2, "testApp2"); + activity2.getTask().setParent(task.getRootTask()); + + // Simulate activity1 and activity2 both have set mImeInsetsFrozenUntilStartInput when + // invisible to user. + activity1.mImeInsetsFrozenUntilStartInput = true; + activity2.mImeInsetsFrozenUntilStartInput = true; + + final WindowState app1 = createWindow(null, TYPE_APPLICATION, activity1, "app1"); + final WindowState app2 = createWindow(null, TYPE_APPLICATION, activity2, "app2"); + makeWindowVisibleAndDrawn(app1, app2); + + final InsetsStateController controller = mDisplayContent.getInsetsStateController(); + controller.getSourceProvider(ITYPE_IME).setWindowContainer( + ime, null, null); + ime.getControllableInsetProvider().setServerVisible(true); + + // app1 starts input and expect IME insets for all activities in split-screen will be + // frozen until the input started. + mDisplayContent.setImeLayeringTarget(app1); + mDisplayContent.updateImeInputAndControlTarget(app1); + mDisplayContent.mWmService.mRoot.performSurfacePlacement(); + + assertEquals(app1, mDisplayContent.getImeInputTarget()); + assertFalse(activity1.mImeInsetsFrozenUntilStartInput); + assertFalse(activity2.mImeInsetsFrozenUntilStartInput); + + app1.setRequestedVisibleTypes(ime()); + controller.onInsetsModified(app1); + + // Expect all activities in split-screen will get IME insets visible state + assertTrue(app1.getInsetsState().peekSource(ITYPE_IME).isVisible()); + assertTrue(app2.getInsetsState().peekSource(ITYPE_IME).isVisible()); + } + + @Test public void testInClosingAnimation_visibilityNotCommitted_doNotHideSurface() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); makeWindowVisibleAndDrawn(app); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index 8bb79e3f7ddc..45b30b204801 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -155,6 +155,18 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test + public void testTreatmentDisabledPerApp_noForceRotationOrRefresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.mLetterboxUiController.shouldForceRotateForCameraCompat()) + .thenReturn(false); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertNoForceRotationOrRefresh(); + } + + @Test public void testMultiWindowMode_returnUnspecified_noForceRotationOrRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mDisplayContent); @@ -327,7 +339,21 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test - public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception { + public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.mLetterboxUiController.shouldRefreshActivityForCameraCompat()) + .thenReturn(false); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertActivityRefreshRequested(/* refreshRequested */ false); + } + + @Test + public void testOnActivityConfigurationChanging_refreshDisabledPerApp_noRefresh() + throws Exception { when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false); configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -362,6 +388,19 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); } + @Test + public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.mLetterboxUiController.shouldRefreshActivityViaPauseForCameraCompat()) + .thenReturn(true); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); + } + private void configureActivity(@ScreenOrientation int activityOrientation) { configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT); } diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 6d778afee88c..5e087f06b36b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -16,8 +16,14 @@ package com.android.server.wm; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -74,6 +80,8 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mController = new LetterboxUiController(mWm, mActivity); } + // shouldIgnoreRequestedOrientation + @Test @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION}) public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() { @@ -134,7 +142,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { } @Test - @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION}) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH}) public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() { prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch(); doReturn(false).when(mLetterboxConfiguration) @@ -143,6 +151,163 @@ public class LetterboxUiControllerTest extends WindowTestsBase { assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED)); } + // shouldRefreshActivityForCameraCompat + + @Test + public void testShouldRefreshActivityForCameraCompat_flagIsDisabled_returnsFalse() { + doReturn(false).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH}) + public void testShouldRefreshActivityForCameraCompat_overrideEnabled_returnsFalse() { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH}) + public void testShouldRefreshActivityForCameraCompat_propertyIsTrueAndOverride_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + public void testShouldRefreshActivityForCameraCompat_propertyIsFalse_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + public void testShouldRefreshActivityForCameraCompat_propertyIsTrue_returnsTrue() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldRefreshActivityForCameraCompat()); + } + + // shouldRefreshActivityViaPauseForCameraCompat + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE}) + public void testShouldRefreshActivityViaPauseForCameraCompat_flagIsDisabled_returnsFalse() { + doReturn(false).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE}) + public void testShouldRefreshActivityViaPauseForCameraCompat_overrideEnabled_returnsTrue() { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE}) + public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsFalseAndOverride_returnFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + @Test + public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsTrue_returnsTrue() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + // shouldForceRotateForCameraCompat + + @Test + public void testShouldForceRotateForCameraCompat_flagIsDisabled_returnsFalse() { + doReturn(false).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION}) + public void testShouldForceRotateForCameraCompat_overrideEnabled_returnsFalse() { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION}) + public void testShouldForceRotateForCameraCompat_propertyIsTrueAndOverride_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + public void testShouldForceRotateForCameraCompat_propertyIsFalse_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + public void testShouldForceRotateForCameraCompat_propertyIsTrue_returnsTrue() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldForceRotateForCameraCompat()); + } + private void mockThatProperty(String propertyName, boolean value) throws Exception { Property property = new Property(propertyName, /* value */ value, /* packageName */ "", /* className */ ""); 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 656c48659383..035d73dc1c5c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -23,7 +23,12 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE; @@ -36,13 +41,6 @@ import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -339,11 +337,11 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final Throwable exception = new IllegalArgumentException("Test exception"); mController.onTaskFragmentError(mTaskFragment.getTaskFragmentOrganizer(), - mErrorToken, null /* taskFragment */, HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS, + mErrorToken, null /* taskFragment */, OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS, exception); mController.dispatchPendingEvents(); - assertTaskFragmentErrorTransaction(HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS, + assertTaskFragmentErrorTransaction(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS, exception.getClass()); } @@ -519,50 +517,20 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @Test public void testApplyTransaction_enforceHierarchyChange_deleteTaskFragment() { doReturn(true).when(mTaskFragment).isAttached(); - - // Throw exception if the transaction is trying to change a window that is not organized by - // the organizer. - mTransaction.deleteTaskFragment(mFragmentWindowToken); - - assertApplyTransactionDisallowed(mTransaction); - - // Allow transaction to change a TaskFragment created by the organizer. - mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, - "Test:TaskFragmentOrganizer" /* processName */); - clearInvocations(mAtm.mRootWindowContainer); - - assertApplyTransactionAllowed(mTransaction); - - // No lifecycle update when the TaskFragment is not recorded. - verify(mAtm.mRootWindowContainer, never()).resumeFocusedTasksTopActivities(); - mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); - assertApplyTransactionAllowed(mTransaction); - - verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); - } - - @Test - public void testApplyTransaction_enforceHierarchyChange_setAdjacentRoots() { - final TaskFragment taskFragment2 = - new TaskFragment(mAtm, new Binder(), true /* createdByOrganizer */); - final WindowContainerToken token2 = taskFragment2.mRemoteToken.toWindowContainerToken(); // Throw exception if the transaction is trying to change a window that is not organized by // the organizer. - mTransaction.setAdjacentRoots(mFragmentWindowToken, token2); + mTransaction.deleteTaskFragment(mFragmentToken); assertApplyTransactionDisallowed(mTransaction); // Allow transaction to change a TaskFragment created by the organizer. mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); - taskFragment2.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, - "Test:TaskFragmentOrganizer" /* processName */); clearInvocations(mAtm.mRootWindowContainer); assertApplyTransactionAllowed(mTransaction); - verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); } @@ -577,6 +545,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { mTransaction.reparentActivityToTaskFragment(mFragmentToken, mock(IBinder.class)); mTransaction.setAdjacentTaskFragments(mFragmentToken, mock(IBinder.class), null /* options */); + mTransaction.clearAdjacentTaskFragments(mFragmentToken); assertApplyTransactionAllowed(mTransaction); // Successfully created a TaskFragment. @@ -651,12 +620,16 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Not allowed because TaskFragments are not organized by the caller organizer. assertApplyTransactionDisallowed(mTransaction); + assertNull(mTaskFragment.getAdjacentTaskFragment()); + assertNull(taskFragment2.getAdjacentTaskFragment()); mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); // Not allowed because TaskFragment2 is not organized by the caller organizer. assertApplyTransactionDisallowed(mTransaction); + assertNull(mTaskFragment.getAdjacentTaskFragment()); + assertNull(taskFragment2.getAdjacentTaskFragment()); mTaskFragment.onTaskFragmentOrganizerRemoved(); taskFragment2.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, @@ -664,11 +637,46 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Not allowed because mTaskFragment is not organized by the caller organizer. assertApplyTransactionDisallowed(mTransaction); + assertNull(mTaskFragment.getAdjacentTaskFragment()); + assertNull(taskFragment2.getAdjacentTaskFragment()); mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); assertApplyTransactionAllowed(mTransaction); + assertEquals(taskFragment2, mTaskFragment.getAdjacentTaskFragment()); + } + + @Test + public void testApplyTransaction_enforceTaskFragmentOrganized_clearAdjacentTaskFragments() { + final Task task = createTask(mDisplayContent); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + final IBinder fragmentToken2 = new Binder(); + final TaskFragment taskFragment2 = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(fragmentToken2) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(fragmentToken2, taskFragment2); + mTaskFragment.setAdjacentTaskFragment(taskFragment2); + + mTransaction.clearAdjacentTaskFragments(mFragmentToken); + mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, + false /* shouldApplyIndependently */); + + // Not allowed because TaskFragment is not organized by the caller organizer. + assertApplyTransactionDisallowed(mTransaction); + assertEquals(taskFragment2, mTaskFragment.getAdjacentTaskFragment()); + + mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, + "Test:TaskFragmentOrganizer" /* processName */); + + assertApplyTransactionAllowed(mTransaction); + assertNull(mTaskFragment.getAdjacentTaskFragment()); + assertNull(taskFragment2.getAdjacentTaskFragment()); } @Test @@ -693,7 +701,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test - public void testApplyTransaction_enforceTaskFragmentOrganized_setTaskFragmentOperation() { + public void testApplyTransaction_enforceTaskFragmentOrganized_addTaskFragmentOperation() { final Task task = createTask(mDisplayContent); mTaskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) @@ -704,7 +712,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { OP_TYPE_SET_ANIMATION_PARAMS) .setAnimationParams(TaskFragmentAnimationParams.DEFAULT) .build(); - mTransaction.setTaskFragmentOperation(mFragmentToken, operation); + mTransaction.addTaskFragmentOperation(mFragmentToken, operation); mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, false /* shouldApplyIndependently */); @@ -718,7 +726,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test - public void testSetTaskFragmentOperation() { + public void testAddTaskFragmentOperation() { final Task task = createTask(mDisplayContent); mTaskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) @@ -736,7 +744,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { OP_TYPE_SET_ANIMATION_PARAMS) .setAnimationParams(animationParams) .build(); - mTransaction.setTaskFragmentOperation(mFragmentToken, operation); + mTransaction.addTaskFragmentOperation(mFragmentToken, operation); mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, false /* shouldApplyIndependently */); assertApplyTransactionAllowed(mTransaction); @@ -845,26 +853,6 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test - public void testApplyTransaction_enforceHierarchyChange_reparentChildren() { - doReturn(true).when(mTaskFragment).isAttached(); - - // Throw exception if the transaction is trying to change a window that is not organized by - // the organizer. - mTransaction.reparentChildren(mFragmentWindowToken, null /* newParent */); - - assertApplyTransactionDisallowed(mTransaction); - - // Allow transaction to change a TaskFragment created by the organizer. - mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, - "Test:TaskFragmentOrganizer" /* processName */); - clearInvocations(mAtm.mRootWindowContainer); - - assertApplyTransactionAllowed(mTransaction); - - verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); - } - - @Test public void testApplyTransaction_reparentActivityToTaskFragment_triggerLifecycleUpdate() { final Task task = createTask(mDisplayContent); final ActivityRecord activity = createActivityRecord(task); @@ -1040,7 +1028,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { any(), any(), anyInt(), anyInt(), any()); verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), eq(mErrorToken), eq(mTaskFragment), - eq(HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT), + eq(OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT), any(IllegalArgumentException.class)); } @@ -1057,7 +1045,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), eq(mErrorToken), eq(mTaskFragment), - eq(HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT), + eq(OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT), any(IllegalArgumentException.class)); assertNull(activity.getOrganizedTaskFragment()); } @@ -1068,14 +1056,12 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { spyOn(mWindowOrganizerController); // Not allow to set adjacent on a TaskFragment that is in a PIP Task. - mTransaction.setAdjacentTaskFragments(mFragmentToken, null /* fragmentToken2 */, - null /* options */) + mTransaction.setAdjacentTaskFragments(mFragmentToken, new Binder(), null /* options */) .setErrorCallbackToken(mErrorToken); assertApplyTransactionAllowed(mTransaction); verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(mErrorToken), eq(mTaskFragment), - eq(HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS), + eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS), any(IllegalArgumentException.class)); verify(mTaskFragment, never()).setAdjacentTaskFragment(any()); } @@ -1094,7 +1080,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertApplyTransactionAllowed(mTransaction); verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(mErrorToken), eq(null), eq(HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT), + eq(mErrorToken), eq(null), eq(OP_TYPE_CREATE_TASK_FRAGMENT), any(IllegalArgumentException.class)); assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); } @@ -1105,12 +1091,12 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { spyOn(mWindowOrganizerController); // Not allow to delete a TaskFragment that is in a PIP Task. - mTransaction.deleteTaskFragment(mFragmentWindowToken) + mTransaction.deleteTaskFragment(mFragmentToken) .setErrorCallbackToken(mErrorToken); assertApplyTransactionAllowed(mTransaction); verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(mErrorToken), eq(mTaskFragment), eq(HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT), + eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_DELETE_TASK_FRAGMENT), any(IllegalArgumentException.class)); assertNotNull(mWindowOrganizerController.getTaskFragment(mFragmentToken)); @@ -1423,43 +1409,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // The pending event will be dispatched on the handler (from requestTraversal). waitHandlerIdle(mWm.mAnimationHandler); - assertTaskFragmentErrorTransaction(HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT, - SecurityException.class); - } - - @Test - public void testMinDimensionViolation_ReparentChildren() { - final Task task = createTask(mDisplayContent); - final IBinder oldFragToken = new Binder(); - final TaskFragment oldTaskFrag = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .createActivityCount(1) - .setFragmentToken(oldFragToken) - .setOrganizer(mOrganizer) - .build(); - final ActivityRecord activity = oldTaskFrag.getTopMostActivity(); - // Make minWidth/minHeight exceeds mTaskFragment bounds. - activity.info.windowLayout = new ActivityInfo.WindowLayout( - 0, 0, 0, 0, 0, mTaskFragBounds.width() + 10, mTaskFragBounds.height() + 10); - mTaskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .setFragmentToken(mFragmentToken) - .setOrganizer(mOrganizer) - .setBounds(mTaskFragBounds) - .build(); - mWindowOrganizerController.mLaunchTaskFragments.put(oldFragToken, oldTaskFrag); - mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); - - // Reparent oldTaskFrag's children to mTaskFragment, which is smaller than activity's - // minimum dimensions. - mTransaction.reparentChildren(oldTaskFrag.mRemoteToken.toWindowContainerToken(), - mTaskFragment.mRemoteToken.toWindowContainerToken()) - .setErrorCallbackToken(mErrorToken); - assertApplyTransactionAllowed(mTransaction); - // The pending event will be dispatched on the handler (from requestTraversal). - waitHandlerIdle(mWm.mAnimationHandler); - - assertTaskFragmentErrorTransaction(HIERARCHY_OP_TYPE_REPARENT_CHILDREN, + assertTaskFragmentErrorTransaction(OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT, SecurityException.class); } @@ -1634,7 +1584,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } /** Asserts that there will be a transaction for TaskFragment error. */ - private void assertTaskFragmentErrorTransaction(int opType, @NonNull Class<?> exceptionClass) { + private void assertTaskFragmentErrorTransaction(@TaskFragmentOperation.OperationType int opType, + @NonNull Class<?> exceptionClass) { verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index a100b9aced21..ef2b691bac0c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -393,7 +393,7 @@ public class WallpaperControllerTests extends WindowTestsBase { dc.updateOrientation(); dc.sendNewConfiguration(); spyOn(wallpaperWindow); - doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getLastReportedBounds(); + doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getParentFrame(); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 5e0e2092f84f..6bce9594571f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -45,6 +45,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; +import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; @@ -980,6 +981,19 @@ public class WindowStateTests extends WindowTestsBase { assertFalse(sameTokenWindow.needsRelativeLayeringToIme()); } + @UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD}) + @Test + public void testNeedsRelativeLayeringToIme_systemDialog() { + WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, + mDisplayContent, + "SystemDialog", true); + mDisplayContent.setImeLayeringTarget(mAppWindow); + mAppWindow.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + makeWindowVisible(mImeWindow); + systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; + assertTrue(systemDialogWindow.needsRelativeLayeringToIme()); + } + @Test public void testSetFreezeInsetsState() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java index a95fa5a4e978..8c581580cf75 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; 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.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; @@ -31,6 +32,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; +import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; @@ -543,4 +545,28 @@ public class ZOrderingTests extends WindowTestsBase { assertZOrderGreaterThan(mTransaction, popupWindow.getSurfaceControl(), mDisplayContent.getImeContainer().getSurfaceControl()); } + + @Test + public void testSystemDialogWindow_expectHigherThanIme_inMultiWindow() { + // Simulate the app window is in multi windowing mode and being IME target + mAppWindow.getConfiguration().windowConfiguration.setWindowingMode( + WINDOWING_MODE_MULTI_WINDOW); + mDisplayContent.setImeLayeringTarget(mAppWindow); + mDisplayContent.setImeInputTarget(mAppWindow); + makeWindowVisible(mImeWindow); + + // Create a popupWindow + final WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, + mDisplayContent, "SystemDialog", true); + systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; + spyOn(systemDialogWindow); + + mDisplayContent.assignChildLayers(mTransaction); + + // Verify the surface layer of the popupWindow should higher than IME + verify(systemDialogWindow).needsRelativeLayeringToIme(); + assertThat(systemDialogWindow.needsRelativeLayeringToIme()).isTrue(); + assertZOrderGreaterThan(mTransaction, systemDialogWindow.getSurfaceControl(), + mDisplayContent.getImeContainer().getSurfaceControl()); + } } diff --git a/telephony/java/android/service/euicc/EuiccService.java b/telephony/java/android/service/euicc/EuiccService.java index e19117bc805f..2c0087eaabf4 100644 --- a/telephony/java/android/service/euicc/EuiccService.java +++ b/telephony/java/android/service/euicc/EuiccService.java @@ -490,6 +490,28 @@ public abstract class EuiccService extends Service { int slotId, DownloadableSubscription subscription, boolean forceDeactivateSim); /** + * Populate {@link DownloadableSubscription} metadata for the given downloadable subscription. + * + * @param slotId ID of the SIM slot to use for the operation. + * @param portIndex Index of the port from the slot. portIndex is used if the eUICC must + * be activated to perform the operation. + * @param subscription A subscription whose metadata needs to be populated. + * @param forceDeactivateSim If true, and if an active SIM must be deactivated to access the + * eUICC, perform this action automatically. Otherwise, {@link #RESULT_MUST_DEACTIVATE_SIM} + * should be returned to allow the user to consent to this operation first. + * @return The result of the operation. + * @see android.telephony.euicc.EuiccManager#getDownloadableSubscriptionMetadata + */ + @NonNull + public GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata( + int slotId, int portIndex, @NonNull DownloadableSubscription subscription, + boolean forceDeactivateSim) { + // stub implementation, LPA needs to implement this + throw new UnsupportedOperationException( + "LPA must override onGetDownloadableSubscriptionMetadata"); + } + + /** * Return metadata for subscriptions which are available for download for this device. * * @param slotId ID of the SIM slot to use for the operation. @@ -833,16 +855,31 @@ public abstract class EuiccService extends Service { } @Override - public void getDownloadableSubscriptionMetadata(int slotId, + public void getDownloadableSubscriptionMetadata(int slotId, int portIndex, DownloadableSubscription subscription, - boolean forceDeactivateSim, + boolean switchAfterDownload, boolean forceDeactivateSim, IGetDownloadableSubscriptionMetadataCallback callback) { mExecutor.execute(new Runnable() { @Override public void run() { - GetDownloadableSubscriptionMetadataResult result = - EuiccService.this.onGetDownloadableSubscriptionMetadata( + GetDownloadableSubscriptionMetadataResult result; + if (switchAfterDownload) { + try { + result = EuiccService.this.onGetDownloadableSubscriptionMetadata( + slotId, portIndex, subscription, forceDeactivateSim); + } catch (UnsupportedOperationException | AbstractMethodError e) { + Log.w(TAG, "The new onGetDownloadableSubscriptionMetadata(int, int, " + + "DownloadableSubscription, boolean) is not implemented." + + " Fall back to the old one.", e); + result = EuiccService.this.onGetDownloadableSubscriptionMetadata( slotId, subscription, forceDeactivateSim); + } + } else { + // When switchAfterDownload is false, this operation is port agnostic. + // Call API without portIndex. + result = EuiccService.this.onGetDownloadableSubscriptionMetadata( + slotId, subscription, forceDeactivateSim); + } try { callback.onComplete(result); } catch (RemoteException e) { diff --git a/telephony/java/android/service/euicc/IEuiccService.aidl b/telephony/java/android/service/euicc/IEuiccService.aidl index 6b0397d67015..f8d5ae9ca86d 100644 --- a/telephony/java/android/service/euicc/IEuiccService.aidl +++ b/telephony/java/android/service/euicc/IEuiccService.aidl @@ -38,8 +38,10 @@ oneway interface IEuiccService { void downloadSubscription(int slotId, int portIndex, in DownloadableSubscription subscription, boolean switchAfterDownload, boolean forceDeactivateSim, in Bundle resolvedBundle, in IDownloadSubscriptionCallback callback); - void getDownloadableSubscriptionMetadata(int slotId, in DownloadableSubscription subscription, - boolean forceDeactivateSim, in IGetDownloadableSubscriptionMetadataCallback callback); + void getDownloadableSubscriptionMetadata( + int slotId, int portIndex, in DownloadableSubscription subscription, + boolean switchAfterDownload, boolean forceDeactivateSim, + in IGetDownloadableSubscriptionMetadataCallback callback); void getEid(int slotId, in IGetEidCallback callback); void getOtaStatus(int slotId, in IGetOtaStatusCallback callback); void startOtaIfNecessary(int slotId, in IOtaStatusChangedCallback statusChangedCallback); diff --git a/telephony/java/android/telephony/ModemActivityInfo.java b/telephony/java/android/telephony/ModemActivityInfo.java index 2d0135ae1c99..64b3c0a203e3 100644 --- a/telephony/java/android/telephony/ModemActivityInfo.java +++ b/telephony/java/android/telephony/ModemActivityInfo.java @@ -567,14 +567,14 @@ public final class ModemActivityInfo implements Parcelable { /** @hide */ @TestApi public boolean isEmpty() { - boolean isTxPowerEmpty = false; - boolean isRxPowerEmpty = false; + boolean isTxPowerEmpty = true; + boolean isRxPowerEmpty = true; for (int i = 0; i < getSpecificInfoLength(); i++) { - if (mActivityStatsTechSpecificInfo[i].isTxPowerEmpty()) { - isTxPowerEmpty = true; + if (!mActivityStatsTechSpecificInfo[i].isTxPowerEmpty()) { + isTxPowerEmpty = false; } - if (mActivityStatsTechSpecificInfo[i].isRxPowerEmpty()) { - isRxPowerEmpty = true; + if (!mActivityStatsTechSpecificInfo[i].isRxPowerEmpty()) { + isRxPowerEmpty = false; } } return isTxPowerEmpty diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt index da3c62daccd9..cb530dadc493 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.ime -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.WindowInsets.Type.ime import android.view.WindowInsets.Type.navigationBars @@ -64,16 +63,6 @@ class LaunchAppShowImeAndDialogThemeAppTest(flicker: FlickerTest) : BaseTest(fli transitions { testApp.dismissDialog(wmHelper) } } - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() - /** Checks that [ComponentNameMatcher.IME] layer becomes visible during the transition */ @Presubmit @Test fun imeWindowIsAlwaysVisible() = flicker.imeWindowIsAlwaysVisible() diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt index c2526d3fff58..0e732b550d2e 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt @@ -16,7 +16,7 @@ package com.android.server.wm.flicker.ime -import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest @@ -68,25 +68,11 @@ class OpenImeWindowFromFixedOrientationAppTest(flicker: FlickerTest) : BaseTest( teardown { imeTestApp.exit(wmHelper) } } - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() - - @Postsubmit - @Test - override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() - @Presubmit @Test fun imeWindowBecomesVisible() = flicker.imeWindowBecomesVisible() @Presubmit @Test fun imeLayerBecomesVisible() = flicker.imeLayerBecomesVisible() - @Postsubmit + @FlakyTest(bugId = 240918620) @Test fun snapshotStartingWindowLayerCoversExactlyOnApp() { Assume.assumeFalse(isShellTransitionsEnabled) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt index a6bbf5489663..477ddb3f82c1 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt @@ -17,7 +17,6 @@ package com.android.server.wm.flicker.ime import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest @@ -92,52 +91,10 @@ open class SwitchImeWindowsFromGestureNavTest(flicker: FlickerTest) : BaseTest(f wmHelper.StateSyncBuilder().withFullScreenApp(imeTestApp).waitForAndVerify() } } - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() - - /** {@inheritDoc} */ - @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun statusBarLayerIsVisibleAtStartAndEnd() = - super.statusBarLayerIsVisibleAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - /** {@inheritDoc} */ - @Postsubmit + @FlakyTest(bugId = 265016201) @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() + override fun entireScreenCovered() = super.entireScreenCovered() @Presubmit @Test diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTestCfArm.kt new file mode 100644 index 000000000000..e6cdd1efa798 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTestCfArm.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.quickswitch + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.FlickerTestFactory +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.traces.common.Rect +import com.android.server.wm.traces.common.service.PlatformConsts +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class QuickSwitchBetweenTwoAppsBackTestCfArm(flicker: FlickerTest) : + QuickSwitchBetweenTwoAppsBackTest(flicker) { + companion object { + private var startDisplayBounds = Rect.EMPTY + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTestCfArm.kt new file mode 100644 index 000000000000..aa9adf0116ae --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTestCfArm.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.quickswitch + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.FlickerTestFactory +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.traces.common.Rect +import com.android.server.wm.traces.common.service.PlatformConsts +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class QuickSwitchBetweenTwoAppsForwardTestCfArm(flicker: FlickerTest) : + QuickSwitchBetweenTwoAppsForwardTest(flicker) { + companion object { + private var startDisplayBounds = Rect.EMPTY + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt index df91754765ba..e06a8d6098e4 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt @@ -53,7 +53,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class QuickSwitchFromLauncherTest(flicker: FlickerTest) : BaseTest(flicker) { +open class QuickSwitchFromLauncherTest(flicker: FlickerTest) : BaseTest(flicker) { private val testApp = SimpleAppHelper(instrumentation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTestCfArm.kt new file mode 100644 index 000000000000..8b216035f9f8 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTestCfArm.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.quickswitch + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.FlickerTestFactory +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.traces.common.service.PlatformConsts +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class QuickSwitchFromLauncherTestCfArm(flicker: FlickerTest) : + QuickSwitchFromLauncherTest(flicker) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL), + // TODO: Test with 90 rotation + supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0) + ) + } + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt index 1b2395218159..8b250c334784 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.rotation -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.IwTest import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice @@ -121,11 +120,6 @@ open class ChangeAppRotationTest(flicker: FlickerTest) : RotationTransition(flic rotationLayerAppearsAndVanishesAssertion() } - /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) - @Test - override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() - @Test @IwTest(focusArea = "framework") override fun cujCompleted() { 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 c26485bfb77f..d76c94d1b54f 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 @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.rotation -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.IwTest import android.platform.test.annotations.Presubmit import android.view.WindowManager @@ -201,11 +200,6 @@ open class SeamlessAppRotationTest(flicker: FlickerTest) : RotationTransition(fl flicker.assertEventLog { this.focusDoesNotChange() } } - /** {@inheritDoc} */ - @FlakyTest - @Test - override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() - @Test @IwTest(focusArea = "framework") override fun cujCompleted() { @@ -224,6 +218,7 @@ open class SeamlessAppRotationTest(flicker: FlickerTest) : RotationTransition(fl runAndIgnoreAssumptionViolation { appLayerAlwaysVisible() } runAndIgnoreAssumptionViolation { navBarLayerIsVisibleAtStartAndEnd() } runAndIgnoreAssumptionViolation { navBarWindowIsAlwaysVisible() } + runAndIgnoreAssumptionViolation { navBarLayerPositionAtStartAndEnd() } runAndIgnoreAssumptionViolation { taskBarLayerIsVisibleAtStartAndEnd() } runAndIgnoreAssumptionViolation { taskBarWindowIsAlwaysVisible() } } diff --git a/tests/Input/src/com/android/test/input/InputDeviceTest.java b/tests/Input/src/com/android/test/input/InputDeviceTest.java index 797e818285f9..ddcc8112d6ed 100644 --- a/tests/Input/src/com/android/test/input/InputDeviceTest.java +++ b/tests/Input/src/com/android/test/input/InputDeviceTest.java @@ -69,7 +69,7 @@ public class InputDeviceTest { } private void assertInputDeviceParcelUnparcel(KeyCharacterMap keyCharacterMap) { - final InputDevice device = new InputDevice.Builder() + final InputDevice.Builder deviceBuilder = new InputDevice.Builder() .setId(DEVICE_ID) .setGeneration(42) .setControllerNumber(43) @@ -88,8 +88,20 @@ public class InputDeviceTest { .setHasBattery(true) .setKeyboardLanguageTag("en-US") .setKeyboardLayoutType("qwerty") - .setSupportsUsi(true) - .build(); + .setSupportsUsi(true); + + for (int i = 0; i < 30; i++) { + deviceBuilder.addMotionRange( + MotionEvent.AXIS_GENERIC_1, + InputDevice.SOURCE_UNKNOWN, + i, + i + 1, + i + 2, + i + 3, + i + 4); + } + + final InputDevice device = deviceBuilder.build(); Parcel parcel = Parcel.obtain(); device.writeToParcel(parcel, 0); diff --git a/tests/Internal/src/com/android/internal/app/OWNERS b/tests/Internal/src/com/android/internal/app/OWNERS new file mode 100644 index 000000000000..d55dc78b8c0a --- /dev/null +++ b/tests/Internal/src/com/android/internal/app/OWNERS @@ -0,0 +1,2 @@ +# Locale related test +per-file *Locale* = file:/services/core/java/com/android/server/locales/OWNERS diff --git a/tests/VectorDrawableTest/Android.bp b/tests/VectorDrawableTest/Android.bp index 9da7c5fdbb17..099d874375a1 100644 --- a/tests/VectorDrawableTest/Android.bp +++ b/tests/VectorDrawableTest/Android.bp @@ -26,5 +26,7 @@ package { android_test { name: "VectorDrawableTest", srcs: ["**/*.java"], + // certificate set as platform to allow testing of @hidden APIs + certificate: "platform", platform_apis: true, } diff --git a/tests/VectorDrawableTest/AndroidManifest.xml b/tests/VectorDrawableTest/AndroidManifest.xml index 5334dac57ca2..163e438e0677 100644 --- a/tests/VectorDrawableTest/AndroidManifest.xml +++ b/tests/VectorDrawableTest/AndroidManifest.xml @@ -158,6 +158,15 @@ <category android:name="com.android.test.dynamic.TEST"/> </intent-filter> </activity> + <activity android:name="LottieDrawableTest" + android:label="Lottie test bed" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="com.android.test.dynamic.TEST" /> + </intent-filter> + </activity> </application> </manifest> diff --git a/tests/VectorDrawableTest/res/raw/lottie.json b/tests/VectorDrawableTest/res/raw/lottie.json new file mode 100644 index 000000000000..fea571c6bedb --- /dev/null +++ b/tests/VectorDrawableTest/res/raw/lottie.json @@ -0,0 +1,123 @@ +{ + "v":"4.6.9", + "fr":60, + "ip":0, + "op":200, + "w":800, + "h":600, + "nm":"Loader 1 JSON", + "ddd":0, + + + "layers":[ + { + "ddd":0, + "ind":1, + "ty":4, + "nm":"Custom Path 1", + "ao": 0, + "ip": 0, + "op": 300, + "st": 0, + "sr": 1, + "bm": 0, + "ks": { + "o": { "a":0, "k":100 }, + "r": { "a":1, "k": [ + { "s": [ 0 ], "e": [ 360], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 }, + { "t": 200 } + ] }, + "p": { "a":0, "k":[ 300, 300, 0 ] }, + "a": { "a":0, "k":[ 100, 100, 0 ] }, + "s": { "a":1, "k":[ + { "s": [ 100, 100 ], "e": [ 200, 200 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 }, + { "s": [ 200, 200 ], "e": [ 100, 100 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 100 }, + { "t": 200 } + ] } + }, + + "shapes":[ + { + "ty":"gr", + "it":[ + { + "ty" : "sh", + "nm" : "Path 1", + "ks" : { + "a" : 1, + "k" : [ + { + "s": [ { + "i": [ [ 0, 50 ], [ -50, 0 ], [ 0, -50 ], [ 50, 0 ] ], + "o": [ [ 0, -50 ], [ 50, 0 ], [ 0, 50 ], [ -50, 0 ] ], + "v": [ [ 0, 100 ], [ 100, 0 ], [ 200, 100 ], [ 100, 200 ] ], + "c": true + } ], + "e": [ { + "i": [ [ 50, 50 ], [ -50, 0 ], [ -50, -50 ], [ 50, 50 ] ], + "o": [ [ 50, -50 ], [ 50, 0 ], [ -50, 50 ], [ -50, 50 ] ], + "v": [ [ 0, 100 ], [ 100, 0 ], [ 200, 100 ], [ 100, 200 ] ], + "c": true + } ], + "i": { "x":0.5, "y":0.5 }, + "o": { "x":0.5, "y":0.5 }, + "t": 0 + }, + { + "s": [ { + "i": [ [ 50, 50 ], [ -50, 0 ], [ -50, -50 ], [ 50, 50 ] ], + "o": [ [ 50, -50 ], [ 50, 0 ], [ -50, 50 ], [ -50, 50 ] ], + "v": [ [ 0, 100 ], [ 100, 0 ], [ 200, 100 ], [ 100, 200 ] ], + "c": true + } ], + "e": [ { + "i": [ [ 0, 50 ], [ -50, 0 ], [ 0, -50 ], [ 50, 0 ] ], + "o": [ [ 0, -50 ], [ 50, 0 ], [ 0, 50 ], [ -50, 0 ] ], + "v": [ [ 0, 100 ], [ 100, 0 ], [ 200, 100 ], [ 100, 200 ] ], + "c": true + } ], + "i": { "x":0.5, "y":0.5 }, + "o": { "x":0.5, "y":0.5 }, + "t": 100 + }, + { + "t": 200 + } + ] + } + }, + + { + "ty": "st", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "w" : { "a": 1, "k": [ + { "s": [ 30 ], "e": [ 50 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 }, + { "s": [ 50 ], "e": [ 30 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 100 }, + { "t": 200 } + ] }, + "o" : { "a": 0, "k": 100 }, + "c" : { "a": 1, "k": [ + { "s": [ 0, 1, 0 ], "e": [ 1, 0, 0 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 }, + { "s": [ 1, 0, 0 ], "e": [ 0, 1, 0 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 100 }, + { "t": 200 } + ] } + }, + + { + "ty":"tr", + "p" : { "a":0, "k":[ 0, 0 ] }, + "a" : { "a":0, "k":[ 0, 0 ] }, + "s" : { "a":0, "k":[ 100, 100 ] }, + "r" : { "a":0, "k": 0 }, + "o" : { "a":0, "k":100 }, + "nm": "Transform" + } + ] + } + ] + } + ] + } diff --git a/tests/VectorDrawableTest/src/com/android/test/dynamic/LottieDrawableTest.java b/tests/VectorDrawableTest/src/com/android/test/dynamic/LottieDrawableTest.java new file mode 100644 index 000000000000..05eae7b0e642 --- /dev/null +++ b/tests/VectorDrawableTest/src/com/android/test/dynamic/LottieDrawableTest.java @@ -0,0 +1,76 @@ +/* + * 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.test.dynamic; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.LottieDrawable; +import android.os.Bundle; +import android.view.View; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Scanner; + +@SuppressWarnings({"UnusedDeclaration"}) +public class LottieDrawableTest extends Activity { + private static final String TAG = "LottieDrawableTest"; + static final int BACKGROUND = 0xFFF44336; + + class LottieDrawableView extends View { + private Rect mLottieBounds; + + private LottieDrawable mLottie; + + LottieDrawableView(Context context, InputStream is) { + super(context); + Scanner s = new Scanner(is).useDelimiter("\\A"); + String json = s.hasNext() ? s.next() : ""; + try { + mLottie = LottieDrawable.makeLottieDrawable(json); + } catch (IOException e) { + throw new RuntimeException(TAG + ": error parsing test Lottie"); + } + mLottie.start(); + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawColor(BACKGROUND); + + mLottie.setBounds(mLottieBounds); + mLottie.draw(canvas); + } + + public void setLottieSize(Rect bounds) { + mLottieBounds = bounds; + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + InputStream is = getResources().openRawResource(R.raw.lottie); + + LottieDrawableView view = new LottieDrawableView(this, is); + view.setLottieSize(new Rect(0, 0, 900, 900)); + setContentView(view); + } +} diff --git a/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt index 3d5d01c9b7a0..0ef165f1523b 100644 --- a/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt +++ b/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt @@ -35,6 +35,6 @@ val ENFORCE_PERMISSION_METHODS = listOf( Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission") ) -const val ANNOTATION_PERMISSION_METHOD = "android.content.pm.PermissionMethod" -const val ANNOTATION_PERMISSION_NAME = "android.content.pm.PermissionName" +const val ANNOTATION_PERMISSION_METHOD = "android.annotation.PermissionMethod" +const val ANNOTATION_PERMISSION_NAME = "android.annotation.PermissionName" const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult" |