diff options
526 files changed, 12519 insertions, 3776 deletions
diff --git a/Android.bp b/Android.bp index effd7ce2a63a..d9615991520a 100644 --- a/Android.bp +++ b/Android.bp @@ -580,7 +580,7 @@ metalava_framework_docs_args = "" + "--hide Todo " + "--hide Typo " + "--hide UnavailableSymbol " + - "--manifest $(location core/res/AndroidManifest.xml) " + "--manifest $(location :frameworks-base-core-AndroidManifest.xml) " packages_to_document = [ "android", @@ -617,7 +617,7 @@ stubs_defaults { sdk_version: "none", system_modules: "none", java_version: "1.8", - arg_files: ["core/res/AndroidManifest.xml"], + arg_files: [":frameworks-base-core-AndroidManifest.xml"], aidl: { local_include_dirs: [ "media/aidl", @@ -696,12 +696,3 @@ 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", - ], -} @@ -30,8 +30,11 @@ per-file */TEST_MAPPING = * # Support bulk translation updates per-file */res*/values*/*.xml = byi@google.com, delphij@google.com +per-file **.bp,**.mk = hansson@google.com per-file *.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION} per-file Android.mk = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION} per-file framework-jarjar-rules.txt = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION} per-file TestProtoLibraries.bp = file:platform/platform_testing:/libraries/health/OWNERS per-file TestProtoLibraries.bp = file:platform/tools/tradefederation:/OWNERS + +per-file ZYGOTE_OWNERS = file:/ZYGOTE_OWNERS diff --git a/StubLibraries.bp b/StubLibraries.bp index 38413c22a3d5..b005591980c0 100644 --- a/StubLibraries.bp +++ b/StubLibraries.bp @@ -36,8 +36,8 @@ droidstubs { args: metalava_framework_docs_args, check_api: { current: { - api_file: "core/api/current.txt", - removed_api_file: "core/api/removed.txt", + api_file: ":non-updatable-current.txt", + removed_api_file: ":non-updatable-removed.txt", }, last_released: { api_file: ":android-non-updatable.api.public.latest", @@ -88,8 +88,8 @@ droidstubs { args: metalava_framework_docs_args + priv_apps, check_api: { current: { - api_file: "core/api/system-current.txt", - removed_api_file: "core/api/system-removed.txt", + api_file: ":non-updatable-system-current.txt", + removed_api_file: ":non-updatable-system-removed.txt", }, last_released: { api_file: ":android-non-updatable.api.system.latest", @@ -99,7 +99,7 @@ droidstubs { api_lint: { enabled: true, new_since: ":android.api.system.latest", - baseline_file: "core/api/system-lint-baseline.txt", + baseline_file: ":non-updatable-system-lint-baseline.txt", }, }, dists: [ @@ -127,12 +127,12 @@ droidstubs { args: metalava_framework_docs_args + test + priv_apps_in_stubs, check_api: { current: { - api_file: "core/api/test-current.txt", - removed_api_file: "core/api/test-removed.txt", + api_file: ":non-updatable-test-current.txt", + removed_api_file: ":non-updatable-test-removed.txt", }, api_lint: { enabled: true, - baseline_file: "core/api/test-lint-baseline.txt", + baseline_file: ":non-updatable-test-lint-baseline.txt", }, }, dists: [ @@ -172,8 +172,8 @@ droidstubs { args: metalava_framework_docs_args + priv_apps_in_stubs + module_libs, check_api: { current: { - api_file: "core/api/module-lib-current.txt", - removed_api_file: "core/api/module-lib-removed.txt", + api_file: ":non-updatable-module-lib-current.txt", + removed_api_file: ":non-updatable-module-lib-removed.txt", }, last_released: { api_file: ":android-non-updatable.api.module-lib.latest", @@ -183,7 +183,7 @@ droidstubs { api_lint: { enabled: true, new_since: ":android.api.module-lib.latest", - baseline_file: "core/api/module-lib-lint-baseline.txt", + baseline_file: ":non-updatable-module-lib-lint-baseline.txt", }, }, dists: [ @@ -364,15 +364,15 @@ java_library { java_library { name: "android_system_server_stubs_current", - defaults: ["android_stubs_dists_default"], + defaults: [ + "android.jar_defaults", + "android_stubs_dists_default", + ], srcs: [":services-non-updatable-stubs"], installable: false, static_libs: [ "android_module_lib_stubs_current", ], - sdk_version: "none", - system_modules: "none", - java_version: "1.8", dist: { dir: "apistubs/android/system-server", }, @@ -575,20 +575,7 @@ droidstubs { droidstubs { name: "hwbinder-stubs-docs", - srcs: [ - "core/java/android/os/HidlSupport.java", - "core/java/android/os/HidlMemory.java", - "core/java/android/os/HwBinder.java", - "core/java/android/os/HwBlob.java", - "core/java/android/os/HwParcel.java", - "core/java/android/os/IHwBinder.java", - "core/java/android/os/IHwInterface.java", - "core/java/android/os/DeadObjectException.java", - "core/java/android/os/DeadSystemException.java", - "core/java/android/os/NativeHandle.java", - "core/java/android/os/RemoteException.java", - "core/java/android/util/AndroidException.java", - ], + srcs: [":hwbinder-stubs-srcs"], libs: ["framework-annotations-lib"], installable: false, sdk_version: "core_platform", @@ -610,12 +597,3 @@ 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/ZYGOTE_OWNERS b/ZYGOTE_OWNERS index 90a185b79b52..f6d15e03a892 100644 --- a/ZYGOTE_OWNERS +++ b/ZYGOTE_OWNERS @@ -1,4 +1,3 @@ -calin@google.com chriswailes@google.com maco@google.com narayan@google.com diff --git a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt index fabf8892b544..9482591c65b5 100644 --- a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt +++ b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt @@ -33,7 +33,6 @@ import androidx.test.filters.LargeTest import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.After -import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -132,8 +131,7 @@ class MotionPredictorBenchmark { predictor.record(moveEvent) val predictionTime = eventTime + eventInterval val predicted = predictor.predict(predictionTime.toNanos()) - assertEquals(1, predicted.size) - assertTrue(predicted[0].eventTime <= (predictionTime + offset).toMillis()) + assertTrue(predicted.eventTime <= (predictionTime + offset).toMillis()) } } diff --git a/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java b/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java index 2e44d82ca428..e9c6c1ae0c87 100644 --- a/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java +++ b/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java @@ -76,6 +76,7 @@ public class ImePerfTest extends ImePerfTestBase implements ManualBenchmarkState.CustomizedIterationListener { private static final String TAG = ImePerfTest.class.getSimpleName(); private static final long ANIMATION_NOT_STARTED = -1; + private static final int WAIT_PROCESS_KILL_TIMEOUT_MS = 2000; @Rule public final PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter(); @@ -248,19 +249,18 @@ public class ImePerfTest extends ImePerfTestBase boolean shouldRetry = false; while (shouldRetry || state.keepRunning(measuredTimeNs)) { shouldRetry = false; - killBaselineIme(); + killBaselineImeSync(); try (ImeSession imeSession = new ImeSession(BaselineIme.getName( getInstrumentation().getContext()))) { + if (!mIsTraceStarted) { + startAsyncAtrace(); + } final AtomicReference<CountDownLatch> latchStart = new AtomicReference<>(); final Activity activity = getActivityWithFocus(); setImeListener(activity, latchStart, null /* latchEnd */); latchStart.set(new CountDownLatch(1)); - if (!mIsTraceStarted) { - startAsyncAtrace(); - } - final WindowInsetsController controller = activity.getWindow().getDecorView().getWindowInsetsController(); AtomicLong startTime = new AtomicLong(); @@ -270,6 +270,7 @@ public class ImePerfTest extends ImePerfTestBase }); measuredTimeNs = waitForAnimationStart(latchStart, startTime); + stopAsyncAtraceAndDumpTraces(); if (measuredTimeNs == ANIMATION_NOT_STARTED) { // Animation didn't start within timeout, @@ -285,7 +286,7 @@ public class ImePerfTest extends ImePerfTestBase addResultToState(state); } - private void killBaselineIme() { + private void killBaselineImeSync() { // pidof returns a space separated list of numeric PIDs. String result = SystemUtil.runShellCommand( "pidof com.android.perftests.inputmethod:BaselineIME"); @@ -294,7 +295,13 @@ public class ImePerfTest extends ImePerfTestBase if (TextUtils.isEmpty(pid)) { continue; } - Process.killProcess(Integer.parseInt(pid)); + final int pidToKill = Integer.parseInt(pid); + Process.killProcess(pidToKill); + try { + // Wait kill IME process being settled down. + Process.waitForProcessDeath(pidToKill, WAIT_PROCESS_KILL_TIMEOUT_MS); + } catch (Exception e) { + } } } @@ -381,7 +388,7 @@ public class ImePerfTest extends ImePerfTestBase } } finally { if (mIsTraceStarted) { - stopAsyncAtrace(); + stopAsyncAtraceAndDumpTraces(); } } mActivityRule.finishActivity(); @@ -488,7 +495,7 @@ public class ImePerfTest extends ImePerfTestBase startAsyncAtrace("wm view"); } - private void stopAsyncAtrace() { + private void stopAsyncAtraceAndDumpTraces() { if (!mIsTraceStarted) { return; } @@ -504,6 +511,14 @@ public class ImePerfTest extends ImePerfTestBase } } + private void stopAsyncAtrace() { + if (!mIsTraceStarted) { + return; + } + mIsTraceStarted = false; + getUiAutomation().executeShellCommand("atrace --async_stop"); + } + @Override public void onStart(int iteration) { // Do not capture trace when profiling because the result will be much slower. diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/WindowPerfTestBase.java b/apct-tests/perftests/utils/src/android/perftests/utils/WindowPerfTestBase.java index ca5913701d3b..804baf4e6504 100644 --- a/apct-tests/perftests/utils/src/android/perftests/utils/WindowPerfTestBase.java +++ b/apct-tests/perftests/utils/src/android/perftests/utils/WindowPerfTestBase.java @@ -73,7 +73,7 @@ public class WindowPerfTestBase { } public static void startAsyncAtrace(String tags) { - getUiAutomation().executeShellCommand("atrace -b 32768 --async_start " + tags); + getUiAutomation().executeShellCommand("atrace --async_start -b 32768 -c " + tags); // Avoid atrace isn't ready immediately. SystemClock.sleep(TimeUnit.NANOSECONDS.toMillis(TIME_1_S_IN_NS)); } diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index 79a26597ed24..5de117261085 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -5277,6 +5277,8 @@ public class DeviceIdleController extends SystemService pw.print(" mScreenLocked="); pw.println(mScreenLocked); pw.print(" mNetworkConnected="); pw.println(mNetworkConnected); pw.print(" mCharging="); pw.println(mCharging); + pw.print(" activeEmergencyCall="); + pw.println(mEmergencyCallListener.isEmergencyCallActive()); if (mConstraints.size() != 0) { pw.println(" mConstraints={"); for (int i = 0; i < mConstraints.size(); i++) { diff --git a/api/Android.bp b/api/Android.bp index 78ddc6ac2967..6e82f1c36eff 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -31,10 +31,12 @@ bootstrap_go_package { "blueprint", "soong", "soong-android", + "soong-bp2build", "soong-genrule", "soong-java", ], srcs: ["api.go"], + testSrcs: ["api_test.go"], pluginFor: ["soong_build"], } diff --git a/api/api.go b/api/api.go index 077ab9679ec9..25d97282035e 100644 --- a/api/api.go +++ b/api/api.go @@ -20,6 +20,7 @@ import ( "github.com/google/blueprint/proptools" "android/soong/android" + "android/soong/bazel" "android/soong/genrule" "android/soong/java" ) @@ -30,6 +31,7 @@ const i18n = "i18n.module.public.api" const virtualization = "framework-virtualization" var core_libraries_modules = []string{art, conscrypt, i18n} + // List of modules that are not yet updatable, and hence they can still compile // against hidden APIs. These modules are filtered out when building the // updatable-framework-module-impl (because updatable-framework-module-impl is @@ -59,6 +61,7 @@ type CombinedApisProperties struct { type CombinedApis struct { android.ModuleBase + android.BazelModuleBase properties CombinedApisProperties } @@ -99,6 +102,19 @@ type fgProps struct { Visibility []string } +type Bazel_module struct { + Bp2build_available *bool +} +type bazelProperties struct { + *Bazel_module +} + +var bp2buildNotAvailable = bazelProperties{ + &Bazel_module{ + Bp2build_available: proptools.BoolPtr(false), + }, +} + // Struct to pass parameters for the various merged [current|removed].txt file modules we create. type MergedTxtDefinition struct { // "current.txt" or "removed.txt" @@ -144,7 +160,7 @@ func createMergedTxt(ctx android.LoadHookContext, txt MergedTxtDefinition) { }, } props.Visibility = []string{"//visibility:public"} - ctx.CreateModule(genrule.GenRuleFactory, &props) + ctx.CreateModule(genrule.GenRuleFactory, &props, &bp2buildNotAvailable) } func createMergedAnnotationsFilegroups(ctx android.LoadHookContext, modules, system_server_modules []string) { @@ -174,7 +190,7 @@ func createMergedAnnotationsFilegroups(ctx android.LoadHookContext, modules, sys props := fgProps{} props.Name = proptools.StringPtr(i.name) props.Srcs = createSrcs(i.modules, i.tag) - ctx.CreateModule(android.FileGroupFactory, &props) + ctx.CreateModule(android.FileGroupFactory, &props, &bp2buildNotAvailable) } } @@ -223,7 +239,7 @@ func createFilteredApiVersions(ctx android.LoadHookContext, modules []string) { props.Tools = []string{"api_versions_trimmer"} props.Cmd = proptools.StringPtr("$(location api_versions_trimmer) $(out) $(in)") props.Dists = []android.Dist{{Targets: []string{"sdk"}}} - ctx.CreateModule(genrule.GenRuleFactory, &props) + ctx.CreateModule(genrule.GenRuleFactory, &props, &bp2buildNotAvailable) } } @@ -315,7 +331,7 @@ func createPublicStubsSourceFilegroup(ctx android.LoadHookContext, modules []str props.Name = proptools.StringPtr("all-modules-public-stubs-source") props.Srcs = createSrcs(modules, "{.public.stubs.source}") props.Visibility = []string{"//frameworks/base"} - ctx.CreateModule(android.FileGroupFactory, &props) + ctx.CreateModule(android.FileGroupFactory, &props, &bp2buildNotAvailable) } func createMergedTxts(ctx android.LoadHookContext, bootclasspath, system_server_classpath []string) { @@ -389,9 +405,57 @@ func combinedApisModuleFactory() android.Module { module.AddProperties(&module.properties) android.InitAndroidModule(module) android.AddLoadHook(module, func(ctx android.LoadHookContext) { module.createInternalModules(ctx) }) + android.InitBazelModule(module) return module } +type bazelCombinedApisAttributes struct { + Scope bazel.StringAttribute + Base bazel.LabelAttribute + Deps bazel.LabelListAttribute +} + +// combined_apis bp2build converter +func (a *CombinedApis) ConvertWithBp2build(ctx android.TopDownMutatorContext) { + basePrefix := "non-updatable" + scopeNames := []string{"public", "system", "module-lib", "system-server"} + scopeToSuffix := map[string]string{ + "public": "-current.txt", + "system": "-system-current.txt", + "module-lib": "-module-lib-current.txt", + "system-server": "-system-server-current.txt", + } + + for _, scopeName := range scopeNames{ + suffix := scopeToSuffix[scopeName] + name := a.Name() + suffix + + var scope bazel.StringAttribute + scope.SetValue(scopeName) + + var base bazel.LabelAttribute + base.SetValue(android.BazelLabelForModuleDepSingle(ctx, basePrefix+suffix)) + + var deps bazel.LabelListAttribute + classpath := a.properties.Bootclasspath + if scopeName == "system-server" { + classpath = a.properties.System_server_classpath + } + deps = bazel.MakeLabelListAttribute(android.BazelLabelForModuleDeps(ctx, classpath)) + + attrs := bazelCombinedApisAttributes{ + Scope: scope, + Base: base, + Deps: deps, + } + props := bazel.BazelTargetModuleProperties{ + Rule_class: "merged_txts", + Bzl_load_location: "//build/bazel/rules/java:merged_txts.bzl", + } + ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: name}, &attrs) + } +} + // Various utility methods below. // Creates an array of ":<m><tag>" for each m in <modules>. diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 000000000000..15b695ca0d36 --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,68 @@ +// 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 api + +import ( + "testing" + + "android/soong/android" + "android/soong/bp2build" +) + +func runCombinedApisTestCaseWithRegistrationCtxFunc(t *testing.T, tc bp2build.Bp2buildTestCase, registrationCtxFunc func(ctx android.RegistrationContext)) { + t.Helper() + (&tc).ModuleTypeUnderTest = "combined_apis" + (&tc).ModuleTypeUnderTestFactory = combinedApisModuleFactory + bp2build.RunBp2BuildTestCase(t, registrationCtxFunc, tc) +} + +func runCombinedApisTestCase(t *testing.T, tc bp2build.Bp2buildTestCase) { + t.Helper() + runCombinedApisTestCaseWithRegistrationCtxFunc(t, tc, func(ctx android.RegistrationContext) {}) +} + +func TestCombinedApisGeneral(t *testing.T) { + runCombinedApisTestCase(t, bp2build.Bp2buildTestCase{ + Description: "combined_apis, general case", + Blueprint: `combined_apis { + name: "foo", + bootclasspath: ["bcp"], + system_server_classpath: ["ssc"], +} +`, + ExpectedBazelTargets: []string{ + bp2build.MakeBazelTargetNoRestrictions("merged_txts", "foo-current.txt", bp2build.AttrNameToString{ + "scope": `"public"`, + "base": `":non-updatable-current.txt__BP2BUILD__MISSING__DEP"`, + "deps": `[":bcp__BP2BUILD__MISSING__DEP"]`, + }), + bp2build.MakeBazelTargetNoRestrictions("merged_txts", "foo-system-current.txt", bp2build.AttrNameToString{ + "scope": `"system"`, + "base": `":non-updatable-system-current.txt__BP2BUILD__MISSING__DEP"`, + "deps": `[":bcp__BP2BUILD__MISSING__DEP"]`, + }), + bp2build.MakeBazelTargetNoRestrictions("merged_txts", "foo-module-lib-current.txt", bp2build.AttrNameToString{ + "scope": `"module-lib"`, + "base": `":non-updatable-module-lib-current.txt__BP2BUILD__MISSING__DEP"`, + "deps": `[":bcp__BP2BUILD__MISSING__DEP"]`, + }), + bp2build.MakeBazelTargetNoRestrictions("merged_txts", "foo-system-server-current.txt", bp2build.AttrNameToString{ + "scope": `"system-server"`, + "base": `":non-updatable-system-server-current.txt__BP2BUILD__MISSING__DEP"`, + "deps": `[":ssc__BP2BUILD__MISSING__DEP"]`, + }), + }, + }) +} diff --git a/core/api/Android.bp b/core/api/Android.bp index 114a957674ae..71a2ca2903f6 100644 --- a/core/api/Android.bp +++ b/core/api/Android.bp @@ -13,7 +13,10 @@ // limitations under the License. package { - default_visibility: ["//visibility:private"], + default_visibility: [ + "//frameworks/base", + "//frameworks/base/api", + ], // See: http://go/android-license-faq // A large-scale-change added 'default_applicable_licenses' to import // all of the 'license_kinds' from "frameworks_base_license" @@ -27,31 +30,33 @@ package { filegroup { name: "non-updatable-current.txt", srcs: ["current.txt"], - visibility: ["//frameworks/base/api"], } filegroup { name: "non-updatable-removed.txt", srcs: ["removed.txt"], - visibility: ["//frameworks/base/api"], } filegroup { name: "non-updatable-system-current.txt", srcs: ["system-current.txt"], - visibility: ["//frameworks/base/api"], } filegroup { name: "non-updatable-system-removed.txt", srcs: ["system-removed.txt"], - visibility: ["//frameworks/base/api"], +} + +filegroup { + name: "non-updatable-system-lint-baseline.txt", + srcs: ["system-lint-baseline.txt"], } filegroup { name: "non-updatable-module-lib-current.txt", srcs: ["module-lib-current.txt"], visibility: [ + "//frameworks/base", "//frameworks/base/api", "//cts/tests/signature/api", ], @@ -61,7 +66,46 @@ filegroup { name: "non-updatable-module-lib-removed.txt", srcs: ["module-lib-removed.txt"], visibility: [ + "//frameworks/base", "//frameworks/base/api", "//cts/tests/signature/api", ], } + +filegroup { + name: "non-updatable-module-lib-lint-baseline.txt", + srcs: ["module-lib-lint-baseline.txt"], +} + +filegroup { + name: "non-updatable-test-current.txt", + srcs: ["test-current.txt"], +} + +filegroup { + name: "non-updatable-test-removed.txt", + srcs: ["test-removed.txt"], +} + +filegroup { + name: "non-updatable-test-lint-baseline.txt", + srcs: ["test-lint-baseline.txt"], +} + +java_api_contribution { + name: "api-stubs-docs-non-updatable-public-stubs", + api_surface: "public", + api_file: "current.txt", + visibility: [ + "//build/orchestrator/apis", + ], +} + +java_api_contribution { + name: "frameworks-base-core-api-module-lib-stubs", + api_surface: "module-lib", + api_file: "module-lib-current.txt", + visibility: [ + "//build/orchestrator/apis", + ], +} diff --git a/core/api/current.txt b/core/api/current.txt index 2b1cb0e4ab65..211f60350cfa 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4492,8 +4492,8 @@ package android.app { method public void openOptionsMenu(); method public void overrideActivityTransition(int, @AnimRes int, @AnimRes int); method public void overrideActivityTransition(int, @AnimRes int, @AnimRes int, @ColorInt int); - method public void overridePendingTransition(int, int); - method public void overridePendingTransition(int, int, int); + method @Deprecated public void overridePendingTransition(int, int); + method @Deprecated public void overridePendingTransition(int, int, int); method public void postponeEnterTransition(); method public void recreate(); method public void registerActivityLifecycleCallbacks(@NonNull android.app.Application.ActivityLifecycleCallbacks); @@ -10333,7 +10333,7 @@ package android.content { field public static final int BIND_AUTO_CREATE = 1; // 0x1 field public static final int BIND_DEBUG_UNBIND = 2; // 0x2 field public static final int BIND_EXTERNAL_SERVICE = -2147483648; // 0x80000000 - field public static final long BIND_EXTERNAL_SERVICE_LONG = -9223372036854775808L; // 0x8000000000000000L + field public static final long BIND_EXTERNAL_SERVICE_LONG = 4611686018427387904L; // 0x4000000000000000L field public static final int BIND_IMPORTANT = 64; // 0x40 field public static final int BIND_INCLUDE_CAPABILITIES = 4096; // 0x1000 field public static final int BIND_NOT_FOREGROUND = 4; // 0x4 @@ -10441,7 +10441,6 @@ package android.content { } public static final class Context.BindServiceFlags { - method public long getValue(); method @NonNull public static android.content.Context.BindServiceFlags of(long); } @@ -15485,14 +15484,14 @@ package android.graphics { method @NonNull public float getMinDisplayRatioForHdrTransition(); method @NonNull public float[] getRatioMax(); method @NonNull public float[] getRatioMin(); - method @NonNull public void setDisplayRatioForFullHdr(float); - method @NonNull public void setEpsilonHdr(float, float, float); - method @NonNull public void setEpsilonSdr(float, float, float); + method public void setDisplayRatioForFullHdr(@FloatRange(from=1.0f) float); + method public void setEpsilonHdr(float, float, float); + method public void setEpsilonSdr(float, float, float); method public void setGainmapContents(@NonNull android.graphics.Bitmap); - method @NonNull public void setGamma(float, float, float); - method @NonNull public void setMinDisplayRatioForHdrTransition(@FloatRange(from=1.0f) float); - method @NonNull public void setRatioMax(float, float, float); - method @NonNull public void setRatioMin(float, float, float); + method public void setGamma(float, float, float); + method public void setMinDisplayRatioForHdrTransition(@FloatRange(from=1.0f) float); + method public void setRatioMax(float, float, float); + method public void setRatioMin(float, float, float); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.graphics.Gainmap> CREATOR; } @@ -19593,7 +19592,7 @@ package android.hardware.display { public final class VirtualDisplayConfig implements android.os.Parcelable { method public int describeContents(); method public int getDensityDpi(); - method @NonNull public java.util.List<java.lang.String> getDisplayCategories(); + method @NonNull public java.util.Set<java.lang.String> getDisplayCategories(); method public int getFlags(); method public int getHeight(); method @NonNull public String getName(); @@ -19608,7 +19607,7 @@ package android.hardware.display { ctor public VirtualDisplayConfig.Builder(@NonNull String, @IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int); method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder addDisplayCategory(@NonNull String); method @NonNull public android.hardware.display.VirtualDisplayConfig build(); - method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDisplayCategories(@NonNull java.util.List<java.lang.String>); + method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDisplayCategories(@NonNull java.util.Set<java.lang.String>); method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setFlags(int); method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setRequestedRefreshRate(@FloatRange(from=0.0f) float); method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setSurface(@Nullable android.view.Surface); @@ -27309,6 +27308,7 @@ package android.media.tv { field public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2; // 0x2 field public static final int TIME_SHIFT_STATUS_UNKNOWN = 0; // 0x0 field public static final int TIME_SHIFT_STATUS_UNSUPPORTED = 1; // 0x1 + field public static final String TV_MESSAGE_KEY_STREAM_ID = "android.media.tv.TvInputManager.stream_id"; field public static final String TV_MESSAGE_TYPE_CLOSED_CAPTION = "CC"; field public static final String TV_MESSAGE_TYPE_WATERMARK = "Watermark"; field public static final int VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY = 4; // 0x4 @@ -27437,6 +27437,7 @@ package android.media.tv { method public boolean onTrackballEvent(android.view.MotionEvent); method public abstract boolean onTune(android.net.Uri); method public boolean onTune(android.net.Uri, android.os.Bundle); + method public void onTvMessage(@NonNull String, @NonNull android.os.Bundle); method public void onUnblockContent(android.media.tv.TvContentRating); method public void setOverlayViewEnabled(boolean); } @@ -36890,6 +36891,7 @@ package android.provider { method public String[] getDocumentStreamTypes(String, String); method public String getDocumentType(String) throws java.io.FileNotFoundException; method public final String getType(android.net.Uri); + method @Nullable public final String getTypeAnonymous(@NonNull android.net.Uri); method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues); method public boolean isChildDocument(String, String); method public String moveDocument(String, String, String) throws java.io.FileNotFoundException; @@ -39766,6 +39768,7 @@ package android.service.autofill { method @NonNull public android.service.autofill.Dataset.Builder setAuthentication(@Nullable android.content.IntentSender); method @NonNull public android.service.autofill.Dataset.Builder setField(@NonNull android.view.autofill.AutofillId, @Nullable android.service.autofill.Field); method @NonNull public android.service.autofill.Dataset.Builder setField(@NonNull String, @NonNull android.service.autofill.Field); + method @NonNull public android.service.autofill.Dataset.Builder setFieldForAllHints(@NonNull android.service.autofill.Field); method @NonNull public android.service.autofill.Dataset.Builder setId(@Nullable String); method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setInlinePresentation(@NonNull android.service.autofill.InlinePresentation); method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setInlinePresentation(@NonNull android.service.autofill.InlinePresentation, @NonNull android.service.autofill.InlinePresentation); @@ -40549,7 +40552,7 @@ package android.service.credentials { method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder addCreateEntry(@NonNull android.service.credentials.CreateEntry); method @NonNull public android.service.credentials.BeginCreateCredentialResponse build(); method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder setCreateEntries(@NonNull java.util.List<android.service.credentials.CreateEntry>); - method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder setRemoteCreateEntry(@Nullable android.service.credentials.RemoteEntry); + method @NonNull @RequiresPermission("android.permission.PROVIDE_REMOTE_CREDENTIALS") public android.service.credentials.BeginCreateCredentialResponse.Builder setRemoteCreateEntry(@Nullable android.service.credentials.RemoteEntry); } public class BeginGetCredentialOption implements android.os.Parcelable { @@ -40598,7 +40601,7 @@ package android.service.credentials { method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setActions(@NonNull java.util.List<android.service.credentials.Action>); method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setAuthenticationActions(@NonNull java.util.List<android.service.credentials.Action>); method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setCredentialEntries(@NonNull java.util.List<android.service.credentials.CredentialEntry>); - method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setRemoteCredentialEntry(@Nullable android.service.credentials.RemoteEntry); + method @NonNull @RequiresPermission("android.permission.PROVIDE_REMOTE_CREDENTIALS") public android.service.credentials.BeginGetCredentialResponse.Builder setRemoteCredentialEntry(@Nullable android.service.credentials.RemoteEntry); } public final class CallingAppInfo implements android.os.Parcelable { @@ -40640,9 +40643,11 @@ package android.service.credentials { } public class CredentialEntry implements android.os.Parcelable { + ctor public CredentialEntry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice); ctor public CredentialEntry(@NonNull android.service.credentials.BeginGetCredentialOption, @NonNull android.app.slice.Slice); + ctor public CredentialEntry(@NonNull String, @NonNull android.app.slice.Slice); method public int describeContents(); - method @NonNull public android.service.credentials.BeginGetCredentialOption getBeginGetCredentialOption(); + method @NonNull public String getBeginGetCredentialOptionId(); method @NonNull public android.app.slice.Slice getSlice(); method @NonNull public String getType(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -51443,7 +51448,7 @@ package android.view { public final class MotionPredictor { ctor public MotionPredictor(@NonNull android.content.Context); method public boolean isPredictionAvailable(int, int); - method @NonNull public java.util.List<android.view.MotionEvent> predict(long); + method @Nullable public android.view.MotionEvent predict(long); method public void record(@NonNull android.view.MotionEvent); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8a6faa109522..11a0f0b81ddd 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -254,7 +254,7 @@ package android { field public static final String PERFORM_SIM_ACTIVATION = "android.permission.PERFORM_SIM_ACTIVATION"; field public static final String POWER_SAVER = "android.permission.POWER_SAVER"; field public static final String PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE = "android.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE"; - field public static final String PROVIDE_HYBRID_CREDENTIAL_SERVICE = "android.permission.PROVIDE_HYBRID_CREDENTIAL_SERVICE"; + field public static final String PROVIDE_REMOTE_CREDENTIALS = "android.permission.PROVIDE_REMOTE_CREDENTIALS"; field public static final String PROVIDE_RESOLVER_RANKER_SERVICE = "android.permission.PROVIDE_RESOLVER_RANKER_SERVICE"; field public static final String PROVIDE_TRUST_AGENT = "android.permission.PROVIDE_TRUST_AGENT"; field public static final String PROVISION_DEMO_DEVICE = "android.permission.PROVISION_DEMO_DEVICE"; @@ -2363,6 +2363,8 @@ package android.app.search { method @NonNull public String getTargetId(); method @NonNull public java.util.List<java.lang.String> getTargetIds(); method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int ACTION_DELETE = 9; // 0x9 + field public static final int ACTION_DISMISS = 10; // 0xa field public static final int ACTION_DRAGNDROP = 7; // 0x7 field public static final int ACTION_LAUNCH_KEYBOARD_FOCUS = 6; // 0x6 field public static final int ACTION_LAUNCH_TOUCH = 5; // 0x5 @@ -3362,7 +3364,7 @@ package android.companion.virtual.sensor { } public static final class VirtualSensorConfig.Builder { - ctor public VirtualSensorConfig.Builder(int, @NonNull String); + ctor public VirtualSensorConfig.Builder(@IntRange(from=1) int, @NonNull String); method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig build(); method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig.Builder setDirectChannelTypesSupported(int); method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig.Builder setHighestDirectReportRateLevel(int); @@ -13166,7 +13168,7 @@ package android.service.voice { method @NonNull public android.service.voice.HotwordDetectedResult.Builder setScore(int); } - public abstract class HotwordDetectionService extends android.app.Service implements android.service.voice.SandboxedDetectionServiceBase { + public abstract class HotwordDetectionService extends android.app.Service implements android.service.voice.SandboxedDetectionInitializer { ctor public HotwordDetectionService(); method @Deprecated public static int getMaxCustomInitializationStatus(); method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent); @@ -13238,7 +13240,7 @@ package android.service.voice { method @NonNull public android.service.voice.HotwordRejectedResult.Builder setConfidenceLevel(int); } - public interface SandboxedDetectionServiceBase { + public interface SandboxedDetectionInitializer { method public static int getMaxCustomInitializationStatus(); method public void onUpdateState(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, long, @Nullable java.util.function.IntConsumer); field public static final int INITIALIZATION_STATUS_SUCCESS = 0; // 0x0 @@ -13260,7 +13262,7 @@ package android.service.voice { field @NonNull public static final android.os.Parcelable.Creator<android.service.voice.UnknownFailure> CREATOR; } - public abstract class VisualQueryDetectionService extends android.app.Service implements android.service.voice.SandboxedDetectionServiceBase { + public abstract class VisualQueryDetectionService extends android.app.Service implements android.service.voice.SandboxedDetectionInitializer { ctor public VisualQueryDetectionService(); method public final void finishQuery() throws java.lang.IllegalStateException; method public final void gainedAttention(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 1860f433402c..a3bc3922126b 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -899,6 +899,7 @@ package android.content.pm { field public static final float OVERRIDE_MIN_ASPECT_RATIO_MEDIUM_VALUE = 1.5f; field public static final long OVERRIDE_MIN_ASPECT_RATIO_PORTRAIT_ONLY = 203647190L; // 0xc2368d6L field public static final long OVERRIDE_MIN_ASPECT_RATIO_TO_ALIGN_WITH_SPLIT_SCREEN = 208648326L; // 0xc6fb886L + field public static final long OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS = 237531167L; // 0xe28701fL field public static final int RESIZE_MODE_RESIZEABLE = 2; // 0x2 } @@ -1071,6 +1072,40 @@ package android.content.rollback { } +package android.credentials { + + public final class CredentialManager { + method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.QUERY_ALL_PACKAGES, "android.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS"}) public java.util.List<android.credentials.CredentialProviderInfo> getCredentialProviderServicesForTesting(int); + method public static boolean isServiceEnabled(@NonNull android.content.Context); + field public static final int PROVIDER_FILTER_ALL_PROVIDERS = 0; // 0x0 + field public static final int PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY = 1; // 0x1 + field public static final int PROVIDER_FILTER_USER_PROVIDERS_ONLY = 2; // 0x2 + } + + public final class CredentialProviderInfo implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.List<java.lang.String> getCapabilities(); + method @NonNull public android.content.ComponentName getComponentName(); + method @Nullable public CharSequence getLabel(@NonNull android.content.Context); + method @Nullable public android.graphics.drawable.Drawable getServiceIcon(@NonNull android.content.Context); + method @NonNull public android.content.pm.ServiceInfo getServiceInfo(); + method @NonNull public boolean hasCapability(@NonNull String); + method public boolean isEnabled(); + method public boolean isSystemProvider(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.credentials.CredentialProviderInfo> CREATOR; + } + + public static final class CredentialProviderInfo.Builder { + ctor public CredentialProviderInfo.Builder(@NonNull android.content.pm.ServiceInfo); + method @NonNull public android.credentials.CredentialProviderInfo.Builder addCapabilities(@NonNull java.util.List<java.lang.String>); + method @NonNull public android.credentials.CredentialProviderInfo build(); + method @NonNull public android.credentials.CredentialProviderInfo.Builder setEnabled(boolean); + method @NonNull public android.credentials.CredentialProviderInfo.Builder setSystemProvider(boolean); + } + +} + package android.credentials.ui { public final class AuthenticationEntry implements android.os.Parcelable { @@ -1921,6 +1956,10 @@ package android.media.tv { method public void removeHardwareDevice(int); } + public class TvView extends android.view.ViewGroup { + method public void notifyTvMessage(@NonNull String, @NonNull android.os.Bundle); + } + } package android.media.tv.tuner { @@ -2629,8 +2668,13 @@ package android.service.autofill { method @Nullable public android.content.IntentSender getAuthentication(); method @Nullable public java.util.ArrayList<java.lang.String> getAutofillDatatypes(); method @Nullable public android.content.ClipData getFieldContent(); + method @Nullable public android.widget.RemoteViews getFieldDialogPresentation(int); method @Nullable public java.util.ArrayList<android.view.autofill.AutofillId> getFieldIds(); + method @Nullable public android.service.autofill.InlinePresentation getFieldInlinePresentation(int); + method @Nullable public android.service.autofill.InlinePresentation getFieldInlineTooltipPresentation(int); + method @Nullable public android.widget.RemoteViews getFieldPresentation(int); method @Nullable public java.util.ArrayList<android.view.autofill.AutofillValue> getFieldValues(); + method @Nullable public android.service.autofill.Dataset.DatasetFieldFilter getFilter(int); method @Nullable public String getId(); method public boolean isEmpty(); } @@ -2639,6 +2683,13 @@ package android.service.autofill { method @NonNull public android.service.autofill.Dataset.Builder setContent(@NonNull android.view.autofill.AutofillId, @Nullable android.content.ClipData); } + public static final class Dataset.DatasetFieldFilter implements android.os.Parcelable { + method public int describeContents(); + method @Nullable public java.util.regex.Pattern getPattern(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.autofill.Dataset.DatasetFieldFilter> CREATOR; + } + public final class DateTransformation extends android.service.autofill.InternalTransformation implements android.os.Parcelable android.service.autofill.Transformation { method public void apply(@NonNull android.service.autofill.ValueFinder, @NonNull android.widget.RemoteViews, int) throws java.lang.Exception; } @@ -2812,7 +2863,7 @@ package android.service.voice { method @NonNull public android.service.voice.AlwaysOnHotwordDetector.EventPayload.Builder setKeyphraseRecognitionExtras(@NonNull java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>); } - public abstract class HotwordDetectionService extends android.app.Service implements android.service.voice.SandboxedDetectionServiceBase { + public abstract class HotwordDetectionService extends android.app.Service implements android.service.voice.SandboxedDetectionInitializer { field public static final boolean ENABLE_PROXIMITY_RESULT = true; } @@ -3291,7 +3342,9 @@ package android.view { } @UiThread public class View implements android.view.accessibility.AccessibilityEventSource android.graphics.drawable.Drawable.Callback android.view.KeyEvent.Callback { + method public void getBoundsOnScreen(@NonNull android.graphics.Rect, boolean); method public android.view.View getTooltipView(); + method public void getWindowDisplayFrame(@NonNull android.graphics.Rect); method public boolean isAutofilled(); method public static boolean isDefaultFocusHighlightEnabled(); method public boolean isDefaultFocusHighlightNeeded(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable); @@ -3419,6 +3472,7 @@ package android.view.autofill { } public final class AutofillManager { + field public static final String ANY_HINT = "any"; field public static final int FLAG_SMART_SUGGESTION_OFF = 0; // 0x0 field public static final int FLAG_SMART_SUGGESTION_SYSTEM = 1; // 0x1 field public static final int MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS = 120000; // 0x1d4c0 @@ -3923,6 +3977,18 @@ package android.window { method public abstract void onTransactionReady(int, @NonNull android.view.SurfaceControl.Transaction); } + public class WindowInfosListenerForTest { + ctor public WindowInfosListenerForTest(); + method @RequiresPermission(android.Manifest.permission.ACCESS_SURFACE_FLINGER) public void addWindowInfosListener(@NonNull java.util.function.Consumer<java.util.List<android.window.WindowInfosListenerForTest.WindowInfo>>); + method public void removeWindowInfosListener(@NonNull java.util.function.Consumer<java.util.List<android.window.WindowInfosListenerForTest.WindowInfo>>); + } + + public static class WindowInfosListenerForTest.WindowInfo { + field @NonNull public final android.graphics.Rect bounds; + field @NonNull public final String name; + field @NonNull public final android.os.IBinder windowToken; + } + public class WindowOrganizer { ctor public WindowOrganizer(); method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public int applySyncTransaction(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.WindowContainerTransactionCallback); diff --git a/core/java/Android.bp b/core/java/Android.bp index 738e2de7d44b..39695770ba58 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -285,6 +285,28 @@ filegroup { ], } +filegroup { + name: "hwbinder-stubs-srcs", + srcs: [ + "android/os/HidlSupport.java", + "android/os/HidlMemory.java", + "android/os/HwBinder.java", + "android/os/HwBlob.java", + "android/os/HwParcel.java", + "android/os/IHwBinder.java", + "android/os/IHwInterface.java", + "android/os/DeadObjectException.java", + "android/os/DeadSystemException.java", + "android/os/NativeHandle.java", + "android/os/RemoteException.java", + "android/util/AndroidException.java", + ], + visibility: [ + "//frameworks/base", + "//frameworks/base/api", + ], +} + cc_defaults { name: "incremental_default", cflags: [ diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index d9c7a27cb95e..125e7270b0e2 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -6621,7 +6621,9 @@ public class Activity extends ContextThemeWrapper * the incoming activity. Use 0 for no animation. * @param exitAnim A resource ID of the animation resource to use for * the outgoing activity. Use 0 for no animation. + * @deprecated Use {@link #overrideActivityTransition(int, int, int)}} instead. */ + @Deprecated public void overridePendingTransition(int enterAnim, int exitAnim) { overridePendingTransition(enterAnim, exitAnim, 0); } @@ -6644,7 +6646,9 @@ public class Activity extends ContextThemeWrapper * the outgoing activity. Use 0 for no animation. * @param backgroundColor The background color to use for the background during the animation if * the animation requires a background. Set to 0 to not override the default color. + * @deprecated Use {@link #overrideActivityTransition(int, int, int, int)}} instead. */ + @Deprecated public void overridePendingTransition(int enterAnim, int exitAnim, int backgroundColor) { ActivityClient.getInstance().overridePendingTransition(mToken, getPackageName(), enterAnim, exitAnim, backgroundColor); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 87fe215edd85..b4068dbf0797 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -4208,8 +4208,8 @@ public class ActivityManager { * processes to reclaim memory; the system will take care of restarting * these processes in the future as needed. * - * <p class="note">On devices with a {@link Build.VERSION#SECURITY_PATCH} of 2022-12-01 or - * greater, third party applications can only use this API to kill their own processes. + * <p class="note">On devices that run Android 14 or higher, + * third party applications can only use this API to kill their own processes. * </p> * * @param packageName The name of the package whose processes are to diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index dfdfd0e2054e..a3ada763265a 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1154,6 +1154,11 @@ public final class ActivityThread extends ClientTransactionHandler } @Override + public final void schedulePing(RemoteCallback pong) { + sendMessage(H.PING, pong); + } + + @Override public final void bindApplication(String processName, ApplicationInfo appInfo, String sdkSandboxClientAppVolumeUuid, String sdkSandboxClientAppPackage, ProviderInfoList providerList, ComponentName instrumentationName, @@ -2154,6 +2159,7 @@ public final class ActivityThread extends ClientTransactionHandler public static final int DUMP_GFXINFO = 165; public static final int DUMP_RESOURCES = 166; public static final int TIMEOUT_SERVICE = 167; + public static final int PING = 168; public static final int INSTRUMENT_WITHOUT_RESTART = 170; public static final int FINISH_INSTRUMENTATION_WITHOUT_RESTART = 171; @@ -2209,6 +2215,7 @@ public final class ActivityThread extends ClientTransactionHandler return "FINISH_INSTRUMENTATION_WITHOUT_RESTART"; case DUMP_RESOURCES: return "DUMP_RESOURCES"; case TIMEOUT_SERVICE: return "TIMEOUT_SERVICE"; + case PING: return "PING"; } } return Integer.toString(code); @@ -2292,6 +2299,9 @@ public final class ActivityThread extends ClientTransactionHandler handleTimeoutService((IBinder) msg.obj, msg.arg1); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; + case PING: + ((RemoteCallback) msg.obj).sendResult(null); + break; case CONFIGURATION_CHANGED: mConfigurationController.handleConfigurationChanged((Configuration) msg.obj); break; diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl index dad9b435e9e7..4f77203c8c6f 100644 --- a/core/java/android/app/IApplicationThread.aidl +++ b/core/java/android/app/IApplicationThread.aidl @@ -172,4 +172,5 @@ oneway interface IApplicationThread { in TranslationSpec targetSpec, in List<AutofillId> viewIds, in UiTranslationSpec uiTranslationSpec); void scheduleTimeoutService(IBinder token, int startId); + void schedulePing(in RemoteCallback pong); } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 5cad1aeb9fe7..ac928116481f 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -93,7 +93,7 @@ public class Instrumentation { private static final String TAG = "Instrumentation"; - private static final long CONNECT_TIMEOUT_MILLIS = 5000; + private static final long CONNECT_TIMEOUT_MILLIS = 60_000; private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java index f4cd60d08804..97cc706fbab6 100644 --- a/core/java/android/app/LocaleConfig.java +++ b/core/java/android/app/LocaleConfig.java @@ -58,10 +58,10 @@ import java.util.Set; * * @attr ref android.R.styleable#LocaleConfig_Locale_name * @attr ref android.R.styleable#AndroidManifestApplication_localeConfig - * - * <p>For more information about the LocaleConfig overridden by the application, see - * TODO(b/261528306): add link to guide */ +// Add following to last Note: when guide is written: +// For more information about the LocaleConfig overridden by the application, see TODO(b/261528306): +// add link to guide public class LocaleConfig implements Parcelable { private static final String TAG = "LocaleConfig"; public static final String TAG_LOCALE_CONFIG = "locale-config"; diff --git a/core/java/android/app/admin/DevicePolicyCache.java b/core/java/android/app/admin/DevicePolicyCache.java index 3957732c56dd..b6e83c8bc8a1 100644 --- a/core/java/android/app/admin/DevicePolicyCache.java +++ b/core/java/android/app/admin/DevicePolicyCache.java @@ -19,6 +19,9 @@ import android.annotation.UserIdInt; import com.android.server.LocalServices; +import java.util.ArrayList; +import java.util.List; + /** * Stores a copy of the set of device policies maintained by {@link DevicePolicyManager} that * can be accessed from any place without risking dead locks. @@ -61,6 +64,12 @@ public abstract class DevicePolicyCache { public abstract boolean canAdminGrantSensorsPermissions(); /** + * Returns a list of package names for which all launcher shortcuts should be modified to be + * launched in the managed profile and badged accordingly. + */ + public abstract List<String> getLauncherShortcutOverrides(); + + /** * Empty implementation. */ private static class EmptyDevicePolicyCache extends DevicePolicyCache { @@ -85,5 +94,9 @@ public abstract class DevicePolicyCache { public boolean canAdminGrantSensorsPermissions() { return false; } + @Override + public List<String> getLauncherShortcutOverrides() { + return new ArrayList<>(); + } } } diff --git a/core/java/android/app/search/SearchTargetEvent.java b/core/java/android/app/search/SearchTargetEvent.java index d4915afd5ce6..e8ef9227ad05 100644 --- a/core/java/android/app/search/SearchTargetEvent.java +++ b/core/java/android/app/search/SearchTargetEvent.java @@ -50,7 +50,9 @@ public final class SearchTargetEvent implements Parcelable { ACTION_LAUNCH_TOUCH, ACTION_LAUNCH_KEYBOARD_FOCUS, ACTION_DRAGNDROP, - ACTION_SURFACE_INVISIBLE + ACTION_SURFACE_INVISIBLE, + ACTION_DELETE, + ACTION_DISMISS }) @Retention(RetentionPolicy.SOURCE) public @interface ActionType {} @@ -114,6 +116,16 @@ public final class SearchTargetEvent implements Parcelable { */ public static final int ACTION_SURFACE_INVISIBLE = 8; + /** + * Constant that defines user deleted a target. + */ + public static final int ACTION_DELETE = 9; + + /** + * Constant that defines user dismissed a target. + */ + public static final int ACTION_DISMISS = 10; + private SearchTargetEvent(@NonNull List<String> targetIds, @Nullable String location, @ActionType int actionType, diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index de4f619392c1..a522cc04eff1 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -801,6 +801,119 @@ public final class CompanionDeviceManager { } /** + * Listener for any changes to {@link com.android.server.companion.transport.Transport}. + * + * @hide + */ + public interface OnTransportsChangedListener { + /** + * Invoked when a change occurs to any of the transports + * + * @param associations all the associations which have connected transports + */ + void onTransportsChanged(@NonNull List<AssociationInfo> associations); + } + + /** + * Register a listener for any changes to + * {@link com.android.server.companion.transport.Transport}. Your app will receive a callback to + * {@link OnTransportsChangedListener} immediately with all the existing transports. + * + * @hide + */ + public void addOnTransportsChangedListener( + @NonNull Executor executor, @NonNull OnTransportsChangedListener listener) { + final OnTransportsChangedListenerProxy proxy = new OnTransportsChangedListenerProxy( + executor, listener); + try { + mService.addOnTransportsChangedListener(proxy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Unregister a listener to stop receiving any changes to + * {@link com.android.server.companion.transport.Transport}. + * + * @hide + */ + public void removeOnTransportsChangedListener( + @NonNull OnTransportsChangedListener listener) { + final OnTransportsChangedListenerProxy proxy = new OnTransportsChangedListenerProxy( + null, listener); + try { + mService.removeOnTransportsChangedListener(proxy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Send a message to remote devices + * + * @hide + */ + public void sendMessage(int messageType, byte[] data, int[] associationIds) { + try { + mService.sendMessage(messageType, data, associationIds); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Listener when a message is received for the registered message type + * + * @see #addOnMessageReceivedListener(Executor, int, OnMessageReceivedListener) + * + * @hide + */ + public interface OnMessageReceivedListener { + /** + * Called when a message is received + */ + void onMessageReceived(int associationId, byte[] data); + } + + /** + * Register a listener to receive callbacks when a message is received by the given type + * + * @see com.android.server.companion.transport.Transport for supported message types + * + * @hide + */ + public void addOnMessageReceivedListener(@NonNull Executor executor, int messageType, + OnMessageReceivedListener listener) { + final OnMessageReceivedListenerProxy proxy = new OnMessageReceivedListenerProxy( + executor, listener); + try { + mService.addOnMessageReceivedListener(messageType, proxy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Unregister a listener to stop receiving callbacks when a message is received by the given + * type + * + * @see com.android.server.companion.transport.Transport for supported message types + * + * @hide + */ + public void removeOnMessageReceivedListener(int messageType, + OnMessageReceivedListener listener) { + final OnMessageReceivedListenerProxy proxy = new OnMessageReceivedListenerProxy( + null, listener); + try { + mService.removeOnMessageReceivedListener(messageType, proxy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Checks whether the bluetooth device represented by the mac address was recently associated * with the companion app. This allows these devices to skip the Bluetooth pairing dialog if * their pairing variant is {@link BluetoothDevice#PAIRING_VARIANT_CONSENT}. @@ -1277,6 +1390,40 @@ public final class CompanionDeviceManager { } } + private static class OnTransportsChangedListenerProxy + extends IOnTransportsChangedListener.Stub { + private final Executor mExecutor; + private final OnTransportsChangedListener mListener; + + private OnTransportsChangedListenerProxy(Executor executor, + OnTransportsChangedListener listener) { + mExecutor = executor; + mListener = listener; + } + + @Override + public void onTransportsChanged(@NonNull List<AssociationInfo> associations) { + mExecutor.execute(() -> mListener.onTransportsChanged(associations)); + } + } + + private static class OnMessageReceivedListenerProxy + extends IOnMessageReceivedListener.Stub { + private final Executor mExecutor; + private final OnMessageReceivedListener mListener; + + private OnMessageReceivedListenerProxy(Executor executor, + OnMessageReceivedListener listener) { + mExecutor = executor; + mListener = listener; + } + + @Override + public void onMessageReceived(int associationId, byte[] data) { + mExecutor.execute(() -> mListener.onMessageReceived(associationId, data)); + } + } + private static class SystemDataTransferCallbackProxy extends ISystemDataTransferCallback.Stub { private final Executor mExecutor; private final OutcomeReceiver<Void, CompanionException> mCallback; diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index cb4baca73ba0..b5e2670e5299 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -19,6 +19,8 @@ package android.companion; import android.app.PendingIntent; import android.companion.IAssociationRequestCallback; import android.companion.IOnAssociationsChangedListener; +import android.companion.IOnMessageReceivedListener; +import android.companion.IOnTransportsChangedListener; import android.companion.ISystemDataTransferCallback; import android.companion.AssociationInfo; import android.companion.AssociationRequest; @@ -67,6 +69,16 @@ interface ICompanionDeviceManager { void removeOnAssociationsChangedListener(IOnAssociationsChangedListener listener, int userId); + void addOnTransportsChangedListener(IOnTransportsChangedListener listener); + + void removeOnTransportsChangedListener(IOnTransportsChangedListener listener); + + void sendMessage(int messageType, in byte[] data, in int[] associationIds); + + void addOnMessageReceivedListener(int messageType, IOnMessageReceivedListener listener); + + void removeOnMessageReceivedListener(int messageType, IOnMessageReceivedListener listener); + void notifyDeviceAppeared(int associationId); void notifyDeviceDisappeared(int associationId); diff --git a/core/java/android/companion/IOnMessageReceivedListener.aidl b/core/java/android/companion/IOnMessageReceivedListener.aidl new file mode 100644 index 000000000000..17f03f80996e --- /dev/null +++ b/core/java/android/companion/IOnMessageReceivedListener.aidl @@ -0,0 +1,23 @@ +/* + * 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 per missions and + * limitations under the License. + */ + +package android.companion; + +/** @hide */ +interface IOnMessageReceivedListener { + + oneway void onMessageReceived(int associationId, in byte[] data); +}
\ No newline at end of file diff --git a/core/java/android/companion/IOnTransportsChangedListener.aidl b/core/java/android/companion/IOnTransportsChangedListener.aidl new file mode 100644 index 000000000000..a10147627a9d --- /dev/null +++ b/core/java/android/companion/IOnTransportsChangedListener.aidl @@ -0,0 +1,25 @@ +/* + * 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 per missions and + * limitations under the License. + */ + +package android.companion; + +import android.companion.AssociationInfo; + +/** @hide */ +interface IOnTransportsChangedListener { + + oneway void onTransportsChanged(in List<AssociationInfo> associations); +}
\ No newline at end of file diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 6cc4c8a24c48..90681cba7d83 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -545,12 +545,13 @@ public final class VirtualDeviceManager { @VirtualDisplayFlag int flags, @Nullable @CallbackExecutor Executor executor, @Nullable VirtualDisplay.Callback callback) { - VirtualDisplayConfig config = new VirtualDisplayConfig.Builder( + VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder( getVirtualDisplayName(), width, height, densityDpi) - .setSurface(surface) - .setFlags(flags) - .build(); - return createVirtualDisplay(config, executor, callback); + .setFlags(flags); + if (surface != null) { + builder.setSurface(surface); + } + return createVirtualDisplay(builder.build(), executor, callback); } /** diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java index ffbdff8c2e3b..401e754abca6 100644 --- a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java +++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java @@ -17,6 +17,7 @@ package android.companion.virtual.sensor; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -167,7 +168,10 @@ public final class VirtualSensorConfig implements Parcelable { * @param name The name of the sensor. Must be unique among all sensors with the same type * that belong to the same virtual device. */ - public Builder(int type, @NonNull String name) { + public Builder(@IntRange(from = 1) int type, @NonNull String name) { + if (type <= 0) { + throw new IllegalArgumentException("Virtual sensor type must be positive"); + } mType = type; mName = Objects.requireNonNull(name); } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index a412560d0347..36f7ff53eb76 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -316,10 +316,12 @@ public abstract class Context { BIND_ALLOW_ACTIVITY_STARTS, BIND_INCLUDE_CAPABILITIES, BIND_SHARED_ISOLATED_PROCESS, - // Intentionally not included, because it'd cause sign-extension. + // Intentionally not include BIND_EXTERNAL_SERVICE, because it'd cause sign-extension. // This would allow Android Studio to show a warning, if someone tries to use // BIND_EXTERNAL_SERVICE BindServiceFlags. - BIND_EXTERNAL_SERVICE_LONG + BIND_EXTERNAL_SERVICE_LONG, + // Make sure no flag uses the sign bit (most significant bit) of the long integer, + // to avoid future confusion. }) @Retention(RetentionPolicy.SOURCE) public @interface BindServiceFlagsLongBits {} @@ -338,6 +340,7 @@ public abstract class Context { /** * @return Return flags in 64 bits long integer. + * @hide */ public long getValue() { return mValue; @@ -678,13 +681,11 @@ public abstract class Context { */ public static final int BIND_EXTERNAL_SERVICE = 0x80000000; - /** * Works in the same way as {@link #BIND_EXTERNAL_SERVICE}, but it's defined as a (@code long) * value that is compatible to {@link BindServiceFlags}. */ - public static final long BIND_EXTERNAL_SERVICE_LONG = 0x8000_0000_0000_0000L; - + public static final long BIND_EXTERNAL_SERVICE_LONG = 1L << 62; /** * These bind flags reduce the strength of the binding such that we shouldn't diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index f8f2663063a6..eb14cc4802ad 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1156,6 +1156,34 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { 264301586L; // buganizer id /** + * This change id forces the packages it is applied to sandbox {@link android.view.View} API to + * an activity bounds for: + * + * <p>{@link android.view.View#getLocationOnScreen}, + * {@link android.view.View#getWindowVisibleDisplayFrame}, + * {@link android.view.View}#getWindowDisplayFrame, + * {@link android.view.View}#getBoundsOnScreen. + * + * <p>For {@link android.view.View#getWindowVisibleDisplayFrame} and + * {@link android.view.View}#getWindowDisplayFrame this sandboxing is happening indirectly + * through + * {@link android.view.ViewRootImpl}#getWindowVisibleDisplayFrame, + * {@link android.view.ViewRootImpl}#getDisplayFrame respectively. + * + * <p>Some applications assume that they occupy the whole screen and therefore use the display + * coordinates in their calculations as if an activity is positioned in the top-left corner of + * the screen, with left coordinate equal to 0. This may not be the case of applications in + * multi-window and in letterbox modes. This can lead to shifted or out of bounds UI elements in + * case the activity is Letterboxed or is in multi-window mode. + * @hide + */ + @ChangeId + @Overridable + @Disabled + @TestApi + public static final long OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS = 237531167L; // 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/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index 08cfbf76a040..96a42e24bc1a 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -33,6 +33,7 @@ import android.content.pm.PackageInstaller; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; +import android.content.pm.LauncherActivityInfoInternal; import android.graphics.Rect; import android.os.Bundle; import android.os.UserHandle; @@ -114,4 +115,5 @@ interface ILauncherApps { String getShortcutIconUri(String callingPackage, String packageName, String shortcutId, int userId); + Map<String, LauncherActivityInfoInternal> getActivityOverrides(String callingPackage, int userId); } diff --git a/core/java/android/content/pm/LauncherActivityInfo.java b/core/java/android/content/pm/LauncherActivityInfo.java index 16e720e3794c..a4d532712cfe 100644 --- a/core/java/android/content/pm/LauncherActivityInfo.java +++ b/core/java/android/content/pm/LauncherActivityInfo.java @@ -34,7 +34,6 @@ import android.util.DisplayMetrics; */ public class LauncherActivityInfo { private final PackageManager mPm; - private UserHandle mUser; private final LauncherActivityInfoInternal mInternal; /** @@ -43,9 +42,8 @@ public class LauncherActivityInfo { * @param context The context for fetching resources. */ - LauncherActivityInfo(Context context, UserHandle user, LauncherActivityInfoInternal internal) { + LauncherActivityInfo(Context context, LauncherActivityInfoInternal internal) { mPm = context.getPackageManager(); - mUser = user; mInternal = internal; } @@ -70,7 +68,7 @@ public class LauncherActivityInfo { * @return The UserHandle of the profile. */ public UserHandle getUser() { - return mUser; + return mInternal.getUser(); } /** @@ -180,6 +178,6 @@ public class LauncherActivityInfo { public Drawable getBadgedIcon(int density) { Drawable originalIcon = getIcon(density); - return mPm.getUserBadgedIcon(originalIcon, mUser); + return mPm.getUserBadgedIcon(originalIcon, mInternal.getUser()); } } diff --git a/core/java/android/content/pm/LauncherActivityInfoInternal.java b/core/java/android/content/pm/LauncherActivityInfoInternal.java index 46c415df7525..5aac97d784b3 100644 --- a/core/java/android/content/pm/LauncherActivityInfoInternal.java +++ b/core/java/android/content/pm/LauncherActivityInfoInternal.java @@ -21,6 +21,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.os.Parcel; import android.os.Parcelable; +import android.os.UserHandle; /** * @hide @@ -30,23 +31,27 @@ public class LauncherActivityInfoInternal implements Parcelable { @NonNull private ActivityInfo mActivityInfo; @NonNull private ComponentName mComponentName; @NonNull private IncrementalStatesInfo mIncrementalStatesInfo; + @NonNull private UserHandle mUser; /** * @param info ActivityInfo from which to create the LauncherActivityInfo. * @param incrementalStatesInfo The package's states. + * @param user The user the activity info belongs to. */ public LauncherActivityInfoInternal(@NonNull ActivityInfo info, - @NonNull IncrementalStatesInfo incrementalStatesInfo) { + @NonNull IncrementalStatesInfo incrementalStatesInfo, + @NonNull UserHandle user) { mActivityInfo = info; mComponentName = new ComponentName(info.packageName, info.name); mIncrementalStatesInfo = incrementalStatesInfo; + mUser = user; } public LauncherActivityInfoInternal(Parcel source) { - mActivityInfo = source.readParcelable(ActivityInfo.class.getClassLoader(), android.content.pm.ActivityInfo.class); + mActivityInfo = source.readTypedObject(ActivityInfo.CREATOR); mComponentName = new ComponentName(mActivityInfo.packageName, mActivityInfo.name); - mIncrementalStatesInfo = source.readParcelable( - IncrementalStatesInfo.class.getClassLoader(), android.content.pm.IncrementalStatesInfo.class); + mIncrementalStatesInfo = source.readTypedObject(IncrementalStatesInfo.CREATOR); + mUser = source.readTypedObject(UserHandle.CREATOR); } public ComponentName getComponentName() { @@ -57,6 +62,10 @@ public class LauncherActivityInfoInternal implements Parcelable { return mActivityInfo; } + public UserHandle getUser() { + return mUser; + } + public IncrementalStatesInfo getIncrementalStatesInfo() { return mIncrementalStatesInfo; } @@ -68,8 +77,9 @@ public class LauncherActivityInfoInternal implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(mActivityInfo, 0); - dest.writeParcelable(mIncrementalStatesInfo, 0); + dest.writeTypedObject(mActivityInfo, flags); + dest.writeTypedObject(mIncrementalStatesInfo, flags); + dest.writeTypedObject(mUser, flags); } public static final @android.annotation.NonNull Creator<LauncherActivityInfoInternal> CREATOR = diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index f8c49744d834..8989006a7e83 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -64,6 +64,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; +import android.util.ArrayMap; import android.util.DisplayMetrics; import android.util.Log; import android.util.Pair; @@ -793,13 +794,45 @@ public class LauncherApps { if (ai == null) { return null; } - return new LauncherActivityInfo(mContext, user, ai); + return new LauncherActivityInfo(mContext, ai); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } } /** + * Returns overrides for the activities that should be launched for the shortcuts of certain + * package names. + * + * @return {@link Map} whose keys are package names and whose values are the + * {@link LauncherActivityInfo}s that should be used for those packages' shortcuts. If there are + * no activity overrides, an empty {@link Map} will be returned. + * + * @hide + */ + @NonNull + public Map<String, LauncherActivityInfo> getActivityOverrides() { + Map<String, LauncherActivityInfo> activityOverrides = new ArrayMap<>(); + try { + Map<String, LauncherActivityInfoInternal> activityOverridesInternal = + mService.getActivityOverrides(mContext.getPackageName(), mContext.getUserId()); + for (Map.Entry<String, LauncherActivityInfoInternal> packageToOverride : + activityOverridesInternal.entrySet()) { + activityOverrides.put( + packageToOverride.getKey(), + new LauncherActivityInfo( + mContext, + packageToOverride.getValue() + ) + ); + } + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + return activityOverrides; + } + + /** * Starts a Main activity in the specified profile. * * @param component The ComponentName of the activity to launch @@ -916,7 +949,7 @@ public class LauncherApps { } ArrayList<LauncherActivityInfo> lais = new ArrayList<>(); for (LauncherActivityInfoInternal internal : internals.getList()) { - LauncherActivityInfo lai = new LauncherActivityInfo(mContext, user, internal); + LauncherActivityInfo lai = new LauncherActivityInfo(mContext, internal); if (DEBUG) { Log.v(TAG, "Returning activity for profile " + user + " : " + lai.getComponentName()); diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index a6a62150d0da..cb988dfdb203 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -2935,13 +2935,14 @@ public class PackageInstaller { * <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 + * <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> * <li>The installer declares the * {@link android.Manifest.permission#UPDATE_PACKAGES_WITHOUT_USER_ACTION * UPDATE_PACKAGES_WITHOUT_USER_ACTION} permission.</li> diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index f0230e7f4593..0806f1db2bb7 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -26,12 +26,12 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemService; +import android.annotation.TestApi; import android.app.Activity; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.IntentSender; -import android.content.pm.ServiceInfo; import android.os.CancellationSignal; import android.os.ICancellationSignal; import android.os.OutcomeReceiver; @@ -75,21 +75,21 @@ public final class CredentialManager { * * @hide */ - public static final int PROVIDER_FILTER_ALL_PROVIDERS = 0; + @TestApi public static final int PROVIDER_FILTER_ALL_PROVIDERS = 0; /** * Returns system credential providers only. * * @hide */ - public static final int PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY = 1; + @TestApi public static final int PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY = 1; /** * Returns user credential providers only. * * @hide */ - public static final int PROVIDER_FILTER_USER_PROVIDERS_ONLY = 2; + @TestApi public static final int PROVIDER_FILTER_USER_PROVIDERS_ONLY = 2; private final Context mContext; private final ICredentialManager mService; @@ -263,44 +263,6 @@ public final class CredentialManager { } /** - * Gets a list of all user configurable credential providers registered on the system. This API - * is intended for browsers and settings apps. - * - * @param cancellationSignal an optional signal that allows for cancelling this call - * @param executor the callback will take place on this {@link Executor} - * @param callback the callback invoked when the request succeeds or fails - * @hide - */ - @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) - public void listEnabledProviders( - @Nullable CancellationSignal cancellationSignal, - @CallbackExecutor @NonNull Executor executor, - @NonNull - OutcomeReceiver<ListEnabledProvidersResponse, ListEnabledProvidersException> - callback) { - requireNonNull(executor, "executor must not be null"); - requireNonNull(callback, "callback must not be null"); - - if (cancellationSignal != null && cancellationSignal.isCanceled()) { - Log.w(TAG, "listEnabledProviders already canceled"); - return; - } - - ICancellationSignal cancelRemote = null; - try { - cancelRemote = - mService.listEnabledProviders( - new ListEnabledProvidersTransport(executor, callback)); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } - - if (cancellationSignal != null && cancelRemote != null) { - cancellationSignal.setRemote(cancelRemote); - } - } - - /** * Sets a list of all user configurable credential providers registered on the system. This API * is intended for settings apps. * @@ -348,36 +310,43 @@ public final class CredentialManager { } /** - * Returns the list of ServiceInfo for all discovered credential providers on this device. + * Returns the list of CredentialProviderInfo for all discovered credential providers on this + * device but will include test system providers as well. * * @hide */ @NonNull - @RequiresPermission(android.Manifest.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS) - public List<ServiceInfo> getCredentialProviderServicesForTesting( - @ProviderFilter int providerFilter) { + @TestApi + @RequiresPermission( + anyOf = { + android.Manifest.permission.QUERY_ALL_PACKAGES, + android.Manifest.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS + }) + public List<CredentialProviderInfo> getCredentialProviderServicesForTesting( + @ProviderFilter int providerFilter) { try { - return mService.getCredentialProviderServices( - mContext.getUserId(), - /* disableSystemAppVerificationForTests= */ true, - providerFilter); + return mService.getCredentialProviderServicesForTesting(providerFilter); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** - * Returns the list of ServiceInfo for all discovered credential providers on this device. + * Returns the list of CredentialProviderInfo for all discovered credential providers on this + * device. * * @hide */ @NonNull - @RequiresPermission(android.Manifest.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS) - public List<ServiceInfo> getCredentialProviderServices( + @RequiresPermission( + anyOf = { + android.Manifest.permission.QUERY_ALL_PACKAGES, + android.Manifest.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS + }) + public List<CredentialProviderInfo> getCredentialProviderServices( int userId, @ProviderFilter int providerFilter) { try { - return mService.getCredentialProviderServices( - userId, /* disableSystemAppVerificationForTests= */ false, providerFilter); + return mService.getCredentialProviderServices(userId, providerFilter); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -388,7 +357,9 @@ public final class CredentialManager { * * @hide */ - public static boolean isServiceEnabled(Context context) { + @TestApi + public static boolean isServiceEnabled(@NonNull Context context) { + requireNonNull(context, "context must not be null"); if (context == null) { return false; } @@ -578,33 +549,6 @@ public final class CredentialManager { } } - private static class ListEnabledProvidersTransport extends IListEnabledProvidersCallback.Stub { - // TODO: listen for cancellation to release callback. - - private final Executor mExecutor; - private final OutcomeReceiver<ListEnabledProvidersResponse, ListEnabledProvidersException> - mCallback; - - private ListEnabledProvidersTransport( - Executor executor, - OutcomeReceiver<ListEnabledProvidersResponse, ListEnabledProvidersException> - callback) { - mExecutor = executor; - mCallback = callback; - } - - @Override - public void onResponse(ListEnabledProvidersResponse response) { - mExecutor.execute(() -> mCallback.onResult(response)); - } - - @Override - public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new ListEnabledProvidersException(errorType, message))); - } - } - private static class SetEnabledProvidersTransport extends ISetEnabledProvidersCallback.Stub { // TODO: listen for cancellation to release callback. diff --git a/core/java/android/credentials/IListEnabledProvidersCallback.aidl b/core/java/android/credentials/CredentialProviderInfo.aidl index 3a8e25ed954a..30b7742d17f8 100644 --- a/core/java/android/credentials/IListEnabledProvidersCallback.aidl +++ b/core/java/android/credentials/CredentialProviderInfo.aidl @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 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. @@ -16,14 +16,4 @@ package android.credentials; -import android.credentials.ListEnabledProvidersResponse; - -/** - * Listener for an listEnabledProviders request. - * - * @hide - */ -interface IListEnabledProvidersCallback { - oneway void onResponse(in ListEnabledProvidersResponse response); - oneway void onError(String errorType, String message); -}
\ No newline at end of file +parcelable CredentialProviderInfo;
\ No newline at end of file diff --git a/core/java/android/credentials/CredentialProviderInfo.java b/core/java/android/credentials/CredentialProviderInfo.java new file mode 100644 index 000000000000..7276770d281e --- /dev/null +++ b/core/java/android/credentials/CredentialProviderInfo.java @@ -0,0 +1,215 @@ +/* + * 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 android.credentials; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TestApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ServiceInfo; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * {@link ServiceInfo} and meta-data about a credential provider. + * + * @hide + */ +@TestApi +public final class CredentialProviderInfo implements Parcelable { + @NonNull private final ServiceInfo mServiceInfo; + @NonNull private final List<String> mCapabilities = new ArrayList<>(); + @Nullable private final CharSequence mOverrideLabel; + private final boolean mIsSystemProvider; + private final boolean mIsEnabled; + + /** + * Constructs an information instance of the credential provider. + * + * @param builder the builder object. + */ + private CredentialProviderInfo(@NonNull Builder builder) { + mServiceInfo = builder.mServiceInfo; + mCapabilities.addAll(builder.mCapabilities); + mIsSystemProvider = builder.mIsSystemProvider; + mIsEnabled = builder.mIsEnabled; + mOverrideLabel = builder.mOverrideLabel; + } + + /** Returns true if the service supports the given {@code credentialType}, false otherwise. */ + @NonNull + public boolean hasCapability(@NonNull String credentialType) { + return mCapabilities.contains(credentialType); + } + + /** Returns the service info. */ + @NonNull + public ServiceInfo getServiceInfo() { + return mServiceInfo; + } + + /** Returns whether it is a system provider. */ + public boolean isSystemProvider() { + return mIsSystemProvider; + } + + /** Returns the service icon. */ + @Nullable + public Drawable getServiceIcon(@NonNull Context context) { + return mServiceInfo.loadIcon(context.getPackageManager()); + } + + /** Returns the service label. */ + @Nullable + public CharSequence getLabel(@NonNull Context context) { + if (mOverrideLabel != null) { + return mOverrideLabel; + } + return mServiceInfo.loadSafeLabel(context.getPackageManager()); + } + + /** Returns a list of capabilities this provider service can support. */ + @NonNull + public List<String> getCapabilities() { + return Collections.unmodifiableList(mCapabilities); + } + + /** Returns whether the provider is enabled by the user. */ + public boolean isEnabled() { + return mIsEnabled; + } + + /** Returns the component name for the service. */ + @NonNull + public ComponentName getComponentName() { + return mServiceInfo.getComponentName(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeTypedObject(mServiceInfo, flags); + dest.writeBoolean(mIsSystemProvider); + dest.writeStringList(mCapabilities); + dest.writeBoolean(mIsEnabled); + TextUtils.writeToParcel(mOverrideLabel, dest, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "CredentialProviderInfo {" + + "serviceInfo=" + + mServiceInfo + + ", " + + "isSystemProvider=" + + mIsSystemProvider + + ", " + + "isEnabled=" + + mIsEnabled + + ", " + + "overrideLabel=" + + mOverrideLabel + + ", " + + "capabilities=" + + String.join(",", mCapabilities) + + "}"; + } + + private CredentialProviderInfo(@NonNull Parcel in) { + mServiceInfo = in.readTypedObject(ServiceInfo.CREATOR); + mIsSystemProvider = in.readBoolean(); + in.readStringList(mCapabilities); + mIsEnabled = in.readBoolean(); + mOverrideLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + } + + public static final @NonNull Parcelable.Creator<CredentialProviderInfo> CREATOR = + new Parcelable.Creator<CredentialProviderInfo>() { + @Override + public CredentialProviderInfo[] newArray(int size) { + return new CredentialProviderInfo[size]; + } + + @Override + public CredentialProviderInfo createFromParcel(@NonNull Parcel in) { + return new CredentialProviderInfo(in); + } + }; + + /** A builder for {@link CredentialProviderInfo} objects. */ + public static final class Builder { + + @NonNull private ServiceInfo mServiceInfo; + @NonNull private List<String> mCapabilities = new ArrayList<>(); + private boolean mIsSystemProvider = false; + private boolean mIsEnabled = false; + @Nullable private CharSequence mOverrideLabel = null; + + /** + * Creates a new builder. + * + * @param serviceInfo the service info of the credential provider service. + */ + public Builder(@NonNull ServiceInfo serviceInfo) { + mServiceInfo = serviceInfo; + } + + /** Sets whether it is a system provider. */ + public @NonNull Builder setSystemProvider(boolean isSystemProvider) { + mIsSystemProvider = isSystemProvider; + return this; + } + + /** + * Sets the label to be used instead of getting from the system (for unit tests). + * + * @hide + */ + public @NonNull Builder setOverrideLabel(@NonNull CharSequence overrideLabel) { + mOverrideLabel = overrideLabel; + return this; + } + + /** Sets a list of capabilities this provider service can support. */ + public @NonNull Builder addCapabilities(@NonNull List<String> capabilities) { + mCapabilities.addAll(capabilities); + return this; + } + + /** Sets whether it is enabled by the user. */ + public @NonNull Builder setEnabled(boolean isEnabled) { + mIsEnabled = isEnabled; + return this; + } + + /** Builds a new {@link CredentialProviderInfo} instance. */ + public @NonNull CredentialProviderInfo build() { + return new CredentialProviderInfo(this); + } + } +} diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl index 625fc8ab5dad..8c2cb5aa0d77 100644 --- a/core/java/android/credentials/ICredentialManager.aidl +++ b/core/java/android/credentials/ICredentialManager.aidl @@ -18,7 +18,7 @@ package android.credentials; import java.util.List; -import android.content.pm.ServiceInfo; +import android.credentials.CredentialProviderInfo; import android.credentials.ClearCredentialStateRequest; import android.credentials.CreateCredentialRequest; import android.credentials.GetCredentialRequest; @@ -27,7 +27,6 @@ import android.credentials.UnregisterCredentialDescriptionRequest; import android.credentials.IClearCredentialStateCallback; import android.credentials.ICreateCredentialCallback; import android.credentials.IGetCredentialCallback; -import android.credentials.IListEnabledProvidersCallback; import android.credentials.ISetEnabledProvidersCallback; import android.content.ComponentName; import android.os.ICancellationSignal; @@ -45,8 +44,6 @@ interface ICredentialManager { @nullable ICancellationSignal clearCredentialState(in ClearCredentialStateRequest request, in IClearCredentialStateCallback callback, String callingPackage); - @nullable ICancellationSignal listEnabledProviders(in IListEnabledProvidersCallback callback); - void setEnabledProviders(in List<String> providers, in int userId, in ISetEnabledProvidersCallback callback); void registerCredentialDescription(in RegisterCredentialDescriptionRequest request, String callingPackage); @@ -55,6 +52,8 @@ interface ICredentialManager { boolean isEnabledCredentialProviderService(in ComponentName componentName, String callingPackage); - List<ServiceInfo> getCredentialProviderServices(in int userId, in boolean disableSystemAppVerificationForTests, in int providerFilter); + List<CredentialProviderInfo> getCredentialProviderServices(in int userId, in int providerFilter); + + List<CredentialProviderInfo> getCredentialProviderServicesForTesting(in int providerFilter); } diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 50dd7a0bc1be..6ae71d2bb25e 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -1409,9 +1409,10 @@ public final class DisplayManager { * @param hdrConversionMode The {@link HdrConversionMode} to set. * Note, {@code HdrConversionMode.preferredHdrOutputType} is only applicable when * {@code HdrConversionMode.conversionMode} is {@link HdrConversionMode#HDR_CONVERSION_FORCE}. + * If {@code HdrConversionMode.preferredHdrOutputType} is not set in case when + * {@code HdrConversionMode.conversionMode} is {@link HdrConversionMode#HDR_CONVERSION_FORCE}, + * it means that preferred output type is SDR. * - * @throws IllegalArgumentException if hdrConversionMode.preferredHdrOutputType is not set - * when hdrConversionMode.conversionMode is {@link HdrConversionMode#HDR_CONVERSION_FORCE}. * @throws IllegalArgumentException if hdrConversionMode.preferredHdrOutputType is set but * hdrConversionMode.conversionMode is not {@link HdrConversionMode#HDR_CONVERSION_FORCE}. * diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java index 6b56a067a198..490e55ba260f 100644 --- a/core/java/android/hardware/display/VirtualDisplayConfig.java +++ b/core/java/android/hardware/display/VirtualDisplayConfig.java @@ -27,13 +27,14 @@ import android.media.projection.MediaProjection; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArraySet; +import android.view.ContentRecordingSession; import android.view.Display; import android.view.Surface; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; import java.util.Objects; +import java.util.Set; /** * Holds configuration used to create {@link VirtualDisplay} instances. @@ -51,8 +52,10 @@ public final class VirtualDisplayConfig implements Parcelable { private final Surface mSurface; private final String mUniqueId; private final int mDisplayIdToMirror; - private final boolean mWindowManagerMirroring; - private ArrayList<String> mDisplayCategories = null; + private final boolean mWindowManagerMirroringEnabled; + private ArraySet<String> mDisplayCategories = null; + @Nullable + private ContentRecordingSession mContentRecordingSession; private final float mRequestedRefreshRate; private VirtualDisplayConfig( @@ -64,8 +67,9 @@ public final class VirtualDisplayConfig implements Parcelable { @Nullable Surface surface, @Nullable String uniqueId, int displayIdToMirror, - boolean windowManagerMirroring, - @NonNull ArrayList<String> displayCategories, + boolean windowManagerMirroringEnabled, + ContentRecordingSession session, + @NonNull ArraySet<String> displayCategories, float requestedRefreshRate) { mName = name; mWidth = width; @@ -75,7 +79,8 @@ public final class VirtualDisplayConfig implements Parcelable { mSurface = surface; mUniqueId = uniqueId; mDisplayIdToMirror = displayIdToMirror; - mWindowManagerMirroring = windowManagerMirroring; + mWindowManagerMirroringEnabled = windowManagerMirroringEnabled; + mContentRecordingSession = session; mDisplayCategories = displayCategories; mRequestedRefreshRate = requestedRefreshRate; } @@ -151,8 +156,19 @@ public final class VirtualDisplayConfig implements Parcelable { * if DisplayManager should record contents instead. * @hide */ - public boolean isWindowManagerMirroring() { - return mWindowManagerMirroring; + public boolean isWindowManagerMirroringEnabled() { + return mWindowManagerMirroringEnabled; + } + + /** + * Returns the recording session associated with this VirtualDisplay. Only used for + * recording via {@link MediaProjection}. + * + * @hide + */ + @Nullable + public ContentRecordingSession getContentRecordingSession() { + return mContentRecordingSession; } /** @@ -161,8 +177,8 @@ public final class VirtualDisplayConfig implements Parcelable { * @see Builder#setDisplayCategories */ @NonNull - public List<String> getDisplayCategories() { - return Collections.unmodifiableList(mDisplayCategories); + public Set<String> getDisplayCategories() { + return Collections.unmodifiableSet(mDisplayCategories); } /** @@ -185,8 +201,9 @@ public final class VirtualDisplayConfig implements Parcelable { dest.writeTypedObject(mSurface, flags); dest.writeString8(mUniqueId); dest.writeInt(mDisplayIdToMirror); - dest.writeBoolean(mWindowManagerMirroring); - dest.writeStringList(mDisplayCategories); + dest.writeBoolean(mWindowManagerMirroringEnabled); + dest.writeTypedObject(mContentRecordingSession, flags); + dest.writeArraySet(mDisplayCategories); dest.writeFloat(mRequestedRefreshRate); } @@ -210,7 +227,8 @@ public final class VirtualDisplayConfig implements Parcelable { && Objects.equals(mSurface, that.mSurface) && Objects.equals(mUniqueId, that.mUniqueId) && mDisplayIdToMirror == that.mDisplayIdToMirror - && mWindowManagerMirroring == that.mWindowManagerMirroring + && mWindowManagerMirroringEnabled == that.mWindowManagerMirroringEnabled + && Objects.equals(mContentRecordingSession, that.mContentRecordingSession) && Objects.equals(mDisplayCategories, that.mDisplayCategories) && mRequestedRefreshRate == that.mRequestedRefreshRate; } @@ -219,8 +237,8 @@ public final class VirtualDisplayConfig implements Parcelable { public int hashCode() { int hashCode = Objects.hash( mName, mWidth, mHeight, mDensityDpi, mFlags, mSurface, mUniqueId, - mDisplayIdToMirror, mWindowManagerMirroring, mDisplayCategories, - mRequestedRefreshRate); + mDisplayIdToMirror, mWindowManagerMirroringEnabled, mContentRecordingSession, + mDisplayCategories, mRequestedRefreshRate); return hashCode; } @@ -236,7 +254,8 @@ public final class VirtualDisplayConfig implements Parcelable { + " mSurface=" + mSurface + " mUniqueId=" + mUniqueId + " mDisplayIdToMirror=" + mDisplayIdToMirror - + " mWindowManagerMirroring=" + mWindowManagerMirroring + + " mWindowManagerMirroringEnabled=" + mWindowManagerMirroringEnabled + + " mContentRecordingSession=" + mContentRecordingSession + " mDisplayCategories=" + mDisplayCategories + " mRequestedRefreshRate=" + mRequestedRefreshRate + ")"; @@ -251,9 +270,9 @@ public final class VirtualDisplayConfig implements Parcelable { mSurface = in.readTypedObject(Surface.CREATOR); mUniqueId = in.readString8(); mDisplayIdToMirror = in.readInt(); - mWindowManagerMirroring = in.readBoolean(); - mDisplayCategories = new ArrayList<>(); - in.readStringList(mDisplayCategories); + mWindowManagerMirroringEnabled = in.readBoolean(); + mContentRecordingSession = in.readTypedObject(ContentRecordingSession.CREATOR); + mDisplayCategories = (ArraySet<String>) in.readArraySet(null); mRequestedRefreshRate = in.readFloat(); } @@ -283,8 +302,10 @@ public final class VirtualDisplayConfig implements Parcelable { private Surface mSurface = null; private String mUniqueId = null; private int mDisplayIdToMirror = DEFAULT_DISPLAY; - private boolean mWindowManagerMirroring = false; - private ArrayList<String> mDisplayCategories = new ArrayList<>(); + private boolean mWindowManagerMirroringEnabled = false; + @Nullable + private ContentRecordingSession mContentRecordingSession; + private ArraySet<String> mDisplayCategories = new ArraySet<>(); private float mRequestedRefreshRate = 0.0f; /** @@ -370,8 +391,20 @@ public final class VirtualDisplayConfig implements Parcelable { * @hide */ @NonNull - public Builder setWindowManagerMirroring(boolean windowManagerMirroring) { - mWindowManagerMirroring = windowManagerMirroring; + public Builder setWindowManagerMirroringEnabled(boolean windowManagerMirroringEnabled) { + mWindowManagerMirroringEnabled = windowManagerMirroringEnabled; + return this; + } + + /** + * Sets the recording session associated with this {@link VirtualDisplay}. Only used for + * recording via {@link MediaProjection}. + * + * @hide + */ + @NonNull + public Builder setContentRecordingSession(@Nullable ContentRecordingSession session) { + mContentRecordingSession = session; return this; } @@ -383,7 +416,7 @@ public final class VirtualDisplayConfig implements Parcelable { * {@link android.content.pm.ActivityInfo#requiredDisplayCategory}. */ @NonNull - public Builder setDisplayCategories(@NonNull List<String> displayCategories) { + public Builder setDisplayCategories(@NonNull Set<String> displayCategories) { mDisplayCategories.clear(); mDisplayCategories.addAll(Objects.requireNonNull(displayCategories)); return this; @@ -435,7 +468,8 @@ public final class VirtualDisplayConfig implements Parcelable { mSurface, mUniqueId, mDisplayIdToMirror, - mWindowManagerMirroring, + mWindowManagerMirroringEnabled, + mContentRecordingSession, mDisplayCategories, mRequestedRefreshRate); } diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index e2af9b03d7f5..cacde7f4a547 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -26,6 +26,8 @@ import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; import android.annotation.UserIdInt; import android.app.Activity; +import android.app.ActivityThread; +import android.app.OnActivityPausedListener; import android.app.PendingIntent; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; @@ -420,6 +422,7 @@ public final class NfcAdapter { // Guarded by NfcAdapter.class static boolean sIsInitialized = false; static boolean sHasNfcFeature; + static boolean sHasCeFeature; // Final after first constructor, except for // attemptDeadServiceRecovery() when NFC crashes - we accept a best effort @@ -616,11 +619,13 @@ public final class NfcAdapter { PackageManager pm; pm = context.getPackageManager(); sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC); - boolean hasHceFeature = + sHasCeFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION) - || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF); + || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF) + || pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC) + || pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE); /* is this device meant to have NFC */ - if (!sHasNfcFeature && !hasHceFeature) { + if (!sHasNfcFeature && !sHasCeFeature) { Log.v(TAG, "this device does not have NFC support"); throw new UnsupportedOperationException(); } @@ -643,7 +648,7 @@ public final class NfcAdapter { throw new UnsupportedOperationException(); } } - if (hasHceFeature) { + if (sHasCeFeature) { try { sNfcFCardEmulationService = sService.getNfcFCardEmulationInterface(); } catch (RemoteException e) { @@ -1467,11 +1472,17 @@ public final class NfcAdapter { if (activity == null || intent == null) { throw new NullPointerException(); } + if (!activity.isResumed()) { + throw new IllegalStateException("Foreground dispatch can only be enabled " + + "when your activity is resumed"); + } try { TechListParcel parcel = null; if (techLists != null && techLists.length > 0) { parcel = new TechListParcel(techLists); } + ActivityThread.currentActivityThread().registerOnActivityPausedListener(activity, + mForegroundDispatchListener); sService.setForegroundDispatch(intent, filters, parcel); } catch (RemoteException e) { attemptDeadServiceRecovery(e); @@ -1499,8 +1510,25 @@ public final class NfcAdapter { throw new UnsupportedOperationException(); } } + ActivityThread.currentActivityThread().unregisterOnActivityPausedListener(activity, + mForegroundDispatchListener); + disableForegroundDispatchInternal(activity, false); + } + + OnActivityPausedListener mForegroundDispatchListener = new OnActivityPausedListener() { + @Override + public void onPaused(Activity activity) { + disableForegroundDispatchInternal(activity, true); + } + }; + + void disableForegroundDispatchInternal(Activity activity, boolean force) { try { sService.setForegroundDispatch(null, null, null); + if (!force && !activity.isResumed()) { + throw new IllegalStateException("You must disable foreground dispatching " + + "while your activity is still resumed"); + } } catch (RemoteException e) { attemptDeadServiceRecovery(e); } @@ -1669,7 +1697,7 @@ public final class NfcAdapter { @SystemApi @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableSecureNfc(boolean enable) { - if (!sHasNfcFeature) { + if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } try { @@ -1694,10 +1722,13 @@ public final class NfcAdapter { * Checks if the device supports Secure NFC functionality. * * @return True if device supports Secure NFC, false otherwise - * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable */ public boolean isSecureNfcSupported() { - if (!sHasNfcFeature) { + if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } try { @@ -1723,11 +1754,14 @@ public final class NfcAdapter { * such as their relative positioning on the device. * * @return Information on the nfc antenna(s) on the device. - * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable */ @Nullable public NfcAntennaInfo getNfcAntennaInfo() { - if (!sHasNfcFeature) { + if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } try { @@ -1752,12 +1786,15 @@ public final class NfcAdapter { * Checks Secure NFC feature is enabled. * * @return True if Secure NFC is enabled, false otherwise - * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable * @throws UnsupportedOperationException if device doesn't support * Secure NFC functionality. {@link #isSecureNfcSupported} */ public boolean isSecureNfcEnabled() { - if (!sHasNfcFeature) { + if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } try { @@ -2071,14 +2108,17 @@ public final class NfcAdapter { * always on. * @param value if true the NFCC will be kept on (with no RF enabled if NFC adapter is * disabled), if false the NFCC will follow completely the Nfc adapter state. - * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable * @return void * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean setControllerAlwaysOn(boolean value) { - if (!sHasNfcFeature) { + if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } try { @@ -2103,7 +2143,10 @@ public final class NfcAdapter { * Checks NFC controller always on feature is enabled. * * @return True if NFC controller always on is enabled, false otherwise - * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable * @hide */ @SystemApi @@ -2131,13 +2174,16 @@ public final class NfcAdapter { * Checks if the device supports NFC controller always on functionality. * * @return True if device supports NFC controller always on, false otherwise - * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOnSupported() { - if (!sHasNfcFeature) { + if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } try { diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index 07d500176fe5..5b527c70b4f7 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -979,6 +979,19 @@ public abstract class DocumentsProvider extends ContentProvider { } /** + * An unrestricted version of getType, which does not reveal sensitive information + */ + @Override + public final @Nullable String getTypeAnonymous(@NonNull Uri uri) { + switch (mMatcher.match(uri)) { + case MATCH_ROOT: + return DocumentsContract.Root.MIME_TYPE_ITEM; + default: + return null; + } + } + + /** * Implementation is provided by the parent class. Can be overridden to * provide additional functionality, but subclasses <em>must</em> always * call the superclass. If the superclass returns {@code null}, the subclass diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ef007746a67f..045ba1f82b7c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5678,6 +5678,36 @@ public final class Settings { public static final String LOCALE_PREFERENCES = "locale_preferences"; /** + * Setting to enable camera flash notification feature. + * <ul> + * <li> 0 = Off + * <li> 1 = On + * </ul> + * @hide + */ + public static final String CAMERA_FLASH_NOTIFICATION = "camera_flash_notification"; + + /** + * Setting to enable screen flash notification feature. + * <ul> + * <li> 0 = Off + * <li> 1 = On + * </ul> + * @hide + */ + public static final String SCREEN_FLASH_NOTIFICATION = "screen_flash_notification"; + + /** + * Integer property that specifes the color for screen flash notification as a + * packed 32-bit color. + * + * @see android.graphics.Color#argb + * @hide + */ + public static final String SCREEN_FLASH_NOTIFICATION_COLOR = + "screen_flash_notification_color_global"; + + /** * IMPORTANT: If you add a new public settings you also have to add it to * PUBLIC_SETTINGS below. If the new setting is hidden you have to add * it to PRIVATE_SETTINGS below. Also add a validator that can validate @@ -5810,6 +5840,9 @@ public final class Settings { PRIVATE_SETTINGS.add(TOUCHPAD_NATURAL_SCROLLING); PRIVATE_SETTINGS.add(TOUCHPAD_TAP_TO_CLICK); PRIVATE_SETTINGS.add(TOUCHPAD_RIGHT_CLICK_ZONE); + PRIVATE_SETTINGS.add(CAMERA_FLASH_NOTIFICATION); + PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION); + PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION_COLOR); } /** diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java index d943bf9ac872..0ef8bb64acaf 100644 --- a/core/java/android/service/autofill/Dataset.java +++ b/core/java/android/service/autofill/Dataset.java @@ -29,6 +29,7 @@ import android.content.IntentSender; import android.os.Parcel; import android.os.Parcelable; import android.view.autofill.AutofillId; +import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; import android.widget.RemoteViews; @@ -283,24 +284,28 @@ public final class Dataset implements Parcelable { } /** @hide */ - public RemoteViews getFieldPresentation(int index) { + @TestApi + public @Nullable RemoteViews getFieldPresentation(int index) { final RemoteViews customPresentation = mFieldPresentations.get(index); return customPresentation != null ? customPresentation : mPresentation; } /** @hide */ - public RemoteViews getFieldDialogPresentation(int index) { + @TestApi + public @Nullable RemoteViews getFieldDialogPresentation(int index) { final RemoteViews customPresentation = mFieldDialogPresentations.get(index); return customPresentation != null ? customPresentation : mDialogPresentation; } /** @hide */ + @TestApi public @Nullable InlinePresentation getFieldInlinePresentation(int index) { final InlinePresentation inlinePresentation = mFieldInlinePresentations.get(index); return inlinePresentation != null ? inlinePresentation : mInlinePresentation; } /** @hide */ + @TestApi public @Nullable InlinePresentation getFieldInlineTooltipPresentation(int index) { final InlinePresentation inlineTooltipPresentation = mFieldInlineTooltipPresentations.get(index); @@ -309,6 +314,7 @@ public final class Dataset implements Parcelable { } /** @hide */ + @TestApi public @Nullable DatasetFieldFilter getFilter(int index) { return mFieldFilters.get(index); } @@ -389,6 +395,9 @@ public final class Dataset implements Parcelable { if (mAuthentication != null) { builder.append(", hasAuthentication"); } + if (mAutofillDatatypes != null) { + builder.append(", autofillDatatypes=").append(mAutofillDatatypes); + } return builder.append(']').toString(); } @@ -1090,8 +1099,7 @@ public final class Dataset implements Parcelable { * * @return this builder. */ - public @NonNull Dataset.Builder setField( - @NonNull String hint, @NonNull Field field) { + public @NonNull Dataset.Builder setField(@NonNull String hint, @NonNull Field field) { throwIfDestroyed(); final DatasetFieldFilter filter = field.getDatasetFieldFilter(); @@ -1111,6 +1119,23 @@ public final class Dataset implements Parcelable { } /** + * Adds a field to this Dataset that is relevant to all applicable hints. This is used to + * provide field information when autofill with platform detections is enabled. + * Platform detections are on when receiving a populated list from + * FillRequest#getHints(). + * + * @param field the fill information about the field. + * + * @throws IllegalStateException if {@link #build()} was already called + * or this builder also contains AutofillId information + * + * @return this builder. + */ + public @NonNull Dataset.Builder setFieldForAllHints(@NonNull Field field) { + return setField(AutofillManager.ANY_HINT, field); + } + + /** * Sets the value of a field with an <a href="#Filtering">explicit filter</a>, and using an * {@link InlinePresentation} to visualize it as an inline suggestion. * @@ -1304,7 +1329,7 @@ public final class Dataset implements Parcelable { parcel.createTypedArrayList(InlinePresentation.CREATOR); final ArrayList<DatasetFieldFilter> filters = parcel.createTypedArrayList(DatasetFieldFilter.CREATOR); - final ArrayList<String> datatypes = + final ArrayList<String> autofillDatatypes = parcel.createStringArrayList(); final ClipData fieldContent = parcel.readParcelable(null, android.content.ClipData.class); @@ -1341,9 +1366,9 @@ public final class Dataset implements Parcelable { } final int inlinePresentationsSize = inlinePresentations.size(); - if (ids.size() == 0 && datatypes.size() > 0) { - for (int i = 0; i < ids.size(); i++) { - final String datatype = datatypes.get(i); + if (ids.size() == 0 && autofillDatatypes.size() > 0) { + for (int i = 0; i < autofillDatatypes.size(); i++) { + final String datatype = autofillDatatypes.get(i); final AutofillValue value = values.get(i); final RemoteViews fieldPresentation = presentations.get(i); final RemoteViews fieldDialogPresentation = dialogPresentations.get(i); @@ -1393,8 +1418,10 @@ public final class Dataset implements Parcelable { * * @hide */ + @TestApi public static final class DatasetFieldFilter implements Parcelable { + /** @hide */ @Nullable public final Pattern pattern; @@ -1402,6 +1429,10 @@ public final class Dataset implements Parcelable { this.pattern = pattern; } + public @Nullable Pattern getPattern() { + return pattern; + } + @Override public String toString() { if (!sDebug) return super.toString(); @@ -1416,7 +1447,7 @@ public final class Dataset implements Parcelable { } @Override - public void writeToParcel(Parcel parcel, int flags) { + public void writeToParcel(@NonNull Parcel parcel, int flags) { parcel.writeSerializable(pattern); } diff --git a/core/java/android/service/credentials/BeginCreateCredentialResponse.java b/core/java/android/service/credentials/BeginCreateCredentialResponse.java index eebd31c6fa96..cd53cb6afc71 100644 --- a/core/java/android/service/credentials/BeginCreateCredentialResponse.java +++ b/core/java/android/service/credentials/BeginCreateCredentialResponse.java @@ -16,8 +16,10 @@ package android.service.credentials; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.os.Parcel; import android.os.Parcelable; @@ -137,7 +139,17 @@ public final class BeginCreateCredentialResponse implements Parcelable { * result should be set to {@link android.app.Activity#RESULT_OK} and an extra with the * {@link CredentialProviderService#EXTRA_CREATE_CREDENTIAL_RESPONSE} key should be populated * with a {@link android.credentials.CreateCredentialResponse} object. + * + * <p> Note that as a provider service you will only be able to set a remote entry if : + * - Provider service possesses the + * {@link Manifest.permission.PROVIDE_REMOTE_CREDENTIALS} permission. + * - Provider service is configured as the provider that can provide remote entries. + * + * If the above conditions are not met, setting back {@link BeginCreateCredentialResponse} + * on the callback from {@link CredentialProviderService#onBeginCreateCredential} + * will throw a {@link SecurityException}. */ + @RequiresPermission(Manifest.permission.PROVIDE_REMOTE_CREDENTIALS) public @NonNull Builder setRemoteCreateEntry(@Nullable RemoteEntry remoteCreateEntry) { mRemoteCreateEntry = remoteCreateEntry; return this; diff --git a/core/java/android/service/credentials/BeginGetCredentialResponse.java b/core/java/android/service/credentials/BeginGetCredentialResponse.java index 97f5079393a0..e25b6869605d 100644 --- a/core/java/android/service/credentials/BeginGetCredentialResponse.java +++ b/core/java/android/service/credentials/BeginGetCredentialResponse.java @@ -16,8 +16,10 @@ package android.service.credentials; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.os.Parcel; import android.os.Parcelable; @@ -154,7 +156,17 @@ public final class BeginGetCredentialResponse implements Parcelable { * result should be set to {@link android.app.Activity#RESULT_OK} and an extra with the * {@link CredentialProviderService#EXTRA_GET_CREDENTIAL_RESPONSE} key should be populated * with a {@link android.credentials.Credential} object. + * + * <p> Note that as a provider service you will only be able to set a remote entry if : + * - Provider service possesses the + * {@link Manifest.permission.PROVIDE_REMOTE_CREDENTIALS} permission. + * - Provider service is configured as the provider that can provide remote entries. + * + * If the above conditions are not met, setting back {@link BeginGetCredentialResponse} + * on the callback from {@link CredentialProviderService#onBeginGetCredential} will + * throw a {@link SecurityException}. */ + @RequiresPermission(Manifest.permission.PROVIDE_REMOTE_CREDENTIALS) public @NonNull Builder setRemoteCredentialEntry(@Nullable RemoteEntry remoteCredentialEntry) { mRemoteCredentialEntry = remoteCredentialEntry; diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java index 7e98bc7eb975..e9cebd2e6af7 100644 --- a/core/java/android/service/credentials/CredentialEntry.java +++ b/core/java/android/service/credentials/CredentialEntry.java @@ -27,6 +27,8 @@ import android.credentials.GetCredentialResponse; import android.os.Parcel; import android.os.Parcelable; +import com.android.internal.util.Preconditions; + /** * A credential entry that is to be displayed on the account selector that is presented to the * user. @@ -56,7 +58,7 @@ import android.os.Parcelable; @SuppressLint("ParcelNotFinal") public class CredentialEntry implements Parcelable { /** The request option that corresponds to this entry. **/ - private final @Nullable BeginGetCredentialOption mBeginGetCredentialOption; + private final @Nullable String mBeginGetCredentialOptionId; /** The type of the credential entry to be shown on the UI. */ private final @NonNull String mType; @@ -72,19 +74,51 @@ public class CredentialEntry implements Parcelable { * to respond to query phase {@link CredentialProviderService#onBeginGetCredential} * credential retrieval requests. * + * @param beginGetCredentialOptionId the beginGetCredentialOptionId to be retrieved from + * {@link BeginGetCredentialOption#getId()} - the request option for which this CredentialEntry + * is being constructed This helps maintain an association + * such that when the user selects this entry, providers can + * receive the complete corresponding + * {@link GetCredentialRequest}. + * @param type the type of the credential for which this credential entry is being created + * @param slice the slice containing the metadata to be shown on the UI. Must be + * constructed through the androidx.credentials jetpack library. + * + * @throws IllegalArgumentException If {@code beginGetCredentialOptionId} or {@code type} + * is null, or empty + */ + public CredentialEntry(@NonNull String beginGetCredentialOptionId, @NonNull String type, + @NonNull Slice slice) { + mBeginGetCredentialOptionId = Preconditions.checkStringNotEmpty( + beginGetCredentialOptionId, "beginGetCredentialOptionId must not be " + + "null, or empty"); + mType = Preconditions.checkStringNotEmpty(type, "type must not be null, or " + + "empty"); + mSlice = requireNonNull(slice, "slice must not be null"); + } + + /** + * Creates an entry that is associated with a {@link BeginGetCredentialOption} request. + * Providers must use this constructor when they extend from {@link CredentialProviderService} + * to respond to query phase {@link CredentialProviderService#onBeginGetCredential} + * credential retrieval requests. + * * @param beginGetCredentialOption the request option for which this credential entry is * being constructed This helps maintain an association, * such that when the user selects this entry, providers - * can receive the conmplete corresponding request. + * can receive the complete corresponding request. * @param slice the slice containing the metadata to be shown on the UI. Must be * constructed through the androidx.credentials jetpack library. */ public CredentialEntry(@NonNull BeginGetCredentialOption beginGetCredentialOption, @NonNull Slice slice) { - mBeginGetCredentialOption = requireNonNull(beginGetCredentialOption, - "beginGetCredentialOption must not be null"); - mType = requireNonNull(mBeginGetCredentialOption.getType(), - "type must not be null"); + requireNonNull(beginGetCredentialOption, "beginGetCredentialOption must not" + + " be null"); + mBeginGetCredentialOptionId = Preconditions.checkStringNotEmpty( + beginGetCredentialOption.getId(), "Id in beginGetCredentialOption " + + "must not be null"); + mType = Preconditions.checkStringNotEmpty(beginGetCredentialOption.getType(), + "type in beginGetCredentialOption must not be null"); mSlice = requireNonNull(slice, "slice must not be null"); } @@ -97,11 +131,9 @@ public class CredentialEntry implements Parcelable { * @param slice the slice containing the metadata to be shown on the UI. Must be * constructed through the androidx.credentials jetpack library. * - * @hide */ - // TODO: Unhide this constructor when the registry APIs are stable public CredentialEntry(@NonNull String type, @NonNull Slice slice) { - mBeginGetCredentialOption = null; + mBeginGetCredentialOptionId = null; mType = requireNonNull(type, "type must not be null"); mSlice = requireNonNull(slice, "slice must not be null"); } @@ -110,7 +142,7 @@ public class CredentialEntry implements Parcelable { requireNonNull(in, "parcel must not be null"); mType = in.readString8(); mSlice = in.readTypedObject(Slice.CREATOR); - mBeginGetCredentialOption = in.readTypedObject(BeginGetCredentialOption.CREATOR); + mBeginGetCredentialOptionId = in.readString8(); } @NonNull @@ -136,15 +168,16 @@ public class CredentialEntry implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString8(mType); dest.writeTypedObject(mSlice, flags); - dest.writeTypedObject(mBeginGetCredentialOption, flags); + dest.writeString8(mBeginGetCredentialOptionId); } /** - * Returns the request option for which this credential entry has been constructed. + * Returns the id of the {@link BeginGetCredentialOption} for which this credential + * entry has been constructed. */ @NonNull - public BeginGetCredentialOption getBeginGetCredentialOption() { - return mBeginGetCredentialOption; + public String getBeginGetCredentialOptionId() { + return mBeginGetCredentialOptionId; } /** diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfoFactory.java index b5464db98d0e..fd9360f00d4a 100644 --- a/core/java/android/service/credentials/CredentialProviderInfo.java +++ b/core/java/android/service/credentials/CredentialProviderInfoFactory.java @@ -34,40 +34,27 @@ import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.credentials.CredentialManager; -import android.graphics.drawable.Drawable; +import android.credentials.CredentialProviderInfo; import android.os.Bundle; import android.os.RemoteException; -import android.text.TextUtils; +import android.os.UserHandle; import android.util.Log; import android.util.Slog; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** - * {@link ServiceInfo} and meta-data about a credential provider. + * {@link CredentialProviderInfo} generator. * * @hide */ -public final class CredentialProviderInfo { - private static final String TAG = "CredentialProviderInfo"; - - @NonNull - private final ServiceInfo mServiceInfo; - @NonNull - private final List<String> mCapabilities; - - @NonNull - private final Context mContext; - @Nullable - private final Drawable mIcon; - @Nullable - private final CharSequence mLabel; - private final boolean mIsSystemProvider; +public final class CredentialProviderInfoFactory { + private static final String TAG = "CredentialProviderInfoFactory"; /** * Constructs an information instance of the credential provider. @@ -79,14 +66,18 @@ public final class CredentialProviderInfo { * @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, boolean isSystemProvider) + public static CredentialProviderInfo create( + @NonNull Context context, + @NonNull ComponentName serviceComponent, + int userId, + boolean isSystemProvider) throws PackageManager.NameNotFoundException { - this( + return create( context, getServiceInfoOrThrow(serviceComponent, userId), isSystemProvider, - /* disableSystemAppVerificationForTests= */ false); + /* disableSystemAppVerificationForTests= */ false, + /* isEnabled= */ false); } /** @@ -98,13 +89,16 @@ public final class CredentialProviderInfo { * @param isSystemProvider whether the provider app is a system provider * @param disableSystemAppVerificationForTests whether to disable system app permission * verification so that tests can install system providers + * @param isEnabled whether the user enabled this provider * @throws SecurityException If provider does not require the relevant permission */ - public CredentialProviderInfo( + public static CredentialProviderInfo create( @NonNull Context context, @NonNull ServiceInfo serviceInfo, boolean isSystemProvider, - boolean disableSystemAppVerificationForTests) { + boolean disableSystemAppVerificationForTests, + boolean isEnabled) + throws SecurityException { verifyProviderPermission(serviceInfo); if (isSystemProvider) { if (!isValidSystemProvider( @@ -114,23 +108,11 @@ public final class CredentialProviderInfo { "Provider is not a valid system provider: " + serviceInfo); } } - mIsSystemProvider = isSystemProvider; - mContext = requireNonNull(context, "context must not be null"); - mServiceInfo = requireNonNull(serviceInfo, "serviceInfo must not be null"); - mCapabilities = new ArrayList<>(); - mIcon = mServiceInfo.loadIcon(mContext.getPackageManager()); - mLabel = - mServiceInfo.loadSafeLabel( - mContext.getPackageManager(), - 0 /* do not ellipsize */, - TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM); - Log.i( - TAG, - "mLabel is : " - + mLabel - + ", for: " - + mServiceInfo.getComponentName().flattenToString()); - populateProviderCapabilities(context, serviceInfo); + + return populateMetadata(context, serviceInfo) + .setSystemProvider(isSystemProvider) + .setEnabled(isEnabled) + .build(); } private static void verifyProviderPermission(ServiceInfo serviceInfo) throws SecurityException { @@ -138,19 +120,14 @@ public final class CredentialProviderInfo { if (permission.equals(serviceInfo.permission)) { return; } - - Slog.e( - TAG, - "Credential Provider Service from : " - + serviceInfo.packageName - + "does not require permission" - + permission); throw new SecurityException( "Service does not require the expected permission : " + permission); } private static boolean isSystemProviderWithValidPermission( ServiceInfo serviceInfo, Context context) { + requireNonNull(context, "context must not be null"); + final String permission = Manifest.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE; try { ApplicationInfo appInfo = @@ -177,67 +154,88 @@ public final class CredentialProviderInfo { Context context, ServiceInfo serviceInfo, boolean disableSystemAppVerificationForTests) { - boolean isValidSystemTestProvider = - isTestSystemProvider(serviceInfo, disableSystemAppVerificationForTests); - if (isValidSystemTestProvider) { - return true; + requireNonNull(context, "context must not be null"); + + if (disableSystemAppVerificationForTests) { + Bundle metadata = serviceInfo.metaData; + if (metadata == null) { + Slog.e(TAG, "isValidSystemProvider - metadata is null: " + serviceInfo); + return false; + } + return metadata.getBoolean( + CredentialProviderService.TEST_SYSTEM_PROVIDER_META_DATA_KEY); } + return isSystemProviderWithValidPermission(serviceInfo, context); } - private static boolean isTestSystemProvider( - ServiceInfo serviceInfo, boolean disableSystemAppVerificationForTests) { - if (!disableSystemAppVerificationForTests) { - return false; - } + private static CredentialProviderInfo.Builder populateMetadata( + @NonNull Context context, ServiceInfo serviceInfo) { + requireNonNull(context, "context must not be null"); - Bundle metadata = serviceInfo.metaData; + final CredentialProviderInfo.Builder builder = + new CredentialProviderInfo.Builder(serviceInfo); + final PackageManager pm = context.getPackageManager(); + + // 1. Get the metadata for the service. + final Bundle metadata = serviceInfo.metaData; if (metadata == null) { - Slog.e(TAG, "metadata is null: " + serviceInfo); - return false; + Log.i(TAG, "populateMetadata - metadata is null"); + return builder; } - return metadata.getBoolean(CredentialProviderService.TEST_SYSTEM_PROVIDER_META_DATA_KEY); - } - private void populateProviderCapabilities(@NonNull Context context, ServiceInfo serviceInfo) { - final PackageManager pm = context.getPackageManager(); + // 2. Extract the capabilities from the bundle. try { - Bundle metadata = serviceInfo.metaData; Resources resources = pm.getResourcesForApplication(serviceInfo.applicationInfo); if (metadata == null || resources == null) { - Log.i(TAG, "populateProviderCapabilities - metadata or resources is null"); - return; - } - - String[] capabilities = resources.getStringArray(metadata.getInt( - CredentialProviderService.CAPABILITY_META_DATA_KEY)); - if (capabilities == null || capabilities.length == 0) { - Slog.i(TAG, "No capabilities found for provider:" + serviceInfo.packageName); - return; + Log.i(TAG, "populateMetadata - resources is null"); + return builder; } - for (String capability : capabilities) { - if (capability.isEmpty()) { - Slog.i(TAG, "Skipping empty capability"); - continue; - } - Slog.i(TAG, "Capabilities found for provider: " + capability); - mCapabilities.add(capability); - } + builder.addCapabilities(populateProviderCapabilities(resources, metadata, serviceInfo)); } catch (PackageManager.NameNotFoundException e) { Slog.e(TAG, e.getMessage()); + } + + return builder; + } + + private static List<String> populateProviderCapabilities( + Resources resources, Bundle metadata, ServiceInfo serviceInfo) { + List<String> output = new ArrayList<>(); + String[] capabilities = new String[0]; + + try { + capabilities = + resources.getStringArray( + metadata.getInt(CredentialProviderService.CAPABILITY_META_DATA_KEY)); } catch (Resources.NotFoundException e) { - Slog.e(TAG, e.getMessage()); + Slog.e(TAG, "Failed to get capabilities: " + e.getMessage()); + } + + if (capabilities == null || capabilities.length == 0) { + Slog.e(TAG, "No capabilities found for provider:" + serviceInfo.packageName); + return output; } + + for (String capability : capabilities) { + if (capability.isEmpty()) { + Slog.e(TAG, "Skipping empty capability"); + continue; + } + Slog.e(TAG, "Capabilities found for provider: " + capability); + output.add(capability); + } + return output; } - private static ServiceInfo getServiceInfoOrThrow(@NonNull ComponentName serviceComponent, - int userId) throws PackageManager.NameNotFoundException { + private static ServiceInfo getServiceInfoOrThrow( + @NonNull ComponentName serviceComponent, int userId) + throws PackageManager.NameNotFoundException { try { - ServiceInfo si = AppGlobals.getPackageManager().getServiceInfo( - serviceComponent, - PackageManager.GET_META_DATA, - userId); + ServiceInfo si = + AppGlobals.getPackageManager() + .getServiceInfo(serviceComponent, PackageManager.GET_META_DATA, userId); if (si != null) { return si; } @@ -256,6 +254,8 @@ public final class CredentialProviderInfo { @NonNull Context context, @UserIdInt int userId, boolean disableSystemAppVerificationForTests) { + requireNonNull(context, "context must not be null"); + final List<ServiceInfo> services = new ArrayList<>(); final List<ResolveInfo> resolveInfos = new ArrayList<>(); @@ -268,15 +268,20 @@ public final class CredentialProviderInfo { for (ResolveInfo resolveInfo : resolveInfos) { final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (disableSystemAppVerificationForTests) { + if (serviceInfo != null) { + services.add(serviceInfo); + } + continue; + } + try { - PackageManager.ApplicationInfoFlags appInfoFlags = - disableSystemAppVerificationForTests - ? PackageManager.ApplicationInfoFlags.of(0) - : PackageManager.ApplicationInfoFlags.of( - PackageManager.MATCH_SYSTEM_ONLY); ApplicationInfo appInfo = context.getPackageManager() - .getApplicationInfo(serviceInfo.packageName, appInfoFlags); + .getApplicationInfo( + serviceInfo.packageName, + PackageManager.ApplicationInfoFlags.of( + PackageManager.MATCH_SYSTEM_ONLY)); if (appInfo == null || serviceInfo == null) { continue; @@ -300,19 +305,22 @@ public final class CredentialProviderInfo { public static List<CredentialProviderInfo> getAvailableSystemServices( @NonNull Context context, @UserIdInt int userId, - boolean disableSystemAppVerificationForTests) { + boolean disableSystemAppVerificationForTests, + Set<ServiceInfo> enabledServices) { requireNonNull(context, "context must not be null"); + final List<CredentialProviderInfo> providerInfos = new ArrayList<>(); for (ServiceInfo si : getAvailableSystemServiceInfos( context, userId, disableSystemAppVerificationForTests)) { try { CredentialProviderInfo cpi = - new CredentialProviderInfo( + CredentialProviderInfoFactory.create( context, si, /* isSystemProvider= */ true, - disableSystemAppVerificationForTests); + disableSystemAppVerificationForTests, + enabledServices.contains(si)); if (cpi.isSystemProvider()) { providerInfos.add(cpi); } else { @@ -325,45 +333,12 @@ public final class CredentialProviderInfo { return providerInfos; } - /** - * Returns true if the service supports the given {@code credentialType}, false otherwise. - */ - @NonNull - public boolean hasCapability(@NonNull String credentialType) { - return mCapabilities.contains(credentialType); - } - - /** Returns the service info. */ - @NonNull - public ServiceInfo getServiceInfo() { - return mServiceInfo; - } - - public boolean isSystemProvider() { - return mIsSystemProvider; - } - - /** Returns the service icon. */ - @Nullable - public Drawable getServiceIcon() { - return mIcon; - } - - /** Returns the service label. */ - @Nullable - public CharSequence getServiceLabel() { - return mLabel; - } - - /** Returns an immutable list of capabilities this provider service can support. */ - @NonNull - public List<String> getCapabilities() { - return Collections.unmodifiableList(mCapabilities); - } + private static @Nullable PackagePolicy getDeviceManagerPolicy( + @NonNull Context context, int userId) { + Context newContext = context.createContextAsUser(UserHandle.of(userId), 0); - private static @Nullable PackagePolicy getDeviceManagerPolicy(@NonNull Context context) { try { - DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); + DevicePolicyManager dpm = newContext.getSystemService(DevicePolicyManager.class); return dpm.getCredentialManagerPolicy(); } catch (SecurityException e) { // If the current user is not enrolled in DPM then this can throw a security error. @@ -381,21 +356,53 @@ public final class CredentialProviderInfo { public static List<CredentialProviderInfo> getCredentialProviderServices( @NonNull Context context, int userId, - boolean disableSystemAppVerificationForTests, - int providerFilter) { + int providerFilter, + Set<ServiceInfo> enabledServices) { + requireNonNull(context, "context must not be null"); + + // Get the device policy. + PackagePolicy pp = getDeviceManagerPolicy(context, userId); + + // Generate the provider list. + final boolean disableSystemAppVerificationForTests = false; + ProviderGenerator generator = + new ProviderGenerator( + context, pp, disableSystemAppVerificationForTests, providerFilter); + generator.addUserProviders( + getUserProviders( + context, userId, disableSystemAppVerificationForTests, enabledServices)); + generator.addSystemProviders( + getAvailableSystemServices( + context, userId, disableSystemAppVerificationForTests, enabledServices)); + return generator.getProviders(); + } + + /** + * Returns the valid credential provider services available for the user with the given {@code + * userId}. Includes test providers. + */ + @NonNull + public static List<CredentialProviderInfo> getCredentialProviderServicesForTesting( + @NonNull Context context, + int userId, + int providerFilter, + Set<ServiceInfo> enabledServices) { requireNonNull(context, "context must not be null"); // Get the device policy. - PackagePolicy pp = getDeviceManagerPolicy(context); + PackagePolicy pp = getDeviceManagerPolicy(context, userId); // Generate the provider list. + final boolean disableSystemAppVerificationForTests = true; ProviderGenerator generator = new ProviderGenerator( context, pp, disableSystemAppVerificationForTests, providerFilter); generator.addUserProviders( - getUserProviders(context, userId, disableSystemAppVerificationForTests)); + getUserProviders( + context, userId, disableSystemAppVerificationForTests, enabledServices)); generator.addSystemProviders( - getAvailableSystemServices(context, userId, disableSystemAppVerificationForTests)); + getAvailableSystemServices( + context, userId, disableSystemAppVerificationForTests, enabledServices)); return generator.getProviders(); } @@ -484,7 +491,8 @@ public final class CredentialProviderInfo { private static List<CredentialProviderInfo> getUserProviders( @NonNull Context context, @UserIdInt int userId, - boolean disableSystemAppVerificationForTests) { + boolean disableSystemAppVerificationForTests, + Set<ServiceInfo> enabledServices) { final List<CredentialProviderInfo> services = new ArrayList<>(); final List<ResolveInfo> resolveInfos = context.getPackageManager() @@ -496,11 +504,12 @@ public final class CredentialProviderInfo { final ServiceInfo serviceInfo = resolveInfo.serviceInfo; try { CredentialProviderInfo cpi = - new CredentialProviderInfo( + CredentialProviderInfoFactory.create( context, serviceInfo, /* isSystemProvider= */ false, - disableSystemAppVerificationForTests); + disableSystemAppVerificationForTests, + enabledServices.contains(serviceInfo)); if (!cpi.isSystemProvider()) { services.add(cpi); } diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index d737f6b6cdc8..e88474d86798 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -18,6 +18,7 @@ package android.service.credentials; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; +import android.Manifest; import android.annotation.CallSuper; import android.annotation.NonNull; import android.annotation.SdkConstant; @@ -218,6 +219,11 @@ public abstract class CredentialProviderService extends Service { GetCredentialException>() { @Override public void onResult(BeginGetCredentialResponse result) { + // If provider service does not possess the HYBRID permission, this + // check will throw an exception in the provider process. + if (result.getRemoteCredentialEntry() != null) { + enforceRemoteEntryPermission(); + } try { callback.onSuccess(result); } catch (RemoteException e) { @@ -236,6 +242,15 @@ public abstract class CredentialProviderService extends Service { )); return transport; } + private void enforceRemoteEntryPermission() { + String permission = + Manifest.permission.PROVIDE_REMOTE_CREDENTIALS; + getApplicationContext().enforceCallingOrSelfPermission( + permission, + String.format("Provider must have %s, in order to set a " + + "remote entry", permission) + ); + } @Override public ICancellationSignal onBeginCreateCredential(BeginCreateCredentialRequest request, @@ -253,6 +268,11 @@ public abstract class CredentialProviderService extends Service { BeginCreateCredentialResponse, CreateCredentialException>() { @Override public void onResult(BeginCreateCredentialResponse result) { + // If provider service does not possess the HYBRID permission, this + // check will throw an exception in the provider process. + if (result.getRemoteCreateEntry() != null) { + enforceRemoteEntryPermission(); + } try { callback.onSuccess(result); } catch (RemoteException e) { diff --git a/core/java/android/service/quicksettings/TileService.java b/core/java/android/service/quicksettings/TileService.java index 94384b0c466d..f74f5332e8d3 100644 --- a/core/java/android/service/quicksettings/TileService.java +++ b/core/java/android/service/quicksettings/TileService.java @@ -533,7 +533,7 @@ public class TileService extends Service { * the calling package or if the calling user cannot act on behalf of the user from the * {@code context}.</li> * <li> {@link IllegalArgumentException} if the user of the {@code context} is not the - * current user.</li> + * current user. Only thrown for apps targeting {@link Build.VERSION_CODES#TIRAMISU}</li> * </ul> */ public static final void requestListeningState(Context context, ComponentName component) { diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java index 0384454bb69c..d9ee859dc66b 100644 --- a/core/java/android/service/voice/HotwordDetectionService.java +++ b/core/java/android/service/voice/HotwordDetectionService.java @@ -72,7 +72,7 @@ import java.util.function.IntConsumer; */ @SystemApi public abstract class HotwordDetectionService extends Service - implements SandboxedDetectionServiceBase { + implements SandboxedDetectionInitializer { private static final String TAG = "HotwordDetectionService"; private static final boolean DBG = false; @@ -89,21 +89,23 @@ public abstract class HotwordDetectionService extends Service /** * Indicates that the updated status is successful. * - * @deprecated Replaced with {@link SandboxedDetectionServiceBase#INITIALIZATION_STATUS_SUCCESS} + * @deprecated Replaced with + * {@link SandboxedDetectionInitializer#INITIALIZATION_STATUS_SUCCESS} */ @Deprecated public static final int INITIALIZATION_STATUS_SUCCESS = - SandboxedDetectionServiceBase.INITIALIZATION_STATUS_SUCCESS; + SandboxedDetectionInitializer.INITIALIZATION_STATUS_SUCCESS; /** * Indicates that the callback wasn’t invoked within the timeout. * This is used by system. * - * @deprecated Replaced with {@link SandboxedDetectionServiceBase#INITIALIZATION_STATUS_UNKNOWN} + * @deprecated Replaced with + * {@link SandboxedDetectionInitializer#INITIALIZATION_STATUS_UNKNOWN} */ @Deprecated public static final int INITIALIZATION_STATUS_UNKNOWN = - SandboxedDetectionServiceBase.INITIALIZATION_STATUS_UNKNOWN; + SandboxedDetectionInitializer.INITIALIZATION_STATUS_UNKNOWN; /** * Source for the given audio stream. @@ -259,7 +261,7 @@ public abstract class HotwordDetectionService extends Service * * @hide * @deprecated Replaced with - * {@link SandboxedDetectionServiceBase#getMaxCustomInitializationStatus()} + * {@link SandboxedDetectionInitializer#getMaxCustomInitializationStatus()} */ @SystemApi @Deprecated @@ -368,7 +370,7 @@ public abstract class HotwordDetectionService extends Service private void onUpdateStateInternal(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory, IRemoteCallback callback) { IntConsumer intConsumer = - SandboxedDetectionServiceBase.createInitializationStatusConsumer(callback); + SandboxedDetectionInitializer.createInitializationStatusConsumer(callback); onUpdateState(options, sharedMemory, UPDATE_TIMEOUT_MILLIS, intConsumer); } diff --git a/core/java/android/service/voice/HotwordDetector.java b/core/java/android/service/voice/HotwordDetector.java index 22d97b7bbcaa..93fcec14cf1c 100644 --- a/core/java/android/service/voice/HotwordDetector.java +++ b/core/java/android/service/voice/HotwordDetector.java @@ -283,9 +283,9 @@ public interface HotwordDetector { * * @param status Info about initialization state of {@link HotwordDetectionService} or * {@link VisualQueryDetectionService}; allowed values are - * {@link SandboxedDetectionServiceBase#INITIALIZATION_STATUS_SUCCESS}, - * 1<->{@link SandboxedDetectionServiceBase#getMaxCustomInitializationStatus()}, - * {@link SandboxedDetectionServiceBase#INITIALIZATION_STATUS_UNKNOWN}. + * {@link SandboxedDetectionInitializer#INITIALIZATION_STATUS_SUCCESS}, + * 1<->{@link SandboxedDetectionInitializer#getMaxCustomInitializationStatus()}, + * {@link SandboxedDetectionInitializer#INITIALIZATION_STATUS_UNKNOWN}. */ void onHotwordDetectionServiceInitialized(int status); diff --git a/core/java/android/service/voice/SandboxedDetectionServiceBase.java b/core/java/android/service/voice/SandboxedDetectionInitializer.java index 43331642bd1e..4a41968395a7 100644 --- a/core/java/android/service/voice/SandboxedDetectionServiceBase.java +++ b/core/java/android/service/voice/SandboxedDetectionInitializer.java @@ -28,12 +28,12 @@ import android.os.SharedMemory; import java.util.function.IntConsumer; /** - * Base for all sandboxed detection services, providing a common interface for initialization. + * Provides common initialzation methods for sandboxed detection services. * * @hide */ @SystemApi -public interface SandboxedDetectionServiceBase { +public interface SandboxedDetectionInitializer { /** * Indicates that the updated status is successful. @@ -77,7 +77,7 @@ public interface SandboxedDetectionServiceBase { if (callback != null) { intConsumer = value -> { - if (value > SandboxedDetectionServiceBase + if (value > SandboxedDetectionInitializer .getMaxCustomInitializationStatus()) { throw new IllegalArgumentException( "The initialization status is invalid for " + value); diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java index 1783186cd045..cbe7666ddf43 100644 --- a/core/java/android/service/voice/VisualQueryDetectionService.java +++ b/core/java/android/service/voice/VisualQueryDetectionService.java @@ -20,9 +20,11 @@ import android.annotation.DurationMillisLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.app.Service; import android.content.ContentCaptureOptions; +import android.content.Context; import android.content.Intent; import android.hardware.soundtrigger.SoundTrigger; import android.media.AudioFormat; @@ -35,6 +37,7 @@ import android.os.RemoteException; import android.os.SharedMemory; import android.speech.IRecognitionServiceManager; import android.util.Log; +import android.view.contentcapture.ContentCaptureManager; import android.view.contentcapture.IContentCaptureManager; import java.util.Objects; @@ -58,7 +61,7 @@ import java.util.function.IntConsumer; */ @SystemApi public abstract class VisualQueryDetectionService extends Service - implements SandboxedDetectionServiceBase { + implements SandboxedDetectionInitializer { private static final String TAG = VisualQueryDetectionService.class.getSimpleName(); @@ -79,6 +82,10 @@ public abstract class VisualQueryDetectionService extends Service public static final String KEY_INITIALIZATION_STATUS = "initialization_status"; private IDetectorSessionVisualQueryDetectionCallback mRemoteCallback = null; + @Nullable + private ContentCaptureManager mContentCaptureManager; + @Nullable + private IRecognitionServiceManager mIRecognitionServiceManager; private final ISandboxedDetectionService mInterface = new ISandboxedDetectionService.Stub() { @@ -139,15 +146,29 @@ public abstract class VisualQueryDetectionService extends Service @Override public void updateContentCaptureManager(IContentCaptureManager manager, ContentCaptureOptions options) { - Log.v(TAG, "Ignore #updateContentCaptureManager"); + mContentCaptureManager = new ContentCaptureManager( + VisualQueryDetectionService.this, manager, options); } @Override public void updateRecognitionServiceManager(IRecognitionServiceManager manager) { - Log.v(TAG, "Ignore #updateRecognitionServiceManager"); + mIRecognitionServiceManager = manager; } }; + @Override + @SuppressLint("OnNameExpected") + public @Nullable Object getSystemService(@ServiceName @NonNull String name) { + if (Context.CONTENT_CAPTURE_MANAGER_SERVICE.equals(name)) { + return mContentCaptureManager; + } else if (Context.SPEECH_RECOGNITION_SERVICE.equals(name) + && mIRecognitionServiceManager != null) { + return mIRecognitionServiceManager.asBinder(); + } else { + return super.getSystemService(name); + } + } + /** * {@inheritDoc} * @hide @@ -175,7 +196,7 @@ public abstract class VisualQueryDetectionService extends Service private void onUpdateStateInternal(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory, IRemoteCallback callback) { IntConsumer intConsumer = - SandboxedDetectionServiceBase.createInitializationStatusConsumer(callback); + SandboxedDetectionInitializer.createInitializationStatusConsumer(callback); onUpdateState(options, sharedMemory, UPDATE_TIMEOUT_MILLIS, intConsumer); } diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java index f0f6a4f829cf..7dc0687b549d 100644 --- a/core/java/android/service/voice/VisualQueryDetector.java +++ b/core/java/android/service/voice/VisualQueryDetector.java @@ -201,9 +201,10 @@ public class VisualQueryDetector { * short amount of time to report its initialization state. * * @param status Info about initialization state of {@link VisualQueryDetectionService}; the - * allowed values are {@link SandboxedDetectionServiceBase#INITIALIZATION_STATUS_SUCCESS}, - * 1<->{@link SandboxedDetectionServiceBase#getMaxCustomInitializationStatus()}, - * {@link SandboxedDetectionServiceBase#INITIALIZATION_STATUS_UNKNOWN}. + * allowed values are + * {@link SandboxedDetectionInitializer#INITIALIZATION_STATUS_SUCCESS}, + * 1<->{@link SandboxedDetectionInitializer#getMaxCustomInitializationStatus()}, + * {@link SandboxedDetectionInitializer#INITIALIZATION_STATUS_UNKNOWN}. */ void onVisualQueryDetectionServiceInitialized(int status); diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 26bc5d12d3b9..259012f5eb30 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -60,7 +60,6 @@ import android.hardware.display.DisplayManager.DisplayListener; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; @@ -181,9 +180,6 @@ public abstract class WallpaperService extends Service { private final ArrayMap<IBinder, IWallpaperEngineWrapper> mActiveEngines = new ArrayMap<>(); - private Handler mBackgroundHandler; - private HandlerThread mBackgroundThread; - static final class WallpaperCommand { String action; int x; @@ -202,6 +198,14 @@ public abstract class WallpaperService extends Service { */ public class Engine { IWallpaperEngineWrapper mIWallpaperEngine; + final ArraySet<RectF> mLocalColorAreas = new ArraySet<>(4); + final ArraySet<RectF> mLocalColorsToAdd = new ArraySet<>(4); + + // 2D matrix [x][y] to represent a page of a portion of a window + EngineWindowPage[] mWindowPages = new EngineWindowPage[0]; + Bitmap mLastScreenshot; + int mLastWindowPage = -1; + private boolean mResetWindowPages; // Copies from mIWallpaperEngine. HandlerCaller mCaller; @@ -262,27 +266,11 @@ public abstract class WallpaperService extends Service { final Object mLock = new Object(); boolean mOffsetMessageEnqueued; - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) float mPendingXOffset; float mPendingYOffset; float mPendingXOffsetStep; float mPendingYOffsetStep; - - /** - * local color extraction related fields - * to be used by the background thread only (except the atomic boolean) - */ - final ArraySet<RectF> mLocalColorAreas = new ArraySet<>(4); - final ArraySet<RectF> mLocalColorsToAdd = new ArraySet<>(4); - private long mLastProcessLocalColorsTimestamp; - private AtomicBoolean mProcessLocalColorsPending = new AtomicBoolean(false); - private int mPixelCopyCount = 0; - // 2D matrix [x][y] to represent a page of a portion of a window - EngineWindowPage[] mWindowPages = new EngineWindowPage[0]; - Bitmap mLastScreenshot; - private boolean mResetWindowPages; - boolean mPendingSync; MotionEvent mPendingMove; boolean mIsInAmbientMode; @@ -291,8 +279,12 @@ public abstract class WallpaperService extends Service { private long mLastColorInvalidation; private final Runnable mNotifyColorsChanged = this::notifyColorsChanged; + // used to throttle processLocalColors + private long mLastProcessLocalColorsTimestamp; + private AtomicBoolean mProcessLocalColorsPending = new AtomicBoolean(false); private final Supplier<Long> mClockFunction; private final Handler mHandler; + private Display mDisplay; private Context mDisplayContext; private int mDisplayState; @@ -862,7 +854,7 @@ public abstract class WallpaperService extends Service { + "was not established."); } mResetWindowPages = true; - processLocalColors(); + processLocalColors(mPendingXOffset, mPendingXOffsetStep); } catch (RemoteException e) { Log.w(TAG, "Can't notify system because wallpaper connection was lost.", e); } @@ -1400,7 +1392,7 @@ public abstract class WallpaperService extends Service { resetWindowPages(); mSession.finishDrawing(mWindow, null /* postDrawTransaction */, Integer.MAX_VALUE); - processLocalColors(); + processLocalColors(mPendingXOffset, mPendingXOffsetStep); } reposition(); reportEngineShown(shouldWaitForEngineShown()); @@ -1544,7 +1536,7 @@ public abstract class WallpaperService extends Service { if (!mDestroyed) { mVisible = visible; reportVisibility(false); - if (mReportedVisible) processLocalColors(); + if (mReportedVisible) processLocalColors(mPendingXOffset, mPendingXOffsetStep); } else { AnimationHandler.requestAnimatorsEnabled(visible, this); } @@ -1647,41 +1639,31 @@ public abstract class WallpaperService extends Service { } // setup local color extraction data - processLocalColors(); + processLocalColors(xOffset, xOffsetStep); } /** * Thread-safe util to call {@link #processLocalColorsInternal} with a minimum interval of * {@link #PROCESS_LOCAL_COLORS_INTERVAL_MS} between two calls. */ - private void processLocalColors() { + private void processLocalColors(float xOffset, float xOffsetStep) { if (mProcessLocalColorsPending.compareAndSet(false, true)) { final long now = mClockFunction.get(); final long timeSinceLastColorProcess = now - mLastProcessLocalColorsTimestamp; final long timeToWait = Math.max(0, PROCESS_LOCAL_COLORS_INTERVAL_MS - timeSinceLastColorProcess); - mBackgroundHandler.postDelayed(() -> { + mHandler.postDelayed(() -> { mLastProcessLocalColorsTimestamp = now + timeToWait; mProcessLocalColorsPending.set(false); - processLocalColorsInternal(); + processLocalColorsInternal(xOffset, xOffsetStep); }, timeToWait); } } - private void processLocalColorsInternal() { + private void processLocalColorsInternal(float xOffset, float xOffsetStep) { // implemented by the wallpaper if (supportsLocalColorExtraction()) return; - assertBackgroundThread(); - float xOffset; - float xOffsetStep; - float wallpaperDimAmount; - synchronized (mLock) { - xOffset = mPendingXOffset; - xOffsetStep = mPendingXOffsetStep; - wallpaperDimAmount = mWallpaperDimAmount; - } - if (DEBUG) { Log.d(TAG, "processLocalColors " + xOffset + " of step " + xOffsetStep); @@ -1744,7 +1726,7 @@ public abstract class WallpaperService extends Service { xPage = mWindowPages.length - 1; } current = mWindowPages[xPage]; - updatePage(current, xPage, xPages, wallpaperDimAmount); + updatePage(current, xPage, xPages, finalXOffsetStep); Trace.endSection(); } @@ -1764,23 +1746,16 @@ public abstract class WallpaperService extends Service { } } - /** - * Must be called with the surface lock held. - * Must not be called if the surface is not valid. - * Will unlock the surface when done using it. - */ void updatePage(EngineWindowPage currentPage, int pageIndx, int numPages, - float wallpaperDimAmount) { - - assertBackgroundThread(); - + float xOffsetStep) { // in case the clock is zero, we start with negative time long current = SystemClock.elapsedRealtime() - DEFAULT_UPDATE_SCREENSHOT_DURATION; long lapsed = current - currentPage.getLastUpdateTime(); // Always update the page when the last update time is <= 0 // This is important especially when the device first boots - if (lapsed < DEFAULT_UPDATE_SCREENSHOT_DURATION) return; - + if (lapsed < DEFAULT_UPDATE_SCREENSHOT_DURATION) { + return; + } Surface surface = mSurfaceHolder.getSurface(); if (!surface.isValid()) return; boolean widthIsLarger = mSurfaceSize.x > mSurfaceSize.y; @@ -1796,42 +1771,33 @@ public abstract class WallpaperService extends Service { Bitmap screenShot = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); final Bitmap finalScreenShot = screenShot; - final String pixelCopySectionName = "WallpaperService#pixelCopy"; - final int pixelCopyCount = mPixelCopyCount++; - Trace.beginAsyncSection(pixelCopySectionName, pixelCopyCount); - try { - PixelCopy.request(surface, screenShot, (res) -> { - Trace.endAsyncSection(pixelCopySectionName, pixelCopyCount); - if (DEBUG) Log.d(TAG, "result of pixel copy is " + res); - if (res != PixelCopy.SUCCESS) { - Bitmap lastBitmap = currentPage.getBitmap(); - // assign the last bitmap taken for now - currentPage.setBitmap(mLastScreenshot); - Bitmap lastScreenshot = mLastScreenshot; - if (lastScreenshot != null && !lastScreenshot.isRecycled() - && !Objects.equals(lastBitmap, lastScreenshot)) { - updatePageColors(currentPage, pageIndx, numPages, wallpaperDimAmount); - } - } else { - mLastScreenshot = finalScreenShot; - // going to hold this lock for a while - currentPage.setBitmap(finalScreenShot); - currentPage.setLastUpdateTime(current); - updatePageColors(currentPage, pageIndx, numPages, wallpaperDimAmount); + Trace.beginSection("WallpaperService#pixelCopy"); + PixelCopy.request(surface, screenShot, (res) -> { + Trace.endSection(); + if (DEBUG) Log.d(TAG, "result of pixel copy is " + res); + if (res != PixelCopy.SUCCESS) { + Bitmap lastBitmap = currentPage.getBitmap(); + // assign the last bitmap taken for now + currentPage.setBitmap(mLastScreenshot); + Bitmap lastScreenshot = mLastScreenshot; + if (lastScreenshot != null && !lastScreenshot.isRecycled() + && !Objects.equals(lastBitmap, lastScreenshot)) { + updatePageColors(currentPage, pageIndx, numPages, xOffsetStep); } - }, mBackgroundHandler); - } catch (IllegalArgumentException e) { - // this can potentially happen if the surface is invalidated right between the - // surface.isValid() check and the PixelCopy operation. - // in this case, stop: we'll compute colors on the next processLocalColors call. - Log.i(TAG, "Cancelling processLocalColors: exception caught during PixelCopy"); - } + } else { + mLastScreenshot = finalScreenShot; + // going to hold this lock for a while + currentPage.setBitmap(finalScreenShot); + currentPage.setLastUpdateTime(current); + updatePageColors(currentPage, pageIndx, numPages, xOffsetStep); + } + }, mHandler); + } // locked by the passed page - private void updatePageColors( - EngineWindowPage page, int pageIndx, int numPages, float wallpaperDimAmount) { + private void updatePageColors(EngineWindowPage page, int pageIndx, int numPages, + float xOffsetStep) { if (page.getBitmap() == null) return; - assertBackgroundThread(); Trace.beginSection("WallpaperService#updatePageColors"); if (DEBUG) { Log.d(TAG, "updatePageColorsLocked for page " + pageIndx + " with areas " @@ -1853,7 +1819,7 @@ public abstract class WallpaperService extends Service { Log.e(TAG, "Error creating page local color bitmap", e); continue; } - WallpaperColors color = WallpaperColors.fromBitmap(target, wallpaperDimAmount); + WallpaperColors color = WallpaperColors.fromBitmap(target, mWallpaperDimAmount); target.recycle(); WallpaperColors currentColor = page.getColors(area); @@ -1870,26 +1836,17 @@ public abstract class WallpaperService extends Service { + " local color callback for area" + area + " for page " + pageIndx + " of " + numPages); } - mHandler.post(() -> { - try { - mConnection.onLocalWallpaperColorsChanged(area, color, - mDisplayContext.getDisplayId()); - } catch (RemoteException e) { - Log.e(TAG, "Error calling Connection.onLocalWallpaperColorsChanged", e); - } - }); + try { + mConnection.onLocalWallpaperColorsChanged(area, color, + mDisplayContext.getDisplayId()); + } catch (RemoteException e) { + Log.e(TAG, "Error calling Connection.onLocalWallpaperColorsChanged", e); + } } } Trace.endSection(); } - private void assertBackgroundThread() { - if (!mBackgroundHandler.getLooper().isCurrentThread()) { - throw new IllegalStateException( - "ProcessLocalColors should be called from the background thread"); - } - } - private RectF generateSubRect(RectF in, int pageInx, int numPages) { float minLeft = (float) (pageInx) / (float) (numPages); float maxRight = (float) (pageInx + 1) / (float) (numPages); @@ -1914,6 +1871,7 @@ public abstract class WallpaperService extends Service { if (supportsLocalColorExtraction()) return; if (!mResetWindowPages) return; mResetWindowPages = false; + mLastWindowPage = -1; for (int i = 0; i < mWindowPages.length; i++) { mWindowPages[i].setLastUpdateTime(0L); } @@ -1939,10 +1897,12 @@ public abstract class WallpaperService extends Service { if (DEBUG) { Log.d(TAG, "addLocalColorsAreas adding local color areas " + regions); } - mBackgroundHandler.post(() -> { + mHandler.post(() -> { mLocalColorsToAdd.addAll(regions); - processLocalColors(); + processLocalColors(mPendingXOffset, mPendingYOffset); }); + + } /** @@ -1952,7 +1912,7 @@ public abstract class WallpaperService extends Service { */ public void removeLocalColorsAreas(@NonNull List<RectF> regions) { if (supportsLocalColorExtraction()) return; - mBackgroundHandler.post(() -> { + mHandler.post(() -> { float step = mPendingXOffsetStep; mLocalColorsToAdd.removeAll(regions); mLocalColorAreas.removeAll(regions); @@ -2618,9 +2578,6 @@ public abstract class WallpaperService extends Service { @Override public void onCreate() { Trace.beginSection("WPMS.onCreate"); - mBackgroundThread = new HandlerThread("DefaultWallpaperLocalColorExtractor"); - mBackgroundThread.start(); - mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); super.onCreate(); Trace.endSection(); } @@ -2633,7 +2590,6 @@ public abstract class WallpaperService extends Service { engineWrapper.destroy(); } mActiveEngines.clear(); - mBackgroundThread.quitSafely(); Trace.endSection(); } diff --git a/core/java/android/text/GraphemeClusterSegmentFinder.java b/core/java/android/text/GraphemeClusterSegmentFinder.java index 656774f66792..0f6fdaf23c65 100644 --- a/core/java/android/text/GraphemeClusterSegmentFinder.java +++ b/core/java/android/text/GraphemeClusterSegmentFinder.java @@ -18,7 +18,8 @@ package android.text; import android.annotation.IntRange; import android.annotation.NonNull; -import android.graphics.Paint; +import android.graphics.TemporaryBuffer; +import android.graphics.text.GraphemeBreak; /** * Implementation of {@code SegmentFinder} using grapheme clusters as the text segment. Whitespace @@ -31,8 +32,8 @@ import android.graphics.Paint; * Segmentation - Grapheme Cluster Boundaries</a> */ public class GraphemeClusterSegmentFinder extends SegmentFinder { - private final CharSequence mText; - private final TextPaint mTextPaint; + private static AutoGrowArray.FloatArray sTempAdvances = null; + private final boolean[] mIsGraphemeBreak; /** * Constructs a GraphemeClusterSegmentFinder instance for the specified text which uses the @@ -43,51 +44,73 @@ public class GraphemeClusterSegmentFinder extends SegmentFinder { */ public GraphemeClusterSegmentFinder( @NonNull CharSequence text, @NonNull TextPaint textPaint) { - mText = text; - mTextPaint = textPaint; + + if (sTempAdvances == null) { + sTempAdvances = new AutoGrowArray.FloatArray(text.length()); + } else if (sTempAdvances.size() < text.length()) { + sTempAdvances.resize(text.length()); + } + + mIsGraphemeBreak = new boolean[text.length()]; + float[] advances = sTempAdvances.getRawArray(); + char[] chars = TemporaryBuffer.obtain(text.length()); + + TextUtils.getChars(text, 0, text.length(), chars, 0); + + textPaint.getTextWidths(chars, 0, text.length(), advances); + + GraphemeBreak.isGraphemeBreak(advances, chars, /* start= */ 0, /* end= */ text.length(), + mIsGraphemeBreak); + TemporaryBuffer.recycle(chars); + } + + private int previousBoundary(@IntRange(from = 0) int offset) { + if (offset <= 0) return DONE; + do { + --offset; + } while (offset > 0 && !mIsGraphemeBreak[offset]); + return offset; + } + + private int nextBoundary(@IntRange(from = 0) int offset) { + if (offset >= mIsGraphemeBreak.length) return DONE; + do { + ++offset; + } while (offset < mIsGraphemeBreak.length && !mIsGraphemeBreak[offset]); + return offset; } @Override public int previousStartBoundary(@IntRange(from = 0) int offset) { - if (offset == 0) return DONE; - int boundary = mTextPaint.getTextRunCursor( - mText, 0, mText.length(), false, offset, Paint.CURSOR_BEFORE); - return boundary == -1 ? DONE : boundary; + return previousBoundary(offset); } @Override public int previousEndBoundary(@IntRange(from = 0) int offset) { if (offset == 0) return DONE; - int boundary = mTextPaint.getTextRunCursor( - mText, 0, mText.length(), false, offset, Paint.CURSOR_BEFORE); + int boundary = previousBoundary(offset); // Check that there is another cursor position before, otherwise this is not a valid // end boundary. - if (mTextPaint.getTextRunCursor( - mText, 0, mText.length(), false, boundary, Paint.CURSOR_BEFORE) == -1) { + if (boundary == DONE || previousBoundary(boundary) == DONE) { return DONE; } - return boundary == -1 ? DONE : boundary; + return boundary; } @Override public int nextStartBoundary(@IntRange(from = 0) int offset) { - if (offset == mText.length()) return DONE; - int boundary = mTextPaint.getTextRunCursor( - mText, 0, mText.length(), false, offset, Paint.CURSOR_AFTER); + if (offset == mIsGraphemeBreak.length) return DONE; + int boundary = nextBoundary(offset); // Check that there is another cursor position after, otherwise this is not a valid // start boundary. - if (mTextPaint.getTextRunCursor( - mText, 0, mText.length(), false, boundary, Paint.CURSOR_AFTER) == -1) { + if (boundary == DONE || nextBoundary(boundary) == DONE) { return DONE; } - return boundary == -1 ? DONE : boundary; + return boundary; } @Override public int nextEndBoundary(@IntRange(from = 0) int offset) { - if (offset == mText.length()) return DONE; - int boundary = mTextPaint.getTextRunCursor( - mText, 0, mText.length(), false, offset, Paint.CURSOR_AFTER); - return boundary == -1 ? DONE : boundary; + return nextBoundary(offset); } } diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index 2ae882c81b50..6201b3a91eff 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -220,9 +220,9 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "true"); DEFAULT_FLAGS.put(SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, "true"); DEFAULT_FLAGS.put(SETTINGS_AUTO_TEXT_WRAPPING, "false"); - DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_UI, "false"); - DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "false"); - DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "false"); + DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_UI, "true"); + DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "true"); + DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "true"); DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE, "false"); DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA, "true"); DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA_PHASE2, "false"); diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java index 07ac5977c4e3..b4675e0127de 100644 --- a/core/java/android/view/DisplayEventReceiver.java +++ b/core/java/android/view/DisplayEventReceiver.java @@ -81,7 +81,10 @@ public abstract class DisplayEventReceiver { // GC'd while the native peer of the receiver is using them. private MessageQueue mMessageQueue; + private final VsyncEventData mVsyncEventData = new VsyncEventData(); + private static native long nativeInit(WeakReference<DisplayEventReceiver> receiver, + WeakReference<VsyncEventData> vsyncEventData, MessageQueue messageQueue, int vsyncSource, int eventRegistration, long layerHandle); private static native long nativeGetDisplayEventReceiverFinalizer(); @FastNative @@ -124,7 +127,9 @@ public abstract class DisplayEventReceiver { } mMessageQueue = looper.getQueue(); - mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue, + mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), + new WeakReference<VsyncEventData>(mVsyncEventData), + mMessageQueue, vsyncSource, eventRegistration, layerHandle); mFreeNativeResources = sNativeAllocationRegistry.registerNativeAllocation(this, mReceiverPtr); @@ -142,9 +147,6 @@ public abstract class DisplayEventReceiver { } static final class VsyncEventData { - - static final FrameTimeline[] INVALID_FRAME_TIMELINES = - {new FrameTimeline(FrameInfo.INVALID_VSYNC_ID, Long.MAX_VALUE, Long.MAX_VALUE)}; // The amount of frame timeline choices. // Must be in sync with VsyncEventData::kFrameTimelinesLength in // frameworks/native/libs/gui/include/gui/VsyncEventData.h. If they do not match, a runtime @@ -152,6 +154,10 @@ public abstract class DisplayEventReceiver { static final int FRAME_TIMELINES_LENGTH = 7; public static class FrameTimeline { + FrameTimeline() {} + + // Called from native code. + @SuppressWarnings("unused") FrameTimeline(long vsyncId, long expectedPresentationTime, long deadline) { this.vsyncId = vsyncId; this.expectedPresentationTime = expectedPresentationTime; @@ -160,14 +166,14 @@ public abstract class DisplayEventReceiver { // The frame timeline vsync id, used to correlate a frame // produced by HWUI with the timeline data stored in Surface Flinger. - public final long vsyncId; + public long vsyncId = FrameInfo.INVALID_VSYNC_ID; // The frame timestamp for when the frame is expected to be presented. - public final long expectedPresentationTime; + public long expectedPresentationTime = Long.MAX_VALUE; // The frame deadline timestamp in {@link System#nanoTime()} timebase that it is // allotted for the frame to be completed. - public final long deadline; + public long deadline = Long.MAX_VALUE; } /** @@ -175,11 +181,18 @@ public abstract class DisplayEventReceiver { * {@link FrameInfo#VSYNC} to the current vsync in case Choreographer callback was heavily * delayed by the app. */ - public final long frameInterval; + public long frameInterval = -1; public final FrameTimeline[] frameTimelines; - public final int preferredFrameTimelineIndex; + public int preferredFrameTimelineIndex = 0; + + VsyncEventData() { + frameTimelines = new FrameTimeline[FRAME_TIMELINES_LENGTH]; + for (int i = 0; i < frameTimelines.length; i++) { + frameTimelines[i] = new FrameTimeline(); + } + } // Called from native code. @SuppressWarnings("unused") @@ -190,12 +203,6 @@ public abstract class DisplayEventReceiver { this.frameInterval = frameInterval; } - VsyncEventData() { - this.frameInterval = -1; - this.frameTimelines = INVALID_FRAME_TIMELINES; - this.preferredFrameTimelineIndex = 0; - } - public FrameTimeline preferredFrameTimeline() { return frameTimelines[preferredFrameTimelineIndex]; } @@ -299,9 +306,8 @@ public abstract class DisplayEventReceiver { // Called from native code. @SuppressWarnings("unused") - private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame, - VsyncEventData vsyncEventData) { - onVsync(timestampNanos, physicalDisplayId, frame, vsyncEventData); + private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame) { + onVsync(timestampNanos, physicalDisplayId, frame, mVsyncEventData); } // Called from native code. diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java index 406f446156a9..24a0355dd10e 100644 --- a/core/java/android/view/InputWindowHandle.java +++ b/core/java/android/view/InputWindowHandle.java @@ -244,6 +244,10 @@ public final class InputWindowHandle { window = iwindow; } + public @Nullable IBinder getWindowToken() { + return windowToken; + } + public IWindow getWindow() { if (window != null) { return window; diff --git a/core/java/android/view/MotionPredictor.java b/core/java/android/view/MotionPredictor.java index 4d32efea9081..7d452f954fb0 100644 --- a/core/java/android/view/MotionPredictor.java +++ b/core/java/android/view/MotionPredictor.java @@ -17,14 +17,11 @@ package android.view; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import libcore.util.NativeAllocationRegistry; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - /** * Calculate motion predictions. * @@ -68,9 +65,12 @@ public final class MotionPredictor { /** * Record a movement so that in the future, a prediction for the current gesture can be - * generated. Ensure to add all motions from the gesture of interest to generate correct - * predictions. + * generated. Only gestures from one input device at a time should be provided to an instance of + * MotionPredictor. + * * @param event The received event + * + * @throws IllegalArgumentException if an inconsistent MotionEvent stream is sent. */ public void record(@NonNull MotionEvent event) { if (!isPredictionEnabled()) { @@ -80,28 +80,24 @@ public final class MotionPredictor { } /** - * Get predicted events for all gestures that have been provided to {@link #record}. - * If events from multiple devices were sent to 'record', this will produce a separate - * {@link MotionEvent} for each device. The returned list may be empty if no predictions for - * any of the added events/devices are available. + * Get a predicted event for the gesture that has been provided to {@link #record}. * Predictions may not reach the requested timestamp if the confidence in the prediction results * is low. * * @param predictionTimeNanos The time that the prediction should target, in the * {@link android.os.SystemClock#uptimeMillis} time base, but in nanoseconds. * - * @return A list of predicted motion events, with at most one for each device observed by - * {@link #record}. Be sure to check the historical data in addition to the latest - * ({@link MotionEvent#getX getX()}, {@link MotionEvent#getY getY()}) coordinates for smooth - * prediction curves. An empty list is returned if predictions are not supported, or not - * possible for the current set of gestures. + * @return The predicted motion event, or `null` if predictions are not supported, or not + * possible for the current gesture. Be sure to check the historical data in addition to the + * latest ({@link MotionEvent#getX getX()}, {@link MotionEvent#getY getY()}) coordinates for + * smooth prediction curves. */ - @NonNull - public List<MotionEvent> predict(long predictionTimeNanos) { + @Nullable + public MotionEvent predict(long predictionTimeNanos) { if (!isPredictionEnabled()) { - return Collections.emptyList(); + return null; } - return Arrays.asList(nativePredict(mPtr, predictionTimeNanos)); + return nativePredict(mPtr, predictionTimeNanos); } private boolean isPredictionEnabled() { @@ -129,7 +125,7 @@ public final class MotionPredictor { private static native long nativeInitialize(int offsetNanos); private static native void nativeRecord(long nativePtr, MotionEvent event); - private static native MotionEvent[] nativePredict(long nativePtr, long predictionTimeNanos); + private static native MotionEvent nativePredict(long nativePtr, long predictionTimeNanos); private static native boolean nativeIsPredictionAvailable(long nativePtr, int deviceId, int source); private static native long nativeGetNativeMotionPredictorFinalizer(); diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java index 85aea85907b5..4a7ed644f9e2 100644 --- a/core/java/android/view/VelocityTracker.java +++ b/core/java/android/view/VelocityTracker.java @@ -32,9 +32,10 @@ import java.util.Map; * * Use {@link #obtain} to retrieve a new instance of the class when you are going * to begin tracking. Put the motion events you receive into it with - * {@link #addMovement(MotionEvent)}. When you want to determine the velocity call - * {@link #computeCurrentVelocity(int)} and then call {@link #getXVelocity(int)} - * and {@link #getYVelocity(int)} to retrieve the velocity for each pointer id. + * {@link #addMovement(MotionEvent)}. When you want to determine the velocity, call + * {@link #computeCurrentVelocity(int)} and then call the velocity-getter methods like + * {@link #getXVelocity(int)}, {@link #getYVelocity(int)}, or {@link #getAxisVelocity(int, int)} + * to retrieve velocity for different axes and/or pointer IDs. */ public final class VelocityTracker { private static final SynchronizedPool<VelocityTracker> sPool = diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 869efc69872e..8bf32324dd5a 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -8920,7 +8920,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ @UnsupportedAppUsage - public void getBoundsOnScreen(Rect outRect, boolean clipToParent) { + @TestApi + public void getBoundsOnScreen(@NonNull Rect outRect, boolean clipToParent) { if (mAttachInfo == null) { return; } @@ -8928,6 +8929,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, getBoundsToScreenInternal(position, clipToParent); outRect.set(Math.round(position.left), Math.round(position.top), Math.round(position.right), Math.round(position.bottom)); + // If "Sandboxing View Bounds APIs" override is enabled, applyViewBoundsSandboxingIfNeeded + // will sandbox outRect within window bounds. + mAttachInfo.mViewRootImpl.applyViewBoundsSandboxingIfNeeded(outRect); } /** @@ -16159,7 +16163,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ @UnsupportedAppUsage - public void getWindowDisplayFrame(Rect outRect) { + @TestApi + public void getWindowDisplayFrame(@NonNull Rect outRect) { if (mAttachInfo != null) { mAttachInfo.mViewRootImpl.getDisplayFrame(outRect); return; @@ -26375,6 +26380,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (info != null) { outLocation[0] += info.mWindowLeft; outLocation[1] += info.mWindowTop; + // If OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS override is enabled, + // applyViewLocationSandboxingIfNeeded sandboxes outLocation within window bounds. + info.mViewRootImpl.applyViewLocationSandboxingIfNeeded(outLocation); } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 807af5b54081..a8b4da14c1c5 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -16,6 +16,7 @@ package android.view; +import static android.content.pm.ActivityInfo.OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS; import static android.graphics.HardwareRenderer.SYNC_CONTEXT_IS_STOPPED; import static android.graphics.HardwareRenderer.SYNC_LOST_SURFACE_REWARD_IF_FOUND; import static android.os.IInputConstants.INVALID_INPUT_EVENT_ID; @@ -79,6 +80,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS; import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; @@ -92,12 +94,14 @@ import android.animation.LayoutTransition; import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.Size; import android.annotation.UiContext; import android.app.ActivityManager; import android.app.ActivityThread; import android.app.ICompatCameraControlCallback; import android.app.ResourcesManager; import android.app.WindowConfiguration; +import android.app.compat.CompatChanges; import android.compat.annotation.UnsupportedAppUsage; import android.content.ClipData; import android.content.ClipDescription; @@ -895,6 +899,15 @@ public final class ViewRootImpl implements ViewParent, private boolean mRelayoutRequested; + /** + * Whether sandboxing of {@link android.view.View#getBoundsOnScreen}, + * {@link android.view.View#getLocationOnScreen(int[])}, + * {@link android.view.View#getWindowDisplayFrame} and + * {@link android.view.View#getWindowVisibleDisplayFrame} + * within Activity bounds is enabled for the current application. + */ + private final boolean mViewBoundsSandboxingEnabled; + private int mLastTransformHint = Integer.MIN_VALUE; private AccessibilityWindowAttributes mAccessibilityWindowAttributes; @@ -986,6 +999,8 @@ public final class ViewRootImpl implements ViewParent, mViewConfiguration, mContext.getSystemService(InputMethodManager.class)); + mViewBoundsSandboxingEnabled = getViewBoundsSandboxingEnabled(); + String processorOverrideName = context.getResources().getString( R.string.config_inputEventCompatProcessorOverrideClassName); if (processorOverrideName.isEmpty()) { @@ -8598,6 +8613,9 @@ public final class ViewRootImpl implements ViewParent, */ void getDisplayFrame(Rect outFrame) { outFrame.set(mTmpFrames.displayFrame); + // Apply sandboxing here (in getter) due to possible layout updates on the client after + // mTmpFrames.displayFrame is received from the server. + applyViewBoundsSandboxingIfNeeded(outFrame); } /** @@ -8614,6 +8632,69 @@ public final class ViewRootImpl implements ViewParent, outFrame.top += insets.top; outFrame.right -= insets.right; outFrame.bottom -= insets.bottom; + // Apply sandboxing here (in getter) due to possible layout updates on the client after + // mTmpFrames.displayFrame is received from the server. + applyViewBoundsSandboxingIfNeeded(outFrame); + } + + /** + * Offset outRect to make it sandboxed within Window's bounds. + * + * <p>This is used by {@link android.view.View#getBoundsOnScreen}, + * {@link android.view.ViewRootImpl#getDisplayFrame} and + * {@link android.view.ViewRootImpl#getWindowVisibleDisplayFrame}, which are invoked by + * {@link android.view.View#getWindowDisplayFrame} and + * {@link android.view.View#getWindowVisibleDisplayFrame}, as well as + * {@link android.view.ViewDebug#captureLayers} for debugging. + */ + void applyViewBoundsSandboxingIfNeeded(final Rect inOutRect) { + if (mViewBoundsSandboxingEnabled) { + final Rect bounds = getConfiguration().windowConfiguration.getBounds(); + inOutRect.offset(-bounds.left, -bounds.top); + } + } + + /** + * Offset outLocation to make it sandboxed within Window's bounds. + * + * <p>This is used by {@link android.view.View#getLocationOnScreen(int[])} + */ + public void applyViewLocationSandboxingIfNeeded(@Size(2) int[] outLocation) { + if (mViewBoundsSandboxingEnabled) { + final Rect bounds = getConfiguration().windowConfiguration.getBounds(); + outLocation[0] -= bounds.left; + outLocation[1] -= bounds.top; + } + } + + private boolean getViewBoundsSandboxingEnabled() { + // System dialogs (e.g. ANR) can be created within System process, so handleBindApplication + // may be never called. This results into all app compat changes being enabled + // (see b/268007823) because AppCompatCallbacks.install() is never called with non-empty + // array. + // With ActivityThread.isSystem we verify that it is not the system process, + // then this CompatChange can take effect. + if (ActivityThread.isSystem() + || !CompatChanges.isChangeEnabled(OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS)) { + // It is a system process or OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS change-id is disabled. + return false; + } + + // OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS is enabled by the device manufacturer. + try { + final List<PackageManager.Property> properties = mContext.getPackageManager() + .queryApplicationProperty(PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS); + + final boolean isOptedOut = !properties.isEmpty() && !properties.get(0).getBoolean(); + if (isOptedOut) { + // PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS is disabled by the app devs. + return false; + } + } catch (RuntimeException e) { + // remote exception. + } + + return true; } /** diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index a208cb3de8aa..21fe87f42e1b 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -823,6 +823,11 @@ public abstract class Window { /** @hide */ public final void destroy() { mDestroyed = true; + onDestroy(); + } + + /** @hide */ + protected void onDestroy() { } /** @hide */ diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index f8636788ee3c..35ed88fc420e 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -454,6 +454,11 @@ public interface WindowManager extends ViewManager { */ int TRANSIT_WAKE = 11; /** + * The screen is turning off. This is used as a message to stop all animations. + * @hide + */ + int TRANSIT_SLEEP = 12; + /** * The first slot for custom transition types. Callers (like Shell) can make use of custom * transition types for dealing with special cases. These types are effectively ignored by * Core and will just be passed along as part of TransitionInfo objects. An example is @@ -462,7 +467,7 @@ public interface WindowManager extends ViewManager { * implementation. * @hide */ - int TRANSIT_FIRST_CUSTOM = 12; + int TRANSIT_FIRST_CUSTOM = 13; /** * @hide @@ -480,6 +485,7 @@ public interface WindowManager extends ViewManager { TRANSIT_KEYGUARD_UNOCCLUDE, TRANSIT_PIP, TRANSIT_WAKE, + TRANSIT_SLEEP, TRANSIT_FIRST_CUSTOM }) @Retention(RetentionPolicy.SOURCE) @@ -866,6 +872,42 @@ public interface WindowManager extends ViewManager { /** * Application level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that it needs to be opted-out from the + * compatibility treatment that sandboxes {@link android.view.View} API. + * + * <p>The treatment can be enabled by device manufacturers for applications which misuse + * {@link android.view.View} APIs by expecting that + * {@link android.view.View#getLocationOnScreen}, + * {@link android.view.View#getBoundsOnScreen}, + * {@link android.view.View#getWindowVisibleDisplayFrame}, + * {@link android.view.View#getWindowDisplayFrame} + * return coordinates as if an activity is positioned in the top-left corner of the screen, with + * left coordinate equal to 0. This may not be the case for applications in multi-window and in + * letterbox modes. + * + * <p>Setting this property to {@code false} informs the system that the application must be + * opted-out from the "Sandbox {@link android.view.View} API to Activity bounds" treatment even + * if the device manufacturer has opted the app into the treatment. + * + * <p>Not setting this property at all, or setting this property to {@code true} has no effect. + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS" + * android:value="false"/> + * </application> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS = + "android.window.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS"; + + /** + * Application level {@link android.content.pm.PackageManager.Property PackageManager * .Property} for an app to inform the system that the application can be opted-in or opted-out * from the compatibility treatment that enables sending a fake focus event for unfocused * resumed split screen activities. This is needed because some game engines wait to get @@ -1437,6 +1479,7 @@ public interface WindowManager extends ViewManager { case TRANSIT_KEYGUARD_UNOCCLUDE: return "KEYGUARD_UNOCCLUDE"; case TRANSIT_PIP: return "PIP"; case TRANSIT_WAKE: return "WAKE"; + case TRANSIT_SLEEP: return "SLEEP"; case TRANSIT_FIRST_CUSTOM: return "FIRST_CUSTOM"; default: if (type > TRANSIT_FIRST_CUSTOM) { diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index aef0e651ff8d..ab9f492694f5 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -301,6 +301,14 @@ public final class AutofillManager { public static final String EXTRA_AUGMENTED_AUTOFILL_CLIENT = "android.view.autofill.extra.AUGMENTED_AUTOFILL_CLIENT"; + /** + * Autofill Hint to indicate that it can match any field. + * + * @hide + */ + @TestApi + public static final String ANY_HINT = "any"; + private static final String SESSION_ID_TAG = "android:sessionId"; private static final String STATE_TAG = "android:state"; private static final String LAST_AUTOFILLED_DATA_TAG = "android:lastAutoFilledData"; diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index 6ddfcb825acc..8cff06641847 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -562,11 +562,11 @@ public final class MainContentCaptureSession extends ContentCaptureSession { final int numberEvents = mEvents.size(); final String reasonString = getFlushReasonAsString(reason); - if (sDebug) { + if (sVerbose) { ContentCaptureEvent event = mEvents.get(numberEvents - 1); String forceString = (reason == FLUSH_REASON_FORCE_FLUSH) ? ". The force flush event " + ContentCaptureEvent.getTypeAsString(event.getType()) : ""; - Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason) + Log.v(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason) + forceString); } if (mFlushHistory != null) { diff --git a/core/java/android/window/WindowInfosListener.java b/core/java/android/window/WindowInfosListener.java index 8db5a5eb0e47..42bb674d9d85 100644 --- a/core/java/android/window/WindowInfosListener.java +++ b/core/java/android/window/WindowInfosListener.java @@ -16,6 +16,8 @@ package android.window; +import android.Manifest; +import android.annotation.RequiresPermission; import android.graphics.Matrix; import android.util.Pair; import android.util.Size; @@ -49,10 +51,13 @@ public abstract class WindowInfosListener { /** * Register the WindowInfosListener. * + * Registering WindowInfosListeners should only be done within system_server and shell. + * * @return The cached values for InputWindowHandles and DisplayInfos. This is the last updated * value that was sent from SurfaceFlinger to this particular process. If there was nothing * registered previously, then the data can be empty. */ + @RequiresPermission(Manifest.permission.ACCESS_SURFACE_FLINGER) public Pair<InputWindowHandle[], DisplayInfo[]> register() { return nativeRegister(mNativeListener); } @@ -65,8 +70,12 @@ public abstract class WindowInfosListener { } private static native long nativeCreate(WindowInfosListener thiz); + + @RequiresPermission(Manifest.permission.ACCESS_SURFACE_FLINGER) private static native Pair<InputWindowHandle[], DisplayInfo[]> nativeRegister(long ptr); + private static native void nativeUnregister(long ptr); + private static native long nativeGetFinalizer(); /** diff --git a/core/java/android/window/WindowInfosListenerForTest.java b/core/java/android/window/WindowInfosListenerForTest.java new file mode 100644 index 000000000000..429156fb980a --- /dev/null +++ b/core/java/android/window/WindowInfosListenerForTest.java @@ -0,0 +1,136 @@ +/* + * 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 android.window; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.TestApi; +import android.graphics.Rect; +import android.os.IBinder; +import android.os.InputConfig; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; +import android.view.InputWindowHandle; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; + +/** + * Wrapper class to provide access to WindowInfosListener within tests. + * + * @hide + */ +@TestApi +public class WindowInfosListenerForTest { + + /** + * Window properties passed to {@code @WindowInfosListenerForTest#onWindowInfosChanged}. + */ + public static class WindowInfo { + /** + * The window's token. + */ + @NonNull + public final IBinder windowToken; + + /** + * The window's name. + */ + @NonNull + public final String name; + + /** + * The window's position and size in display space. + */ + @NonNull + public final Rect bounds; + + WindowInfo(@NonNull IBinder windowToken, @NonNull String name, @NonNull Rect bounds) { + this.windowToken = windowToken; + this.name = name; + this.bounds = bounds; + } + } + + private static final String TAG = "WindowInfosListenerForTest"; + + private ArrayMap<Consumer<List<WindowInfo>>, WindowInfosListener> mListeners; + + public WindowInfosListenerForTest() { + mListeners = new ArrayMap<>(); + } + + /** + * Register a listener that is called when the system's list of visible windows has changes in + * position or visibility. + * + * @param consumer Consumer that is called with reverse Z ordered lists of WindowInfo instances + * where the first value is the topmost window. + */ + @RequiresPermission(Manifest.permission.ACCESS_SURFACE_FLINGER) + public void addWindowInfosListener( + @NonNull Consumer<List<WindowInfo>> consumer) { + var calledWithInitialState = new CountDownLatch(1); + var listener = new WindowInfosListener() { + @Override + public void onWindowInfosChanged(InputWindowHandle[] windowHandles, + DisplayInfo[] displayInfos) { + try { + calledWithInitialState.await(); + } catch (InterruptedException exception) { + Log.e(TAG, + "Exception thrown while waiting for listener to be called with " + + "initial state"); + } + consumer.accept(buildWindowInfos(windowHandles)); + } + }; + mListeners.put(consumer, listener); + Pair<InputWindowHandle[], WindowInfosListener.DisplayInfo[]> initialState = + listener.register(); + consumer.accept(buildWindowInfos(initialState.first)); + calledWithInitialState.countDown(); + } + + /** + * Unregisters the listener. + */ + public void removeWindowInfosListener(@NonNull Consumer<List<WindowInfo>> consumer) { + WindowInfosListener listener = mListeners.remove(consumer); + if (listener == null) { + return; + } + listener.unregister(); + } + + private static List<WindowInfo> buildWindowInfos(InputWindowHandle[] windowHandles) { + var windowInfos = new ArrayList<WindowInfo>(windowHandles.length); + for (var handle : windowHandles) { + if ((handle.inputConfig & InputConfig.NOT_VISIBLE) != 0) { + continue; + } + var bounds = new Rect(handle.frameLeft, handle.frameTop, handle.frameRight, + handle.frameBottom); + windowInfos.add(new WindowInfo(handle.getWindowToken(), handle.name, bounds)); + } + return windowInfos; + } +} diff --git a/core/java/com/android/internal/app/procstats/UidState.java b/core/java/com/android/internal/app/procstats/UidState.java index 8761b7470cd3..49113465c26a 100644 --- a/core/java/com/android/internal/app/procstats/UidState.java +++ b/core/java/com/android/internal/app/procstats/UidState.java @@ -150,6 +150,7 @@ public final class UidState { public void resetSafely(long now) { mDurations.resetTable(); mStartTime = now; + mProcesses.removeIf(p -> !p.isInUse()); } /** diff --git a/core/java/com/android/internal/infra/AbstractMultiplePendingRequestsRemoteService.java b/core/java/com/android/internal/infra/AbstractMultiplePendingRequestsRemoteService.java index 6c01780dac76..2a9025d35339 100644 --- a/core/java/com/android/internal/infra/AbstractMultiplePendingRequestsRemoteService.java +++ b/core/java/com/android/internal/infra/AbstractMultiplePendingRequestsRemoteService.java @@ -61,7 +61,7 @@ public abstract class AbstractMultiplePendingRequestsRemoteService<S final int size = mPendingRequests.size(); if (mVerbose) Slog.v(mTag, "Sending " + size + " pending requests"); for (int i = 0; i < size; i++) { - mPendingRequests.get(i).run(); + handlePendingRequest(mPendingRequests.get(i)); } mPendingRequests.clear(); } diff --git a/core/java/com/android/internal/infra/AbstractRemoteService.java b/core/java/com/android/internal/infra/AbstractRemoteService.java index d5f7ba57a694..18414cf93bc8 100644 --- a/core/java/com/android/internal/infra/AbstractRemoteService.java +++ b/core/java/com/android/internal/infra/AbstractRemoteService.java @@ -107,7 +107,7 @@ public abstract class AbstractRemoteService<S extends AbstractRemoteService<S, I private int mServiceExitSubReason; /** Requests that have been scheduled, but that are not finished yet */ - private final ArrayList<BasePendingRequest<S, I>> mUnfinishedRequests = new ArrayList<>(); + protected final ArrayList<BasePendingRequest<S, I>> mUnfinishedRequests = new ArrayList<>(); /** * Callback called when the service dies. @@ -673,6 +673,11 @@ public abstract class AbstractRemoteService<S extends AbstractRemoteService<S, I mCancelled = true; } + S service = mWeakService.get(); + if (service != null) { + service.finishRequest(this); + } + onCancel(); return true; } diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index c41b8223d181..9b9e010039a6 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -295,6 +295,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { private boolean mClosingActionMenu; private int mVolumeControlStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE; + private int mAudioMode = AudioManager.MODE_NORMAL; private MediaController mMediaController; private AudioManager mAudioManager; @@ -317,6 +318,8 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { } }; + private AudioManager.OnModeChangedListener mOnModeChangedListener; + private Transition mEnterTransition = null; private Transition mReturnTransition = USE_DEFAULT_TRANSITION; private Transition mExitTransition = null; @@ -1944,7 +1947,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { case KeyEvent.KEYCODE_VOLUME_MUTE: { // If we have a session and no active phone call send it the volume command, // otherwise use the suggested stream. - if (mMediaController != null && !isActivePhoneCallKnown()) { + if (mMediaController != null && !isActivePhoneCallOngoing()) { getMediaSessionManager().dispatchVolumeKeyEventToSessionAsSystemService(event, mMediaController.getSessionToken()); } else { @@ -1995,16 +1998,9 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { return false; } - private boolean isActivePhoneCallKnown() { - boolean isActivePhoneCallKnown = false; - AudioManager audioManager = - (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); - int audioManagerMode = audioManager.getMode(); - if (audioManagerMode == AudioManager.MODE_IN_CALL - || audioManagerMode == AudioManager.MODE_IN_COMMUNICATION) { - isActivePhoneCallKnown = true; - } - return isActivePhoneCallKnown; + private boolean isActivePhoneCallOngoing() { + return mAudioMode == AudioManager.MODE_IN_CALL + || mAudioMode == AudioManager.MODE_IN_COMMUNICATION; } private KeyguardManager getKeyguardManager() { @@ -2331,6 +2327,14 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { } } + @Override + protected void onDestroy() { + if (mOnModeChangedListener != null) { + getAudioManager().removeOnModeChangedListener(mOnModeChangedListener); + mOnModeChangedListener = null; + } + } + private class PanelMenuPresenterCallback implements MenuPresenter.Callback { @Override public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { @@ -3229,6 +3233,15 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { @Override public void setMediaController(MediaController controller) { mMediaController = controller; + if (controller != null && mOnModeChangedListener == null) { + mAudioMode = getAudioManager().getMode(); + mOnModeChangedListener = mode -> mAudioMode = mode; + getAudioManager().addOnModeChangedListener(getContext().getMainExecutor(), + mOnModeChangedListener); + } else if (mOnModeChangedListener != null) { + getAudioManager().removeOnModeChangedListener(mOnModeChangedListener); + mOnModeChangedListener = null; + } } @Override diff --git a/core/java/com/android/internal/protolog/BaseProtoLogImpl.java b/core/java/com/android/internal/protolog/BaseProtoLogImpl.java index f2b0544bcfad..abe6c7c079c2 100644 --- a/core/java/com/android/internal/protolog/BaseProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/BaseProtoLogImpl.java @@ -72,11 +72,13 @@ public class BaseProtoLogImpl { private static final String TAG = "ProtoLog"; private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; static final String PROTOLOG_VERSION = "1.0.0"; + private static final int DEFAULT_PER_CHUNK_SIZE = 0; private final File mLogFile; private final String mViewerConfigFilename; private final TraceBuffer mBuffer; protected final ProtoLogViewerConfigReader mViewerConfig; + private final int mPerChunkSize; private boolean mProtoLogEnabled; private boolean mProtoLogEnabledLockFree; @@ -160,7 +162,7 @@ public class BaseProtoLogImpl { return; } try { - ProtoOutputStream os = new ProtoOutputStream(); + ProtoOutputStream os = new ProtoOutputStream(mPerChunkSize); long token = os.start(LOG); os.write(MESSAGE_HASH, messageHash); os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos()); @@ -219,10 +221,16 @@ public class BaseProtoLogImpl { public BaseProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity, ProtoLogViewerConfigReader viewerConfig) { + this(file, viewerConfigFilename, bufferCapacity, viewerConfig, DEFAULT_PER_CHUNK_SIZE); + } + + public BaseProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity, + ProtoLogViewerConfigReader viewerConfig, int perChunkSize) { mLogFile = file; mBuffer = new TraceBuffer(bufferCapacity); mViewerConfigFilename = viewerConfigFilename; mViewerConfig = viewerConfig; + mPerChunkSize = perChunkSize; } /** @@ -368,7 +376,7 @@ public class BaseProtoLogImpl { try { long offset = (System.currentTimeMillis() - (SystemClock.elapsedRealtimeNanos() / 1000000)); - ProtoOutputStream proto = new ProtoOutputStream(); + ProtoOutputStream proto = new ProtoOutputStream(mPerChunkSize); proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); proto.write(VERSION, PROTOLOG_VERSION); proto.write(REAL_TIME_TO_ELAPSED_TIME_OFFSET_MILLIS, offset); diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java index 353c6c083d9d..527cfddf6d8e 100644 --- a/core/java/com/android/internal/protolog/ProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java @@ -30,6 +30,7 @@ public class ProtoLogImpl extends BaseProtoLogImpl { private static final int BUFFER_CAPACITY = 1024 * 1024; private static final String LOG_FILENAME = "/data/misc/wmtrace/wm_log.winscope"; private static final String VIEWER_CONFIG_FILENAME = "/system/etc/protolog.conf.json.gz"; + private static final int PER_CHUNK_SIZE = 1024; private static ProtoLogImpl sServiceInstance = null; @@ -94,7 +95,10 @@ public class ProtoLogImpl extends BaseProtoLogImpl { public static synchronized ProtoLogImpl getSingleInstance() { if (sServiceInstance == null) { sServiceInstance = new ProtoLogImpl( - new File(LOG_FILENAME), BUFFER_CAPACITY, new ProtoLogViewerConfigReader()); + new File(LOG_FILENAME) + , BUFFER_CAPACITY + , new ProtoLogViewerConfigReader() + , PER_CHUNK_SIZE); } return sServiceInstance; } @@ -105,8 +109,8 @@ public class ProtoLogImpl extends BaseProtoLogImpl { } public ProtoLogImpl(File logFile, int bufferCapacity, - ProtoLogViewerConfigReader viewConfigReader) { - super(logFile, VIEWER_CONFIG_FILENAME, bufferCapacity, viewConfigReader); - } + ProtoLogViewerConfigReader viewConfigReader, int perChunkSize) { + super(logFile, VIEWER_CONFIG_FILENAME, bufferCapacity, viewConfigReader, perChunkSize); + } } diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp index b09a9c3a505d..739055e040af 100644 --- a/core/jni/android_view_DisplayEventReceiver.cpp +++ b/core/jni/android_view_DisplayEventReceiver.cpp @@ -50,12 +50,22 @@ static struct { struct { jclass clazz; + jmethodID init; + + jfieldID vsyncId; + jfieldID expectedPresentationTime; + jfieldID deadline; } frameTimelineClassInfo; struct { jclass clazz; + jmethodID init; + + jfieldID frameInterval; + jfieldID preferredFrameTimelineIndex; + jfieldID frameTimelines; } vsyncEventDataClassInfo; } gDisplayEventReceiverClassInfo; @@ -63,7 +73,7 @@ static struct { class NativeDisplayEventReceiver : public DisplayEventDispatcher { public: - NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, + NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, jobject vsyncEventDataWeak, const sp<MessageQueue>& messageQueue, jint vsyncSource, jint eventRegistration, jlong layerHandle); @@ -74,6 +84,7 @@ protected: private: jobject mReceiverWeakGlobal; + jobject mVsyncEventDataWeakGlobal; sp<MessageQueue> mMessageQueue; void dispatchVsync(nsecs_t timestamp, PhysicalDisplayId displayId, uint32_t count, @@ -87,6 +98,7 @@ private: }; NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, + jobject vsyncEventDataWeak, const sp<MessageQueue>& messageQueue, jint vsyncSource, jint eventRegistration, jlong layerHandle) @@ -98,6 +110,7 @@ NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject rece reinterpret_cast<IBinder*>(layerHandle)) : nullptr), mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)), + mVsyncEventDataWeakGlobal(env->NewGlobalRef(vsyncEventDataWeak)), mMessageQueue(messageQueue) { ALOGV("receiver %p ~ Initializing display event receiver.", this); } @@ -144,12 +157,39 @@ void NativeDisplayEventReceiver::dispatchVsync(nsecs_t timestamp, PhysicalDispla JNIEnv* env = AndroidRuntime::getJNIEnv(); ScopedLocalRef<jobject> receiverObj(env, GetReferent(env, mReceiverWeakGlobal)); - if (receiverObj.get()) { + ScopedLocalRef<jobject> vsyncEventDataObj(env, GetReferent(env, mVsyncEventDataWeakGlobal)); + if (receiverObj.get() && vsyncEventDataObj.get()) { ALOGV("receiver %p ~ Invoking vsync handler.", this); - jobject javaVsyncEventData = createJavaVsyncEventData(env, vsyncEventData); + env->SetIntField(vsyncEventDataObj.get(), + gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo + .preferredFrameTimelineIndex, + vsyncEventData.preferredFrameTimelineIndex); + env->SetLongField(vsyncEventDataObj.get(), + gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.frameInterval, + vsyncEventData.frameInterval); + + jobjectArray frameTimelinesObj = reinterpret_cast<jobjectArray>( + env->GetObjectField(vsyncEventDataObj.get(), + gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo + .frameTimelines)); + for (int i = 0; i < VsyncEventData::kFrameTimelinesLength; i++) { + VsyncEventData::FrameTimeline& frameTimeline = vsyncEventData.frameTimelines[i]; + jobject frameTimelineObj = env->GetObjectArrayElement(frameTimelinesObj, i); + env->SetLongField(frameTimelineObj, + gDisplayEventReceiverClassInfo.frameTimelineClassInfo.vsyncId, + frameTimeline.vsyncId); + env->SetLongField(frameTimelineObj, + gDisplayEventReceiverClassInfo.frameTimelineClassInfo + .expectedPresentationTime, + frameTimeline.expectedPresentationTime); + env->SetLongField(frameTimelineObj, + gDisplayEventReceiverClassInfo.frameTimelineClassInfo.deadline, + frameTimeline.deadlineTimestamp); + } + env->CallVoidMethod(receiverObj.get(), gDisplayEventReceiverClassInfo.dispatchVsync, - timestamp, displayId.value, count, javaVsyncEventData); + timestamp, displayId.value, count); ALOGV("receiver %p ~ Returned from vsync handler.", this); } @@ -217,8 +257,9 @@ void NativeDisplayEventReceiver::dispatchFrameRateOverrides( mMessageQueue->raiseAndClearException(env, "dispatchModeChanged"); } -static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject messageQueueObj, - jint vsyncSource, jint eventRegistration, jlong layerHandle) { +static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject vsyncEventDataWeak, + jobject messageQueueObj, jint vsyncSource, jint eventRegistration, + jlong layerHandle) { sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj); if (messageQueue == NULL) { jniThrowRuntimeException(env, "MessageQueue is not initialized."); @@ -226,8 +267,8 @@ static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject } sp<NativeDisplayEventReceiver> receiver = - new NativeDisplayEventReceiver(env, receiverWeak, messageQueue, vsyncSource, - eventRegistration, layerHandle); + new NativeDisplayEventReceiver(env, receiverWeak, vsyncEventDataWeak, messageQueue, + vsyncSource, eventRegistration, layerHandle); status_t status = receiver->initialize(); if (status) { String8 message; @@ -274,7 +315,9 @@ static jobject nativeGetLatestVsyncEventData(JNIEnv* env, jclass clazz, jlong re static const JNINativeMethod gMethods[] = { /* name, signature, funcPtr */ - {"nativeInit", "(Ljava/lang/ref/WeakReference;Landroid/os/MessageQueue;IIJ)J", + {"nativeInit", + "(Ljava/lang/ref/WeakReference;Ljava/lang/ref/WeakReference;Landroid/os/" + "MessageQueue;IIJ)J", (void*)nativeInit}, {"nativeGetDisplayEventReceiverFinalizer", "()J", (void*)nativeGetDisplayEventReceiverFinalizer}, @@ -291,8 +334,7 @@ int register_android_view_DisplayEventReceiver(JNIEnv* env) { gDisplayEventReceiverClassInfo.clazz = MakeGlobalRefOrDie(env, clazz); gDisplayEventReceiverClassInfo.dispatchVsync = - GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchVsync", - "(JJILandroid/view/DisplayEventReceiver$VsyncEventData;)V"); + GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchVsync", "(JJI)V"); gDisplayEventReceiverClassInfo.dispatchHotplug = GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchHotplug", "(JJZ)V"); gDisplayEventReceiverClassInfo.dispatchModeChanged = @@ -318,6 +360,15 @@ int register_android_view_DisplayEventReceiver(JNIEnv* env) { gDisplayEventReceiverClassInfo.frameTimelineClassInfo.init = GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.frameTimelineClassInfo.clazz, "<init>", "(JJJ)V"); + gDisplayEventReceiverClassInfo.frameTimelineClassInfo.vsyncId = + GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.frameTimelineClassInfo.clazz, + "vsyncId", "J"); + gDisplayEventReceiverClassInfo.frameTimelineClassInfo.expectedPresentationTime = + GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.frameTimelineClassInfo.clazz, + "expectedPresentationTime", "J"); + gDisplayEventReceiverClassInfo.frameTimelineClassInfo.deadline = + GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.frameTimelineClassInfo.clazz, + "deadline", "J"); jclass vsyncEventDataClazz = FindClassOrDie(env, "android/view/DisplayEventReceiver$VsyncEventData"); @@ -329,6 +380,17 @@ int register_android_view_DisplayEventReceiver(JNIEnv* env) { "([Landroid/view/" "DisplayEventReceiver$VsyncEventData$FrameTimeline;IJ)V"); + gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.preferredFrameTimelineIndex = + GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.clazz, + "preferredFrameTimelineIndex", "I"); + gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.frameInterval = + GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.clazz, + "frameInterval", "J"); + gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.frameTimelines = + GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.clazz, + "frameTimelines", + "[Landroid/view/DisplayEventReceiver$VsyncEventData$FrameTimeline;"); + return res; } diff --git a/core/jni/android_view_InputEventSender.cpp b/core/jni/android_view_InputEventSender.cpp index 4bc567abf27a..ad54004ec81c 100644 --- a/core/jni/android_view_InputEventSender.cpp +++ b/core/jni/android_view_InputEventSender.cpp @@ -20,20 +20,21 @@ #include <android_runtime/AndroidRuntime.h> #include <input/InputTransport.h> +#include <inttypes.h> #include <log/log.h> #include <nativehelper/JNIHelp.h> #include <nativehelper/ScopedLocalRef.h> #include <utils/Looper.h> + +#include <optional> +#include <unordered_map> + #include "android_os_MessageQueue.h" #include "android_view_InputChannel.h" #include "android_view_KeyEvent.h" #include "android_view_MotionEvent.h" #include "core_jni_helpers.h" -#include <inttypes.h> -#include <unordered_map> - - using android::base::Result; namespace android { @@ -67,7 +68,7 @@ private: jobject mSenderWeakGlobal; InputPublisher mInputPublisher; sp<MessageQueue> mMessageQueue; - std::unordered_map<uint32_t, uint32_t> mPublishedSeqMap; + std::unordered_map<uint32_t, std::optional<uint32_t>> mPublishedSeqMap; uint32_t mNextPublishedSeq; @@ -165,8 +166,14 @@ status_t NativeInputEventSender::sendMotionEvent(uint32_t seq, const MotionEvent getInputChannelName().c_str(), status); return status; } + // mPublishedSeqMap tracks all sequences published from this sender. Only the last + // sequence number is used to signal this motion event is finished. + if (i == event->getHistorySize()) { + mPublishedSeqMap.emplace(publishedSeq, seq); + } else { + mPublishedSeqMap.emplace(publishedSeq, std::nullopt); + } } - mPublishedSeqMap.emplace(publishedSeq, seq); return OK; } @@ -277,8 +284,16 @@ bool NativeInputEventSender::notifyConsumerResponse( // does something wrong and sends bad data. Just ignore and process other events. return true; } - const uint32_t seq = it->second; + + const std::optional<uint32_t> seqOptional = it->second; mPublishedSeqMap.erase(it); + // If this optional does not have a value, it means we are processing an event that had history + // and was split. There are more events coming, so we can't call 'dispatchInputEventFinished' + // yet. The final split event will have a valid sequence number. + if (!seqOptional.has_value()) { + return true; + } + const uint32_t seq = seqOptional.value(); if (kDebugDispatchCycle) { ALOGD("channel '%s' ~ Received finished signal, seq=%u, handled=%s, pendingEvents=%zu.", diff --git a/core/jni/android_view_MotionPredictor.cpp b/core/jni/android_view_MotionPredictor.cpp index 2c232fadbbc5..de3e81c7088b 100644 --- a/core/jni/android_view_MotionPredictor.cpp +++ b/core/jni/android_view_MotionPredictor.cpp @@ -20,7 +20,6 @@ #include <input/MotionPredictor.h> #include "android_view_MotionEvent.h" -#include "core_jni_converters.h" #include "core_jni_helpers.h" /** @@ -30,14 +29,6 @@ namespace android { -// ---------------------------------------------------------------------------- - -static struct { - jclass clazz; -} gMotionEventClassInfo; - -// ---------------------------------------------------------------------------- - static void release(void* ptr) { delete reinterpret_cast<MotionPredictor*>(ptr); } @@ -57,14 +48,20 @@ static void android_view_MotionPredictor_nativeRecord(JNIEnv* env, jclass clazz, jobject event) { MotionPredictor* predictor = reinterpret_cast<MotionPredictor*>(ptr); MotionEvent* motionEvent = android_view_MotionEvent_getNativePtr(env, event); - predictor->record(*motionEvent); + + android::base::Result<void> result = predictor->record(*motionEvent); + if (!result.ok()) { + jniThrowException(env, "java/lang/IllegalArgumentException", + result.error().message().c_str()); + } } static jobject android_view_MotionPredictor_nativePredict(JNIEnv* env, jclass clazz, jlong ptr, jlong predictionTimeNanos) { MotionPredictor* predictor = reinterpret_cast<MotionPredictor*>(ptr); - return toJavaArray(env, predictor->predict(static_cast<nsecs_t>(predictionTimeNanos)), - gMotionEventClassInfo.clazz, &android_view_MotionEvent_obtainFromNative); + return android_view_MotionEvent_obtainFromNative(env, + predictor->predict(static_cast<nsecs_t>( + predictionTimeNanos))); } static jboolean android_view_MotionPredictor_nativeIsPredictionAvailable(JNIEnv* env, jclass clazz, @@ -84,15 +81,13 @@ static const std::array<JNINativeMethod, 5> gMotionPredictorMethods{{ (void*)android_view_MotionPredictor_nativeGetNativeMotionPredictorFinalizer}, {"nativeRecord", "(JLandroid/view/MotionEvent;)V", (void*)android_view_MotionPredictor_nativeRecord}, - {"nativePredict", "(JJ)[Landroid/view/MotionEvent;", + {"nativePredict", "(JJ)Landroid/view/MotionEvent;", (void*)android_view_MotionPredictor_nativePredict}, {"nativeIsPredictionAvailable", "(JII)Z", (void*)android_view_MotionPredictor_nativeIsPredictionAvailable}, }}; int register_android_view_MotionPredictor(JNIEnv* env) { - jclass motionEventClazz = FindClassOrDie(env, "android/view/MotionEvent"); - gMotionEventClassInfo.clazz = MakeGlobalRefOrDie(env, motionEventClazz); return RegisterMethodsOrDie(env, "android/view/MotionPredictor", gMotionPredictorMethods.data(), gMotionPredictorMethods.size()); } diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto index 146ac9d2c425..7503dde440e0 100644 --- a/core/proto/android/providers/settings/system.proto +++ b/core/proto/android/providers/settings/system.proto @@ -115,6 +115,9 @@ message SystemSettingsProto { optional SettingProto sound_cache = 2; optional SettingProto light_pulse = 3 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto vibration_intensity = 4 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto camera_flash_notification = 5 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto screen_flash_notification = 6 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto screen_flash_notification_color_global = 7 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Notification notification = 17; diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto index ab7b0ab8d1fc..ed612a05e6b5 100644 --- a/core/proto/android/server/activitymanagerservice.proto +++ b/core/proto/android/server/activitymanagerservice.proto @@ -142,6 +142,7 @@ message BroadcastQueueProto { optional int64 finish_clock_time_ms = 4; } repeated BroadcastSummary historical_broadcasts_summary = 6; + repeated BroadcastRecordProto pending_broadcasts = 7; } message MemInfoDumpProto { @@ -439,6 +440,7 @@ message ServiceRecordProto { optional int32 id = 1; optional .android.app.NotificationProto notification = 2; + optional int32 foregroundServiceType = 3; } optional Foreground foreground = 13; diff --git a/core/res/Android.bp b/core/res/Android.bp index 3c4bac8156d2..b71995f899c5 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -178,3 +178,12 @@ genrule { " > $(out)", tools: ["xmllint"], } + +filegroup { + name: "frameworks-base-core-AndroidManifest.xml", + srcs: ["AndroidManifest.xml"], + visibility: [ + "//frameworks/base", + "//frameworks/base/api", + ], +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index b73623031dc1..cb6c09241807 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -822,6 +822,8 @@ <protected-broadcast android:name="com.android.internal.telephony.cat.SMS_SENT_ACTION" /> <protected-broadcast android:name="com.android.internal.telephony.cat.SMS_DELIVERY_ACTION" /> <protected-broadcast android:name="android.companion.virtual.action.VIRTUAL_DEVICE_REMOVED" /> + <protected-broadcast android:name="com.android.internal.intent.action.FLASH_NOTIFICATION_START_PREVIEW" /> + <protected-broadcast android:name="com.android.internal.intent.action.FLASH_NOTIFICATION_STOP_PREVIEW" /> <!-- ====================================================================== --> <!-- RUNTIME PERMISSIONS --> @@ -4482,8 +4484,8 @@ <!-- Allows an application to be able to store and retrieve credentials from a remote device. @hide @SystemApi --> - <permission android:name="android.permission.PROVIDE_HYBRID_CREDENTIAL_SERVICE" - android:protectionLevel="signature|privileged" /> + <permission android:name="android.permission.PROVIDE_REMOTE_CREDENTIALS" + android:protectionLevel="signature|privileged|role" /> <!-- ========================================= --> <!-- Permissions for special development tools --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 47d771fea12d..07f353025f87 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4746,8 +4746,7 @@ <string name="accessibility_shortcut_disabling_service">Held volume keys. <xliff:g id="service_name" example="TalkBack">%1$s</xliff:g> turned off.</string> <!-- Text spoken when accessibility shortcut warning dialog is shown. [CHAR LIMIT=none] --> - <string name="accessibility_shortcut_spoken_feedback">Press and hold both volume keys for three seconds to use - <xliff:g id="service_name" example="TalkBack">%1$s</xliff:g></string> + <string name="accessibility_shortcut_spoken_feedback">Release the volume keys. To turn on <xliff:g id="service_name" example="TalkBack">%1$s</xliff:g>, press and hold both volume keys again for 3 seconds.</string> <!-- Text appearing in a prompt at the top of UI allowing the user to select a target service or feature to be assigned to the Accessibility button in the navigation bar. [CHAR LIMIT=none]--> <string name="accessibility_button_prompt_text">Choose a feature to use when you tap the accessibility button:</string> diff --git a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java deleted file mode 100644 index 16ed3ef42da3..000000000000 --- a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java +++ /dev/null @@ -1,104 +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.companion.virtual.sensor; - -import static android.hardware.Sensor.TYPE_ACCELEROMETER; -import static android.hardware.SensorDirectChannel.RATE_STOP; -import static android.hardware.SensorDirectChannel.RATE_VERY_FAST; -import static android.hardware.SensorDirectChannel.TYPE_HARDWARE_BUFFER; -import static android.hardware.SensorDirectChannel.TYPE_MEMORY_FILE; - -import static com.google.common.truth.Truth.assertThat; - -import static org.testng.Assert.assertThrows; - -import android.os.Parcel; -import android.platform.test.annotations.Presubmit; - -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -@Presubmit -@RunWith(AndroidJUnit4.class) -public class VirtualSensorConfigTest { - - private static final String SENSOR_NAME = "VirtualSensorName"; - private static final String SENSOR_VENDOR = "VirtualSensorVendor"; - - @Test - public void parcelAndUnparcel_matches() { - final VirtualSensorConfig originalConfig = - new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME) - .setVendor(SENSOR_VENDOR) - .setHighestDirectReportRateLevel(RATE_VERY_FAST) - .setDirectChannelTypesSupported(TYPE_MEMORY_FILE) - .build(); - final Parcel parcel = Parcel.obtain(); - originalConfig.writeToParcel(parcel, /* flags= */ 0); - parcel.setDataPosition(0); - final VirtualSensorConfig recreatedConfig = - VirtualSensorConfig.CREATOR.createFromParcel(parcel); - assertThat(recreatedConfig.getType()).isEqualTo(originalConfig.getType()); - assertThat(recreatedConfig.getName()).isEqualTo(originalConfig.getName()); - assertThat(recreatedConfig.getVendor()).isEqualTo(originalConfig.getVendor()); - assertThat(recreatedConfig.getHighestDirectReportRateLevel()).isEqualTo(RATE_VERY_FAST); - assertThat(recreatedConfig.getDirectChannelTypesSupported()).isEqualTo(TYPE_MEMORY_FILE); - // From hardware/libhardware/include/hardware/sensors-base.h: - // 0x400 is SENSOR_FLAG_DIRECT_CHANNEL_ASHMEM (i.e. TYPE_MEMORY_FILE) - // 0x800 is SENSOR_FLAG_DIRECT_CHANNEL_GRALLOC (i.e. TYPE_HARDWARE_BUFFER) - // 7 is SENSOR_FLAG_SHIFT_DIRECT_REPORT - assertThat(recreatedConfig.getFlags()).isEqualTo(0x400 | RATE_VERY_FAST << 7); - } - - @Test - public void hardwareBufferDirectChannelTypeSupported_throwsException() { - assertThrows( - IllegalArgumentException.class, - () -> new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME) - .setDirectChannelTypesSupported(TYPE_HARDWARE_BUFFER | TYPE_MEMORY_FILE)); - } - - @Test - public void directChannelTypeSupported_missingHighestReportRateLevel_throwsException() { - assertThrows( - IllegalArgumentException.class, - () -> new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME) - .setDirectChannelTypesSupported(TYPE_MEMORY_FILE) - .build()); - } - - @Test - public void directChannelTypeSupported_missingDirectChannelTypeSupported_throwsException() { - assertThrows( - IllegalArgumentException.class, - () -> new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME) - .setHighestDirectReportRateLevel(RATE_VERY_FAST) - .build()); - } - - @Test - public void sensorConfig_onlyRequiredFields() { - final VirtualSensorConfig config = - new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME).build(); - assertThat(config.getVendor()).isNull(); - assertThat(config.getHighestDirectReportRateLevel()).isEqualTo(RATE_STOP); - assertThat(config.getDirectChannelTypesSupported()).isEqualTo(0); - assertThat(config.getFlags()).isEqualTo(0); - } -} diff --git a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java deleted file mode 100644 index c260ef90cd4e..000000000000 --- a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java +++ /dev/null @@ -1,82 +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.companion.virtual.sensor; - -import static com.google.common.truth.Truth.assertThat; - -import static org.testng.Assert.assertThrows; - -import android.os.Parcel; -import android.os.SystemClock; -import android.platform.test.annotations.Presubmit; - -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -@Presubmit -@RunWith(AndroidJUnit4.class) -public class VirtualSensorEventTest { - - private static final long TIMESTAMP_NANOS = SystemClock.elapsedRealtimeNanos(); - private static final float[] SENSOR_VALUES = new float[] {1.2f, 3.4f, 5.6f}; - - @Test - public void parcelAndUnparcel_matches() { - final VirtualSensorEvent originalEvent = new VirtualSensorEvent.Builder(SENSOR_VALUES) - .setTimestampNanos(TIMESTAMP_NANOS) - .build(); - final Parcel parcel = Parcel.obtain(); - originalEvent.writeToParcel(parcel, /* flags= */ 0); - parcel.setDataPosition(0); - final VirtualSensorEvent recreatedEvent = - VirtualSensorEvent.CREATOR.createFromParcel(parcel); - assertThat(recreatedEvent.getValues()).isEqualTo(originalEvent.getValues()); - assertThat(recreatedEvent.getTimestampNanos()).isEqualTo(originalEvent.getTimestampNanos()); - } - - @Test - public void sensorEvent_nullValues() { - assertThrows( - IllegalArgumentException.class, () -> new VirtualSensorEvent.Builder(null).build()); - } - - @Test - public void sensorEvent_noValues() { - assertThrows( - IllegalArgumentException.class, - () -> new VirtualSensorEvent.Builder(new float[0]).build()); - } - - @Test - public void sensorEvent_noTimestamp_usesCurrentTime() { - final VirtualSensorEvent event = new VirtualSensorEvent.Builder(SENSOR_VALUES).build(); - assertThat(event.getValues()).isEqualTo(SENSOR_VALUES); - assertThat(TIMESTAMP_NANOS).isLessThan(event.getTimestampNanos()); - assertThat(event.getTimestampNanos()).isLessThan(SystemClock.elapsedRealtimeNanos()); - } - - @Test - public void sensorEvent_created() { - final VirtualSensorEvent event = new VirtualSensorEvent.Builder(SENSOR_VALUES) - .setTimestampNanos(TIMESTAMP_NANOS) - .build(); - assertThat(event.getTimestampNanos()).isEqualTo(TIMESTAMP_NANOS); - assertThat(event.getValues()).isEqualTo(SENSOR_VALUES); - } -} diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt index e48f8a01d738..a0d8dcf830e8 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt @@ -28,6 +28,7 @@ import kotlin.math.ceil import kotlin.math.floor import org.junit.Test import org.junit.runner.RunWith +import kotlin.random.Random.Default.nextFloat @Presubmit @RunWith(AndroidJUnit4::class) @@ -124,10 +125,12 @@ class FontScaleConverterFactoryTest { @LargeTest @Test fun allFeasibleScalesAndConversionsDoNotCrash() { - generateSequenceOfFractions(-10f..10f, step = 0.01f) - .mapNotNull{ FontScaleConverterFactory.forScale(it) }!! + generateSequenceOfFractions(-10f..10f, step = 0.1f) + .fuzzFractions() + .mapNotNull{ FontScaleConverterFactory.forScale(it) } .flatMap{ table -> - generateSequenceOfFractions(-2000f..2000f, step = 0.01f) + generateSequenceOfFractions(-2000f..2000f, step = 0.1f) + .fuzzFractions() .map{ Pair(table, it) } } .forEach { (table, sp) -> @@ -172,6 +175,30 @@ class FontScaleConverterFactoryTest { assertThat(fractions).doesNotContain(-.35f) } + @Test + fun testFuzzFractions() { + val numFuzzedFractions = 6 + val fractions = generateSequenceOfFractions(-1000f..1000f, step = 0.1f) + .fuzzFractions() + .toList() + fractions.forEach { + assertThat(it).isAtLeast(-1000f) + assertThat(it).isLessThan(1001f) + } + + val numGeneratedFractions = 1000 * 2 * 10 + 1 // Don't forget the 0 in the middle! + assertThat(fractions).hasSize(numGeneratedFractions * numFuzzedFractions) + + assertThat(fractions).contains(100f) + assertThat(fractions).contains(500.1f) + assertThat(fractions).contains(500.2f) + assertThat(fractions).contains(0.2f) + assertThat(fractions).contains(0f) + assertThat(fractions).contains(-10f) + assertThat(fractions).contains(-10f) + assertThat(fractions).contains(-10.3f) + } + companion object { private const val CONVERSION_TOLERANCE = 0.05f } @@ -188,3 +215,9 @@ fun generateSequenceOfFractions( .takeWhile { it <= endInclusive } .map{ it.toFloat() / multiplier } } + +private fun Sequence<Float>.fuzzFractions(): Sequence<Float> { + return flatMap { i -> + listOf(i, i + 0.01f, i + 0.054f, i + 0.099f, i + nextFloat(), i + nextFloat()) + } +} diff --git a/core/tests/coretests/src/android/credentials/CredentialManagerTest.java b/core/tests/coretests/src/android/credentials/CredentialManagerTest.java index 43334ab08b2f..e31d5aef9b69 100644 --- a/core/tests/coretests/src/android/credentials/CredentialManagerTest.java +++ b/core/tests/coretests/src/android/credentials/CredentialManagerTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.when; import android.app.Activity; import android.app.slice.Slice; import android.content.Context; +import android.content.pm.ServiceInfo; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; @@ -47,6 +48,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -59,6 +61,17 @@ public class CredentialManagerTest { @Mock private Activity mMockActivity; + private static final int TEST_USER_ID = 1; + private static final CredentialProviderInfo TEST_CREDENTIAL_PROVIDER_INFO = + new CredentialProviderInfo.Builder(new ServiceInfo()) + .setSystemProvider(true) + .setOverrideLabel("test") + .addCapabilities(Arrays.asList("passkey")) + .setEnabled(true) + .build(); + private static final List<CredentialProviderInfo> TEST_CREDENTIAL_PROVIDER_INFO_LIST = + Arrays.asList(TEST_CREDENTIAL_PROVIDER_INFO); + private GetCredentialRequest mGetRequest; private CreateCredentialRequest mCreateRequest; @@ -438,95 +451,53 @@ public class CredentialManagerTest { } @Test - public void testListEnabledProviders_nullExecutor() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.listEnabledProviders(null, null, result -> { - })); - + public void testGetCredentialProviderServices_allProviders() throws RemoteException { + verifyGetCredentialProviderServices(CredentialManager.PROVIDER_FILTER_ALL_PROVIDERS); } @Test - public void testListEnabledProviders_nullCallback() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.listEnabledProviders(null, mExecutor, null)); - + public void testGetCredentialProviderServices_userProviders() throws RemoteException { + verifyGetCredentialProviderServices(CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY); } @Test - public void testListEnabledProviders_alreadyCancelled() throws RemoteException { - final CancellationSignal cancellation = new CancellationSignal(); - cancellation.cancel(); - - mCredentialManager.listEnabledProviders(cancellation, mExecutor, result -> { - }); - - verify(mMockCredentialManagerService, never()).listEnabledProviders(any()); + public void testGetCredentialProviderServices_systemProviders() throws RemoteException { + verifyGetCredentialProviderServices(CredentialManager.PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY); } @Test - public void testListEnabledProviders_cancel() throws RemoteException { - final ICancellationSignal serviceSignal = mock(ICancellationSignal.class); - final CancellationSignal cancellation = new CancellationSignal(); - - OutcomeReceiver<ListEnabledProvidersResponse, ListEnabledProvidersException> callback = - mock(OutcomeReceiver.class); - - when(mMockCredentialManagerService.listEnabledProviders(any())).thenReturn(serviceSignal); - - mCredentialManager.listEnabledProviders(cancellation, mExecutor, callback); - - verify(mMockCredentialManagerService).listEnabledProviders(any()); - - cancellation.cancel(); - verify(serviceSignal).cancel(); + public void testGetCredentialProviderServicesForTesting_allProviders() throws RemoteException { + verifyGetCredentialProviderServicesForTesting(CredentialManager.PROVIDER_FILTER_ALL_PROVIDERS); } @Test - public void testListEnabledProviders_failed() throws RemoteException { - ArgumentCaptor<IListEnabledProvidersCallback> callbackCaptor = ArgumentCaptor.forClass( - IListEnabledProvidersCallback.class); - ArgumentCaptor<ListEnabledProvidersException> errorCaptor = ArgumentCaptor.forClass( - ListEnabledProvidersException.class); - - OutcomeReceiver<ListEnabledProvidersResponse, ListEnabledProvidersException> callback = - mock(OutcomeReceiver.class); - - when(mMockCredentialManagerService.listEnabledProviders( - callbackCaptor.capture())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.listEnabledProviders(null, mExecutor, callback); - verify(mMockCredentialManagerService).listEnabledProviders(any()); - - final String errorType = "type"; - callbackCaptor.getValue().onError("type", "unknown error"); - verify(callback).onError(errorCaptor.capture()); - - assertThat(errorCaptor.getValue().getType()).isEqualTo(errorType); + public void testGetCredentialProviderServicesForTesting_userProviders() throws RemoteException { + verifyGetCredentialProviderServicesForTesting(CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY); } @Test - public void testListEnabledProviders_success() throws RemoteException { - ListEnabledProvidersResponse response = ListEnabledProvidersResponse.create( - List.of("foo", "bar", "baz")); + public void testGetCredentialProviderServicesForTesting_systemProviders() throws RemoteException { + verifyGetCredentialProviderServicesForTesting(CredentialManager.PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY); + } - OutcomeReceiver<ListEnabledProvidersResponse, ListEnabledProvidersException> callback = - mock(OutcomeReceiver.class); + private void verifyGetCredentialProviderServices(int testFilter) throws RemoteException { + when(mMockCredentialManagerService.getCredentialProviderServices( + TEST_USER_ID, testFilter)).thenReturn(TEST_CREDENTIAL_PROVIDER_INFO_LIST); - ArgumentCaptor<IListEnabledProvidersCallback> callbackCaptor = ArgumentCaptor.forClass( - IListEnabledProvidersCallback.class); - ArgumentCaptor<ListEnabledProvidersResponse> responseCaptor = ArgumentCaptor.forClass( - ListEnabledProvidersResponse.class); + List<CredentialProviderInfo> output = + mCredentialManager.getCredentialProviderServices(TEST_USER_ID, testFilter); - when(mMockCredentialManagerService.listEnabledProviders( - callbackCaptor.capture())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.listEnabledProviders(null, mExecutor, callback); + assertThat(output).containsExactlyElementsIn(TEST_CREDENTIAL_PROVIDER_INFO_LIST); + } - verify(mMockCredentialManagerService).listEnabledProviders(any()); + private void verifyGetCredentialProviderServicesForTesting(int testFilter) throws RemoteException { + when(mMockCredentialManagerService.getCredentialProviderServicesForTesting( + testFilter)).thenReturn(TEST_CREDENTIAL_PROVIDER_INFO_LIST); - callbackCaptor.getValue().onResponse(response); + List<CredentialProviderInfo> output = + mCredentialManager.getCredentialProviderServicesForTesting(testFilter); - verify(callback).onResult(responseCaptor.capture()); - assertThat(responseCaptor.getValue().getProviderComponentNames()).containsExactlyElementsIn( - response.getProviderComponentNames()); + assertThat(output).containsExactlyElementsIn(TEST_CREDENTIAL_PROVIDER_INFO_LIST); } @Test diff --git a/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java b/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java index 9b9a84b79da3..35b3267ea301 100644 --- a/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java +++ b/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java @@ -156,4 +156,19 @@ public class ProcessStatsTest extends TestCase { eq(0), eq(APP_1_PROCESS_NAME)); } + + @SmallTest + public void testSafelyResetClearsProcessInUidState() throws Exception { + ProcessStats processStats = new ProcessStats(); + ProcessState processState = + processStats.getProcessStateLocked( + APP_1_PACKAGE_NAME, APP_1_UID, APP_1_VERSION, APP_1_PROCESS_NAME); + processState.makeActive(); + UidState uidState = processStats.mUidStates.get(APP_1_UID); + assertTrue(uidState.isInUse()); + processState.makeInactive(); + uidState.resetSafely(NOW_MS); + processState.makeActive(); + assertFalse(uidState.isInUse()); + } } diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 05e17720b175..913eaf2391f6 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1297,6 +1297,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-894942237": { + "message": "Force Playing Transition: %d", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-883738232": { "message": "Adding more than one toast window for UID at a time.", "level": "WARN", @@ -4243,12 +4249,6 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/DisplayContent.java" }, - "1878927091": { - "message": "prepareSurface: No changes in animation for %s", - "level": "VERBOSE", - "group": "WM_DEBUG_ANIM", - "at": "com\/android\/server\/wm\/WindowStateAnimator.java" - }, "1891501279": { "message": "cancelAnimation(): reason=%s", "level": "DEBUG", diff --git a/graphics/java/android/graphics/Gainmap.java b/graphics/java/android/graphics/Gainmap.java index feedb7d3e2bf..9ac84a6159da 100644 --- a/graphics/java/android/graphics/Gainmap.java +++ b/graphics/java/android/graphics/Gainmap.java @@ -25,13 +25,11 @@ import libcore.util.NativeAllocationRegistry; /** * Gainmap represents a mechanism for augmenting an SDR image to produce an HDR one with variable - * display adjustment capability. - * - * It is a combination of a set of metadata describing how to apply the gainmap, as well as either - * a 1 (such as {@link android.graphics.Bitmap.Config#ALPHA_8} or 3 + * display adjustment capability. It is a combination of a set of metadata describing how to apply + * the gainmap, as well as either a 1 (such as {@link android.graphics.Bitmap.Config#ALPHA_8} or 3 * (such as {@link android.graphics.Bitmap.Config#ARGB_8888} with the alpha channel ignored) * channel Bitmap that represents the gainmap data itself. - * + * <p> * When rendering to an {@link android.content.pm.ActivityInfo#COLOR_MODE_HDR} activity, the * hardware accelerated {@link Canvas} will automatically apply the gainmap when sufficient * HDR headroom is available. @@ -45,7 +43,7 @@ import libcore.util.NativeAllocationRegistry; * image, often at a lower resolution (such as 1/4th), along with some metadata to describe * how to apply the gainmap. The gainmap image itself is then a greyscale image representing * the transformation to apply onto the base image to reconstruct an HDR rendition of it. - * + * <p> * As such these "gainmap images" consist of 3 parts - a base {@link Bitmap} with a * {@link Bitmap#getGainmap()} that returns an instance of this class which in turn contains * the enhancement layer represented as another Bitmap, accessible via {@link #getGainmapContents()} @@ -55,25 +53,27 @@ import libcore.util.NativeAllocationRegistry; * When doing custom rendering such as to an OpenGL ES or Vulkan context, the gainmap is not * automatically applied. In such situations, the following steps are appropriate to render the * gainmap in combination with the base image. - * + * <p> * Suppose our display has HDR to SDR ratio of H, and we wish to display an image with gainmap on * this display. Let B be the pixel value from the base image in a color space that has the * primaries of the base image and a linear transfer function. Let G be the pixel value from the * gainmap. Let D be the output pixel in the same color space as B. The value of D is computed * as follows: - * + * <p> * First, let W be a weight parameter determining how much the gainmap will be applied. + * <pre class="prettyprint"> * W = clamp((log(H) - log(minDisplayRatioForHdrTransition)) / - * (log(displayRatioForFullHdr) - log(minDisplayRatioForHdrTransition), 0, 1) + * (log(displayRatioForFullHdr) - log(minDisplayRatioForHdrTransition), 0, 1)</pre> * * Next, let L be the gainmap value in log space. We compute this from the value G that was * sampled from the texture as follows: - * L = mix(log(ratioMin), log(ratioMax), pow(G, gamma)) - * + * <pre class="prettyprint"> + * L = mix(log(ratioMin), log(ratioMax), pow(G, gamma))</pre> * Finally, apply the gainmap to compute D, the displayed pixel. If the base image is SDR then * compute: - * D = (B + epsilonSdr) * exp(L * W) - epsilonHdr - * + * <pre class="prettyprint"> + * D = (B + epsilonSdr) * exp(L * W) - epsilonHdr</pre> + * <p> * In the above math, log() is a natural logarithm and exp() is natural exponentiation. The base * for these functions cancels out and does not affect the result, so other bases may be used * if preferred. @@ -150,7 +150,6 @@ public final class Gainmap implements Parcelable { /** * Sets the gainmap ratio min. For single-plane gainmaps, r, g, and b should be the same. */ - @NonNull public void setRatioMin(float r, float g, float b) { nSetRatioMin(mNativePtr, r, g, b); } @@ -169,7 +168,6 @@ public final class Gainmap implements Parcelable { /** * Sets the gainmap ratio max. For single-plane gainmaps, r, g, and b should be the same. */ - @NonNull public void setRatioMax(float r, float g, float b) { nSetRatioMax(mNativePtr, r, g, b); } @@ -188,7 +186,6 @@ public final class Gainmap implements Parcelable { /** * Sets the gainmap gamma. For single-plane gainmaps, r, g, and b should be the same. */ - @NonNull public void setGamma(float r, float g, float b) { nSetGamma(mNativePtr, r, g, b); } @@ -208,7 +205,6 @@ public final class Gainmap implements Parcelable { * Sets the sdr epsilon which is used to avoid numerical instability. * For single-plane gainmaps, r, g, and b should be the same. */ - @NonNull public void setEpsilonSdr(float r, float g, float b) { nSetEpsilonSdr(mNativePtr, r, g, b); } @@ -228,7 +224,6 @@ public final class Gainmap implements Parcelable { * Sets the hdr epsilon which is used to avoid numerical instability. * For single-plane gainmaps, r, g, and b should be the same. */ - @NonNull public void setEpsilonHdr(float r, float g, float b) { nSetEpsilonHdr(mNativePtr, r, g, b); } @@ -248,8 +243,7 @@ public final class Gainmap implements Parcelable { * Sets the hdr/sdr ratio at which point the gainmap is fully applied. * @param max The hdr/sdr ratio at which the gainmap is fully applied. Must be >= 1.0f */ - @NonNull - public void setDisplayRatioForFullHdr(float max) { + public void setDisplayRatioForFullHdr(@FloatRange(from = 1.0f) float max) { if (!Float.isFinite(max) || max < 1f) { throw new IllegalArgumentException( "setDisplayRatioForFullHdr must be >= 1.0f, got = " + max); @@ -269,7 +263,6 @@ public final class Gainmap implements Parcelable { * Sets the hdr/sdr ratio below which only the SDR image is displayed. * @param min The minimum hdr/sdr ratio at which to begin applying the gainmap. Must be >= 1.0f */ - @NonNull public void setMinDisplayRatioForHdrTransition(@FloatRange(from = 1.0f) float min) { if (!Float.isFinite(min) || min < 1f) { throw new IllegalArgumentException( diff --git a/graphics/java/android/graphics/text/GraphemeBreak.java b/graphics/java/android/graphics/text/GraphemeBreak.java new file mode 100644 index 000000000000..f82b2fd659cc --- /dev/null +++ b/graphics/java/android/graphics/text/GraphemeBreak.java @@ -0,0 +1,59 @@ +/* + * 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 android.graphics.text; + +/** @hide */ +public class GraphemeBreak { + private GraphemeBreak() { } + + /** + * Util method that checks if the offsets in given range are grapheme break. + * + * @param advances the advances of characters in the given text. It contains the font + * information used by the algorithm to determine the grapheme break. It's useful + * when some character is missing in the font. For example, if the smile emoji + * "0xD83D 0xDE0A" is not found in the font and is displayed as 2 characters. + * We can't treat it as a single grapheme cluster. + * @param text the text to be processed. + * @param start the start offset of the queried range, inclusive. + * @param end the end offset of the queried range, exclusive. + * @param isGraphemeBreak the array to receive the result. The i-th element of the + * array will be set to true if the offset (start + i) is a grapheme + * break; otherwise, it will be set to false. + */ + public static void isGraphemeBreak(float[] advances, char[] text, int start, int end, + boolean[] isGraphemeBreak) { + if (start > end) { + throw new IllegalArgumentException("start is greater than end: start = " + start + + " end = " + end); + } + if (advances.length < end) { + throw new IllegalArgumentException("the length of advances is less than end" + + "advances.length = " + advances.length + + " end = " + end); + } + if (isGraphemeBreak.length < end - start) { + throw new IndexOutOfBoundsException("isGraphemeBreak doesn't have enough space to " + + "receive the result, isGraphemeBreak.length: " + isGraphemeBreak.length + + " needed space: " + (end - start)); + } + nIsGraphemeBreak(advances, text, start, end, isGraphemeBreak); + } + + private static native void nIsGraphemeBreak(float[] advances, char[] text, int start, int end, + boolean[] isGraphemeBreak); +} 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 36c0cb6dfe19..852fae695046 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 @@ -61,6 +61,7 @@ import android.os.Binder; import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.service.notification.NotificationListenerService; @@ -129,6 +130,15 @@ public class BubbleController implements ConfigurationChangeListener { private static final String SYSTEM_DIALOG_REASON_KEY = "reason"; private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav"; + // TODO(b/256873975) Should use proper flag when available to shell/launcher + /** + * Whether bubbles are showing in the bubble bar from launcher. This is only available + * on large screens and {@link BubbleController#isShowingAsBubbleBar()} should be used + * to check all conditions that indicate if the bubble bar is in use. + */ + private static final boolean BUBBLE_BAR_ENABLED = + SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false); + private final Context mContext; private final BubblesImpl mImpl = new BubblesImpl(); private Bubbles.BubbleExpandListener mExpandListener; @@ -155,9 +165,6 @@ public class BubbleController implements ConfigurationChangeListener { private final ShellExecutor mBackgroundExecutor; - // Whether or not we should show bubbles pinned at the bottom of the screen. - private boolean mIsBubbleBarEnabled; - private BubbleLogger mLogger; private BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; @@ -540,10 +547,10 @@ public class BubbleController implements ConfigurationChangeListener { mDataRepository.removeBubblesForUser(removedUserId, parentUserId); } - // TODO(b/256873975): Should pass this into the constructor once flags are available to shell. - /** Sets whether the bubble bar is enabled (i.e. bubbles pinned to bottom on large screens). */ - public void setBubbleBarEnabled(boolean enabled) { - mIsBubbleBarEnabled = enabled; + /** Whether bubbles are showing in the bubble bar. */ + public boolean isShowingAsBubbleBar() { + // TODO(b/269670598): should also check that we're in gesture nav + return BUBBLE_BAR_ENABLED && mBubblePositioner.isLargeScreen(); } /** Whether this userId belongs to the current user. */ @@ -612,12 +619,6 @@ public class BubbleController implements ConfigurationChangeListener { mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation); } - if (mIsBubbleBarEnabled && mBubblePositioner.isLargeScreen()) { - mBubblePositioner.setUsePinnedLocation(true); - } else { - mBubblePositioner.setUsePinnedLocation(false); - } - addToWindowManagerMaybe(); } @@ -1918,13 +1919,6 @@ public class BubbleController implements ConfigurationChangeListener { } @Override - public void setBubbleBarEnabled(boolean enabled) { - mMainExecutor.execute(() -> { - BubbleController.this.setBubbleBarEnabled(enabled); - }); - } - - @Override public void onNotificationPanelExpandedChanged(boolean expanded) { mMainExecutor.execute( () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded)); 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 6230d22ebe12..3fd09675a245 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 @@ -283,7 +283,7 @@ public class BubbleData { } boolean isShowingOverflow() { - return mShowingOverflow && (isExpanded() || mPositioner.showingInTaskbar()); + return mShowingOverflow && isExpanded(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 57c7731e69ed..ecddbda0fff4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -16,6 +16,7 @@ package com.android.wm.shell.bubbles; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -226,6 +227,8 @@ public class BubbleExpandedView extends LinearLayout { try { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); + options.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOWED); Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 07c58527a815..5ea2450114f0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -18,9 +18,6 @@ package com.android.wm.shell.bubbles; import static android.view.View.LAYOUT_DIRECTION_RTL; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.annotation.IntDef; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -39,8 +36,6 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; -import java.lang.annotation.Retention; - /** * Keeps track of display size, configuration, and specific bubble sizes. One place for all * placement and positioning calculations to refer to. @@ -50,15 +45,6 @@ public class BubblePositioner { ? "BubblePositioner" : BubbleDebugConfig.TAG_BUBBLES; - @Retention(SOURCE) - @IntDef({TASKBAR_POSITION_NONE, TASKBAR_POSITION_RIGHT, TASKBAR_POSITION_LEFT, - TASKBAR_POSITION_BOTTOM}) - @interface TaskbarPosition {} - public static final int TASKBAR_POSITION_NONE = -1; - public static final int TASKBAR_POSITION_RIGHT = 0; - public static final int TASKBAR_POSITION_LEFT = 1; - public static final int TASKBAR_POSITION_BOTTOM = 2; - /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/ public static final int NUM_VISIBLE_WHEN_RESTING = 2; /** Indicates a bubble's height should be the maximum available space. **/ @@ -108,15 +94,9 @@ public class BubblePositioner { private int mOverflowHeight; private int mMinimumFlyoutWidthLargeScreen; - private PointF mPinLocation; private PointF mRestingStackPosition; private int[] mPaddings = new int[4]; - private boolean mShowingInTaskbar; - private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE; - private int mTaskbarIconSize; - private int mTaskbarSize; - public BubblePositioner(Context context, WindowManager windowManager) { mContext = context; mWindowManager = windowManager; @@ -153,27 +133,11 @@ public class BubblePositioner { + " insets: " + insets + " isLargeScreen: " + mIsLargeScreen + " isSmallTablet: " + mIsSmallTablet - + " bounds: " + bounds - + " showingInTaskbar: " + mShowingInTaskbar); + + " bounds: " + bounds); } updateInternal(mRotation, insets, bounds); } - /** - * Updates position information to account for taskbar state. - * - * @param taskbarPosition which position the taskbar is displayed in. - * @param showingInTaskbar whether the taskbar is being shown. - */ - public void updateForTaskbar(int iconSize, - @TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize) { - mShowingInTaskbar = showingInTaskbar; - mTaskbarIconSize = iconSize; - mTaskbarPosition = taskbarPosition; - mTaskbarSize = taskbarSize; - update(); - } - @VisibleForTesting public void updateInternal(int rotation, Insets insets, Rect bounds) { mRotation = rotation; @@ -232,10 +196,6 @@ public class BubblePositioner { R.dimen.bubbles_flyout_min_width_large_screen); mMaxBubbles = calculateMaxBubbles(); - - if (mShowingInTaskbar) { - adjustForTaskbar(); - } } /** @@ -260,30 +220,6 @@ public class BubblePositioner { return mDefaultMaxBubbles; } - /** - * Taskbar insets appear as navigationBar insets, however, unlike navigationBar this should - * not inset bubbles UI as bubbles floats above the taskbar. This adjust the available space - * and insets to account for the taskbar. - */ - // TODO(b/171559950): When the insets are reported correctly we can remove this logic - private void adjustForTaskbar() { - // When bar is showing on edges... subtract that inset because we appear on top - if (mShowingInTaskbar && mTaskbarPosition != TASKBAR_POSITION_BOTTOM) { - WindowInsets metricInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - Insets navBarInsets = metricInsets.getInsetsIgnoringVisibility( - WindowInsets.Type.navigationBars()); - int newInsetLeft = mInsets.left; - int newInsetRight = mInsets.right; - if (mTaskbarPosition == TASKBAR_POSITION_LEFT) { - mPositionRect.left -= navBarInsets.left; - newInsetLeft -= navBarInsets.left; - } else if (mTaskbarPosition == TASKBAR_POSITION_RIGHT) { - mPositionRect.right += navBarInsets.right; - newInsetRight -= navBarInsets.right; - } - mInsets = Insets.of(newInsetLeft, mInsets.top, newInsetRight, mInsets.bottom); - } - } /** * @return a rect of available screen space accounting for orientation, system bars and cutouts. @@ -327,14 +263,12 @@ public class BubblePositioner { * to the left or right side. */ public boolean showBubblesVertically() { - return isLandscape() || mShowingInTaskbar || mIsLargeScreen; + return isLandscape() || mIsLargeScreen; } /** Size of the bubble. */ public int getBubbleSize() { - return (mShowingInTaskbar && mTaskbarIconSize > 0) - ? mTaskbarIconSize - : mBubbleSize; + return mBubbleSize; } /** The amount of padding at the top of the screen that the bubbles avoid when being placed. */ @@ -699,9 +633,6 @@ public class BubblePositioner { /** The position the bubble stack should rest at when collapsed. */ public PointF getRestingPosition() { - if (mPinLocation != null) { - return mPinLocation; - } if (mRestingStackPosition == null) { return getDefaultStartPosition(); } @@ -713,9 +644,6 @@ public class BubblePositioner { * is being shown. */ public PointF getDefaultStartPosition() { - if (mPinLocation != null) { - return mPinLocation; - } // Start on the left if we're in LTR, right otherwise. final boolean startOnLeft = mContext.getResources().getConfiguration().getLayoutDirection() @@ -730,7 +658,6 @@ public class BubblePositioner { 1 /* default starts with 1 bubble */)); } - /** * Returns the region that the stack position must stay within. This goes slightly off the left * and right sides of the screen, below the status bar/cutout and above the navigation bar. @@ -751,39 +678,6 @@ public class BubblePositioner { } /** - * @return whether the bubble stack is pinned to the taskbar. - */ - public boolean showingInTaskbar() { - return mShowingInTaskbar; - } - - /** - * @return the taskbar position if set. - */ - public int getTaskbarPosition() { - return mTaskbarPosition; - } - - public int getTaskbarSize() { - return mTaskbarSize; - } - - /** - * In some situations bubbles will be pinned to a specific onscreen location. This sets whether - * bubbles should be pinned or not. - */ - public void setUsePinnedLocation(boolean usePinnedLocation) { - if (usePinnedLocation) { - mShowingInTaskbar = true; - mPinLocation = new PointF(mPositionRect.right - mBubbleSize, - mPositionRect.bottom - mBubbleSize); - } else { - mPinLocation = null; - mShowingInTaskbar = false; - } - } - - /** * Navigation bar has an area where system gestures can be started from. * * @return {@link Rect} for system navigation bar gesture zone diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index abe42eec7061..7d71089ef4fa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -680,8 +680,6 @@ public class BubbleStackView extends FrameLayout // Re-show the expanded view if we hid it. showExpandedViewIfNeeded(); - } else if (mPositioner.showingInTaskbar()) { - mStackAnimationController.snapStackBack(); } else { // Fling the stack to the edge, and save whether or not it's going to end up on // the left side of the screen. 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 1753cda895fe..4c0a93fb9355 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 @@ -273,11 +273,6 @@ public interface Bubbles { */ void onUserRemoved(int removedUserId); - /** - * Sets whether bubble bar should be enabled or not. - */ - void setBubbleBarEnabled(boolean enabled); - /** Listener to find out about stack expansion / collapse events. */ interface BubbleExpandListener { /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 0ee0ea60a1bc..5533842f2d89 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -417,23 +417,9 @@ public class StackAnimationController extends } /** - * Snaps the stack back to the previous resting position. - */ - public void snapStackBack() { - if (mLayout == null) { - return; - } - PointF p = getStackPositionAlongNearestHorizontalEdge(); - springStackAfterFling(p.x, p.y); - } - - /** * Where the stack would be if it were snapped to the nearest horizontal edge (left or right). */ public PointF getStackPositionAlongNearestHorizontalEdge() { - if (mPositioner.showingInTaskbar()) { - return mPositioner.getRestingPosition(); - } final PointF stackPos = getStackPosition(); final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java index 480bf93b2ddb..53bf42a3c911 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java @@ -39,6 +39,9 @@ import android.window.TaskSnapshot; * Represents the content overlay used during the entering PiP animation. */ public abstract class PipContentOverlay { + // Fixed string used in WMShellFlickerTests + protected static final String LAYER_NAME = "PipContentOverlay"; + protected SurfaceControl mLeash; /** Attaches the internal {@link #mLeash} to the given parent leash. */ @@ -86,7 +89,7 @@ public abstract class PipContentOverlay { mContext = context; mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) - .setName(TAG) + .setName(LAYER_NAME) .setColorLayer() .build(); } @@ -139,7 +142,7 @@ public abstract class PipContentOverlay { mSourceRectHint = new Rect(sourceRectHint); mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) - .setName(TAG) + .setName(LAYER_NAME) .build(); } @@ -194,7 +197,7 @@ public abstract class PipContentOverlay { prepareAppIconOverlay(activityInfo); mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) - .setName(TAG) + .setName(LAYER_NAME) .build(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index f11836ea5bee..e9d257139779 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -1594,7 +1594,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // source rect hint to enter PiP use bounds animation. if (sourceHintRect == null) { if (SystemProperties.getBoolean( - "persist.wm.debug.enable_pip_app_icon_overlay", false)) { + "persist.wm.debug.enable_pip_app_icon_overlay", true)) { animator.setAppIconContentOverlay( mContext, currentBounds, mTaskInfo.topActivityInfo); } else { 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 6b0337d3fb4a..a91a3424f3a0 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 @@ -804,7 +804,7 @@ public class PipTransition extends PipTransitionController { // We use content overlay when there is no source rect hint to enter PiP use bounds // animation. if (SystemProperties.getBoolean( - "persist.wm.debug.enable_pip_app_icon_overlay", false)) { + "persist.wm.debug.enable_pip_app_icon_overlay", true)) { animator.setAppIconContentOverlay( mContext, currentBounds, taskInfo.topActivityInfo); } else { 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 225258773013..146abea2bc31 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 @@ -204,7 +204,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // and exit, since exit itself can trigger a number of changes that update the stages. private boolean mShouldUpdateRecents; private boolean mExitSplitScreenOnHide; - private boolean mIsSplitEntering; + private boolean mIsDividerRemoteAnimating; private boolean mIsDropEntering; private boolean mIsExiting; @@ -764,17 +764,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); if (pendingIntent2 == null) { - // Launching a solo task. - ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); - activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); - options1 = activityOptions.toBundle(); - addActivityOptions(options1, null /* launchTarget */); - if (shortcutInfo1 != null) { - wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1); - } else { - wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1); - } - mSyncQueue.queue(wct); + // Launching a solo intent or shortcut as fullscreen. + launchAsFullscreenWithRemoteAnimation(pendingIntent1, fillInIntent1, shortcutInfo1, + options1, adapter, wct); return; } @@ -797,13 +789,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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); + // Launching a solo intent as fullscreen. + launchAsFullscreenWithRemoteAnimation(pendingIntent, fillInIntent, null, options1, + adapter, wct); return; } @@ -822,13 +810,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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.startShortcut(mContext.getPackageName(), shortcutInfo, options1); - mSyncQueue.queue(wct); + // Launching a solo shortcut as fullscreen. + launchAsFullscreenWithRemoteAnimation(null, null, shortcutInfo, options1, adapter, wct); return; } @@ -838,6 +821,49 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, instanceId); } + private void launchAsFullscreenWithRemoteAnimation(@Nullable PendingIntent pendingIntent, + @Nullable Intent fillInIntent, @Nullable ShortcutInfo shortcutInfo, + @Nullable Bundle options, RemoteAnimationAdapter adapter, + WindowContainerTransaction wct) { + LegacyTransitions.ILegacyTransition transition = + (transit, apps, wallpapers, nonApps, finishedCallback, t) -> { + if (apps == null || apps.length == 0) { + onRemoteAnimationFinished(apps); + t.apply(); + try { + adapter.getRunner().onAnimationCancelled(mKeyguardShowing); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + return; + } + + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } + } + t.apply(); + + try { + adapter.getRunner().onAnimationStart( + transit, apps, wallpapers, nonApps, finishedCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + }; + + addActivityOptions(options, null /* launchTarget */); + if (shortcutInfo != null) { + wct.startShortcut(mContext.getPackageName(), shortcutInfo, options); + } else if (pendingIntent != null) { + wct.sendPendingIntent(pendingIntent, fillInIntent, options); + } else { + Slog.e(TAG, "Pending intent and shortcut are null is invalid case."); + } + mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); + } + private void startWithLegacyTransition(WindowContainerTransaction wct, @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle mainOptions, @@ -881,7 +907,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Set false to avoid record new bounds with old task still on top; mShouldUpdateRecents = false; - mIsSplitEntering = true; + mIsDividerRemoteAnimating = true; if (mSplitRequest == null) { mSplitRequest = new SplitRequest(mainTaskId, mainPendingIntent != null ? mainPendingIntent.getIntent() : null, @@ -894,23 +920,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (options == null) options = new Bundle(); addActivityOptions(options, mMainStage); - options = wrapAsSplitRemoteAnimation(adapter, options); updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); // TODO(b/268008375): Merge APIs to start a split pair into one. if (mainTaskId != INVALID_TASK_ID) { + options = wrapAsSplitRemoteAnimation(adapter, options); wct.startTask(mainTaskId, options); - } else if (mainShortcutInfo != null) { - wct.startShortcut(mContext.getPackageName(), mainShortcutInfo, options); + mSyncQueue.queue(wct); } else { - wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, options); + if (mainShortcutInfo != null) { + wct.startShortcut(mContext.getPackageName(), mainShortcutInfo, options); + } else { + wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, options); + } + mSyncQueue.queue(wrapAsSplitRemoteAnimation(adapter), WindowManager.TRANSIT_OPEN, wct); } - wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); - - mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { setDividerVisibility(true, t); }); @@ -967,6 +995,54 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return activityOptions.toBundle(); } + private LegacyTransitions.ILegacyTransition wrapAsSplitRemoteAnimation( + RemoteAnimationAdapter adapter) { + LegacyTransitions.ILegacyTransition transition = + (transit, apps, wallpapers, nonApps, finishedCallback, t) -> { + if (apps == null || apps.length == 0) { + onRemoteAnimationFinished(apps); + t.apply(); + try { + adapter.getRunner().onAnimationCancelled(mKeyguardShowing); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + return; + } + + // Wrap the divider bar into non-apps target to animate together. + nonApps = ArrayUtils.appendElement(RemoteAnimationTarget.class, nonApps, + getDividerBarLegacyTarget()); + + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + // Reset the surface position of the opening app to prevent offset. + t.setPosition(apps[i].leash, 0, 0); + } + } + t.apply(); + + IRemoteAnimationFinishedCallback wrapCallback = + new IRemoteAnimationFinishedCallback.Stub() { + @Override + public void onAnimationFinished() throws RemoteException { + onRemoteAnimationFinished(apps); + finishedCallback.onAnimationFinished(); + } + }; + Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication()); + try { + adapter.getRunner().onAnimationStart( + transit, apps, wallpapers, nonApps, wrapCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + }; + + return transition; + } + private void setEnterInstanceId(InstanceId instanceId) { if (instanceId != null) { mLogger.enterRequested(instanceId, ENTER_REASON_LAUNCHER); @@ -974,7 +1050,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } private void onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) { - mIsSplitEntering = false; + mIsDividerRemoteAnimating = false; mShouldUpdateRecents = true; mSplitRequest = null; // If any stage has no child after animation finished, it means that split will display @@ -993,6 +1069,27 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + private void onRemoteAnimationFinished(RemoteAnimationTarget[] apps) { + mIsDividerRemoteAnimating = false; + mShouldUpdateRecents = true; + mSplitRequest = null; + // If any stage has no child after finished animation, that side of the split will display + // nothing. This might happen if starting the same app on the both sides while not + // supporting multi-instance. Exit the split screen and expand that app to full screen. + if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { + mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0 + ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + mSplitUnsupportedToast.show(); + return; + } + + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + prepareEvictNonOpeningChildTasks(SPLIT_POSITION_TOP_OR_LEFT, apps, evictWct); + prepareEvictNonOpeningChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, apps, evictWct); + mSyncQueue.queue(evictWct); + } + + /** * Collects all the current child tasks of a specific split and prepares transaction to evict * them to display. @@ -1247,7 +1344,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } }); mShouldUpdateRecents = false; - mIsSplitEntering = false; + mIsDividerRemoteAnimating = false; mSplitLayout.getInvisibleBounds(mTempRect1); if (childrenToTop == null || childrenToTop.getTopVisibleChildTaskId() == INVALID_TASK_ID) { @@ -1590,7 +1687,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, && !ENABLE_SHELL_TRANSITIONS) { // Clear the divider remote animating flag as the divider will be re-rendered to apply // the new rotation config. - mIsSplitEntering = false; + mIsDividerRemoteAnimating = false; mSplitLayout.update(null /* t */); onLayoutSizeChanged(mSplitLayout); } @@ -1640,9 +1737,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } void onChildTaskAppeared(StageListenerImpl stageListener, int taskId) { + // Handle entering split screen while there is a split pair running in the background. if (stageListener == mSideStageListener && !isSplitScreenVisible() && isSplitActive() - && !mIsSplitEntering) { - // Handle entring split case here if split already running background. + && mSplitRequest == null) { if (mIsDropEntering) { mSplitLayout.resetDividerPosition(); } else { @@ -1734,7 +1831,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDividerVisible = visible; sendSplitVisibilityChanged(); - if (mIsSplitEntering) { + if (mIsDividerRemoteAnimating) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, " Skip animating divider bar due to it's remote animating."); return; @@ -1754,7 +1851,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, " Skip animating divider bar due to divider leash not ready."); return; } - if (mIsSplitEntering) { + if (mIsDividerRemoteAnimating) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, " Skip animating divider bar due to it's remote animating."); return; @@ -1822,7 +1919,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.flingDividerToDismiss( mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT, EXIT_REASON_APP_FINISHED); - } else if (!isSplitScreenVisible() && !mIsSplitEntering) { + } else if (!isSplitScreenVisible() && mSplitRequest == null) { + // Dismiss split screen in the background once any sides of the split become empty. exitSplitScreen(null /* childrenToTop */, EXIT_REASON_APP_FINISHED); } } else if (isSideStage && hasChildren && !mMainStage.isActive()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index ef405c858e3d..75112b62c1c6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -78,7 +78,6 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { int mAnimType = 0; final IBinder mTransition; - Transitions.TransitionFinishCallback mFinishCallback = null; Transitions.TransitionHandler mLeftoversHandler = null; WindowContainerTransaction mFinishWCT = null; @@ -241,20 +240,25 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { } if (pipChange == null) { if (mixed.mLeftoversHandler != null) { - return mixed.mLeftoversHandler.startAnimation(mixed.mTransition, info, - startTransaction, finishTransaction, finishCallback); + if (mixed.mLeftoversHandler.startAnimation(mixed.mTransition, + info, startTransaction, finishTransaction, (wct, wctCB) -> { + mActiveTransitions.remove(mixed); + finishCallback.onTransitionFinished(wct, wctCB); + })) { + return true; + } } + mActiveTransitions.remove(mixed); return false; } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate" + " animation because remote-animation likely doesn't support it"); - mixed.mFinishCallback = finishCallback; Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { --mixed.mInFlightSubAnimations; mixed.joinFinishArgs(wct, wctCB); if (mixed.mInFlightSubAnimations > 0) return; mActiveTransitions.remove(mixed); - mixed.mFinishCallback.onTransitionFinished(mixed.mFinishWCT, wctCB); + finishCallback.onTransitionFinished(mixed.mFinishWCT, wctCB); }; // Split the transition into 2 parts: the pip part and the rest. mixed.mInFlightSubAnimations = 2; @@ -304,10 +308,10 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { } if (pipChange == null) { // um, something probably went wrong. + mActiveTransitions.remove(mixed); return false; } final boolean isGoingHome = homeIsOpening; - mixed.mFinishCallback = finishCallback; Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { --mixed.mInFlightSubAnimations; mixed.joinFinishArgs(wct, wctCB); @@ -316,7 +320,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { if (isGoingHome) { mSplitHandler.onTransitionAnimationComplete(); } - mixed.mFinishCallback.onTransitionFinished(mixed.mFinishWCT, wctCB); + finishCallback.onTransitionFinished(mixed.mFinishWCT, wctCB); }; if (isGoingHome) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed " @@ -408,7 +412,6 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { unlinkMissingParents(everythingElse); final MixedTransition mixed = new MixedTransition( MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition); - mixed.mFinishCallback = finishCallback; mActiveTransitions.add(mixed); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change " + "and split change."); @@ -420,7 +423,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { mixed.joinFinishArgs(wct, wctCB); if (mixed.mInFlightSubAnimations > 0) return; mActiveTransitions.remove(mixed); - mixed.mFinishCallback.onTransitionFinished(mixed.mFinishWCT, null /* wctCB */); + finishCallback.onTransitionFinished(mixed.mFinishWCT, null /* wctCB */); }; // Dispatch the display change. This will most-likely be taken by the default handler. @@ -447,7 +450,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { // Already done, so no need to end it. return; } - if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { + if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) { + // queue since no actual animation. + } else if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) { boolean ended = mSplitHandler.end(); // If split couldn't end (because it is remote), then don't end everything else @@ -461,8 +466,12 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { } else { mPipHandler.end(); } - } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) { - // queue + } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) { + mPipHandler.end(); + if (mixed.mLeftoversHandler != null) { + mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, + finishCallback); + } } else { throw new IllegalStateException("Playing a mixed transition with unknown type? " + mixed.mType); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java new file mode 100644 index 000000000000..0386ec38a3ff --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java @@ -0,0 +1,65 @@ +/* + * 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.transition; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.util.Log; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import java.util.ArrayList; + +/** + * A Simple handler that tracks SLEEP transitions. We track them specially since we (ab)use these + * as sentinels for fast-forwarding through animations when the screen is off. + * + * There should only be one SleepHandler and it is used explicitly by {@link Transitions} so we + * don't register it like a normal handler. + */ +class SleepHandler implements Transitions.TransitionHandler { + final ArrayList<IBinder> mSleepTransitions = new ArrayList<>(); + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + startTransaction.apply(); + finishCallback.onTransitionFinished(null, null); + mSleepTransitions.remove(transition); + return true; + } + + @Override + @Nullable + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + mSleepTransitions.add(transition); + return new WindowContainerTransaction(); + } + + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishTransaction) { + Log.e(Transitions.TAG, "Sleep transition was consumed. This doesn't make sense"); + mSleepTransitions.remove(transition); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index 8d29901c3a07..bcc37baa5b00 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -143,8 +143,12 @@ public class TransitionAnimationHelper { Animation a = null; if (animAttr != 0) { if (overrideType == ANIM_FROM_STYLE && !isTask) { - a = loadCustomActivityTransition(animAttr, options, enter, transitionAnimation); - if (a == null) { + final TransitionInfo.AnimationOptions.CustomActivityTransition customTransition = + getCustomActivityTransition(animAttr, options); + if (customTransition != null) { + a = loadCustomActivityTransition( + customTransition, options, enter, transitionAnimation); + } else { a = transitionAnimation .loadAnimationAttr(options.getPackageName(), options.getAnimations(), animAttr, translucent); @@ -161,10 +165,8 @@ public class TransitionAnimationHelper { return a; } - static Animation loadCustomActivityTransition(int animAttr, - TransitionInfo.AnimationOptions options, boolean enter, - TransitionAnimation transitionAnimation) { - Animation a = null; + static TransitionInfo.AnimationOptions.CustomActivityTransition getCustomActivityTransition( + int animAttr, TransitionInfo.AnimationOptions options) { boolean isOpen = false; switch (animAttr) { case R.styleable.WindowAnimation_activityOpenEnterAnimation: @@ -178,17 +180,19 @@ public class TransitionAnimationHelper { return null; } - final TransitionInfo.AnimationOptions.CustomActivityTransition transitionAnim = - options.getCustomActivityTransition(isOpen); - if (transitionAnim != null) { - a = transitionAnimation.loadAppTransitionAnimation(options.getPackageName(), - enter ? transitionAnim.getCustomEnterResId() - : transitionAnim.getCustomExitResId()); - if (a != null && transitionAnim.getCustomBackgroundColor() != 0) { - a.setBackdropColor(transitionAnim.getCustomBackgroundColor()); - } - } + return options.getCustomActivityTransition(isOpen); + } + static Animation loadCustomActivityTransition( + @NonNull TransitionInfo.AnimationOptions.CustomActivityTransition transitionAnim, + TransitionInfo.AnimationOptions options, boolean enter, + TransitionAnimation transitionAnimation) { + final Animation a = transitionAnimation.loadAppTransitionAnimation(options.getPackageName(), + enter ? transitionAnim.getCustomEnterResId() + : transitionAnim.getCustomExitResId()); + if (a != null && transitionAnim.getCustomBackgroundColor() != 0) { + a.setBackdropColor(transitionAnim.getCustomBackgroundColor()); + } return a; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 3b154d1cda83..155990a40836 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -21,6 +21,7 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.fixScale; @@ -124,6 +125,7 @@ public class Transitions implements RemoteCallable<Transitions> { private final DisplayController mDisplayController; private final ShellController mShellController; private final ShellTransitionImpl mImpl = new ShellTransitionImpl(); + private final SleepHandler mSleepHandler = new SleepHandler(); private boolean mIsRegistered = false; @@ -137,6 +139,14 @@ public class Transitions implements RemoteCallable<Transitions> { private float mTransitionAnimationScaleSetting = 1.0f; + /** + * How much time we allow for an animation to finish itself on sleep. If it takes longer, we + * will force-finish it (on this end) which may leave it in a bad state but won't hang the + * device. This needs to be pretty small because it is an allowance for each queued animation, + * however it can't be too small since there is some potential IPC involved. + */ + private static final int SLEEP_ALLOWANCE_MS = 120; + private static final class ActiveTransition { IBinder mToken; TransitionHandler mHandler; @@ -478,11 +488,29 @@ public class Transitions implements RemoteCallable<Transitions> { + Arrays.toString(mActiveTransitions.stream().map( activeTransition -> activeTransition.mToken).toArray())); } + final ActiveTransition active = mActiveTransitions.get(activeIdx); for (int i = 0; i < mObservers.size(); ++i) { mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT); } + if (info.getType() == TRANSIT_SLEEP) { + if (activeIdx > 0) { + active.mInfo = info; + active.mStartT = t; + active.mFinishT = finishT; + if (!info.getRootLeash().isValid()) { + // Shell has some debug settings which makes calling binders with invalid + // surfaces crash, so replace it with a "real" one. + info.setRootLeash(new SurfaceControl.Builder().setName("Invalid") + .setContainerLayer().build(), 0, 0); + } + // Sleep starts a process of forcing all prior transitions to finish immediately + finishForSleep(null /* forceFinish */); + return; + } + } + // Allow to notify keyguard un-occluding state to KeyguardService, which can happen while // screen-off, so there might no visibility change involved. if (!info.getRootLeash().isValid() && info.getType() != TRANSIT_KEYGUARD_UNOCCLUDE) { @@ -527,7 +555,6 @@ public class Transitions implements RemoteCallable<Transitions> { return; } - final ActiveTransition active = mActiveTransitions.get(activeIdx); active.mInfo = info; active.mStartT = t; active.mFinishT = finishT; @@ -772,6 +799,12 @@ public class Transitions implements RemoteCallable<Transitions> { ++mergeIdx; continue; } + if (mergeCandidate.mInfo == null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition merge candidate" + + " %s is not ready yet", mergeCandidate.mToken); + // The later transition should not be merged if the prior one is not ready. + return; + } if (mergeCandidate.mMerged) { throw new IllegalStateException("Can't merge a transition after not-merging" + " a preceding one."); @@ -797,23 +830,30 @@ public class Transitions implements RemoteCallable<Transitions> { } final ActiveTransition active = new ActiveTransition(); WindowContainerTransaction wct = null; - for (int i = mHandlers.size() - 1; i >= 0; --i) { - wct = mHandlers.get(i).handleRequest(transitionToken, request); - if (wct != null) { - active.mHandler = mHandlers.get(i); - break; + + // If we have sleep, we use a special handler and we try to finish everything ASAP. + if (request.getType() == TRANSIT_SLEEP) { + mSleepHandler.handleRequest(transitionToken, request); + active.mHandler = mSleepHandler; + } else { + for (int i = mHandlers.size() - 1; i >= 0; --i) { + wct = mHandlers.get(i).handleRequest(transitionToken, request); + if (wct != null) { + active.mHandler = mHandlers.get(i); + break; + } } - } - if (request.getDisplayChange() != null) { - TransitionRequestInfo.DisplayChange change = request.getDisplayChange(); - if (change.getEndRotation() != change.getStartRotation()) { - // Is a rotation, so dispatch to all displayChange listeners - if (wct == null) { - wct = new WindowContainerTransaction(); + if (request.getDisplayChange() != null) { + TransitionRequestInfo.DisplayChange change = request.getDisplayChange(); + if (change.getEndRotation() != change.getStartRotation()) { + // Is a rotation, so dispatch to all displayChange listeners + if (wct == null) { + wct = new WindowContainerTransaction(); + } + mDisplayController.getChangeController().dispatchOnDisplayChange(wct, + change.getDisplayId(), change.getStartRotation(), + change.getEndRotation(), null /* newDisplayAreaInfo */); } - mDisplayController.getChangeController().dispatchOnDisplayChange(wct, - change.getDisplayId(), change.getStartRotation(), change.getEndRotation(), - null /* newDisplayAreaInfo */); } } mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct); @@ -840,6 +880,56 @@ public class Transitions implements RemoteCallable<Transitions> { } /** + * Finish running animations (almost) immediately when a SLEEP transition comes in. We use this + * as both a way to reduce unnecessary work (animations not visible while screen off) and as a + * failsafe to unblock "stuck" animations (in particular remote animations). + * + * This works by "merging" the sleep transition into the currently-playing transition (even if + * its out-of-order) -- turning SLEEP into a signal. If the playing transition doesn't finish + * within `SLEEP_ALLOWANCE_MS` from this merge attempt, this will then finish it directly (and + * send an abort/consumed message). + * + * This is then repeated until there are no more pending sleep transitions. + * + * @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge + * signal to -- so it will be force-finished if it's still running. + */ + private void finishForSleep(@Nullable IBinder forceFinish) { + if (mActiveTransitions.isEmpty() || mSleepHandler.mSleepTransitions.isEmpty()) { + return; + } + if (forceFinish != null && mActiveTransitions.get(0).mToken == forceFinish) { + Log.e(TAG, "Forcing transition to finish due to sleep timeout: " + + mActiveTransitions.get(0).mToken); + onFinish(mActiveTransitions.get(0).mToken, null, null, true); + } + final SurfaceControl.Transaction dummyT = new SurfaceControl.Transaction(); + while (!mActiveTransitions.isEmpty() && !mSleepHandler.mSleepTransitions.isEmpty()) { + final ActiveTransition playing = mActiveTransitions.get(0); + int sleepIdx = findActiveTransition(mSleepHandler.mSleepTransitions.get(0)); + if (sleepIdx >= 0) { + // Try to signal that we are sleeping by attempting to merge the sleep transition + // into the playing one. + final ActiveTransition nextSleep = mActiveTransitions.get(sleepIdx); + playing.mHandler.mergeAnimation(nextSleep.mToken, nextSleep.mInfo, dummyT, + playing.mToken, (wct, cb) -> {}); + } else { + Log.e(TAG, "Couldn't find sleep transition in active list: " + + mSleepHandler.mSleepTransitions.get(0)); + } + // it's possible to complete immediately. If that happens, just repeat the signal + // loop until we either finish everything or start playing an animation that isn't + // finishing immediately. + if (!mActiveTransitions.isEmpty() && mActiveTransitions.get(0) == playing) { + // Give it a (very) short amount of time to process it before forcing. + mMainExecutor.executeDelayed( + () -> finishForSleep(playing.mToken), SLEEP_ALLOWANCE_MS); + break; + } + } + } + + /** * Interface for a callback that must be called after a TransitionHandler finishes playing an * animation. */ diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt index da80c6f46976..5c9920970761 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt @@ -80,7 +80,8 @@ class EnterSplitScreenByDragFromTaskbar(flicker: FlickerTest) : SplitScreenBase( @IwTest(focusArea = "sysui") @Presubmit @Test - fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = false) + fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = false, + appExistAtStart = false) @Presubmit @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 6c9b186b7ede..e63bbeb05575 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -563,6 +563,33 @@ public class ShellTransitionTests extends ShellTestCase { assertEquals(0, mDefaultHandler.activeCount()); } + + @Test + public void testTransitionMergingOnFinish() { + final Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + // The current transition. + final IBinder transitToken1 = new Binder(); + requestStartTransition(transitions, transitToken1); + onTransitionReady(transitions, transitToken1); + + // The next ready transition. + final IBinder transitToken2 = new Binder(); + requestStartTransition(transitions, transitToken2); + onTransitionReady(transitions, transitToken2); + + // The non-ready merge candidate. + final IBinder transitTokenNotReady = new Binder(); + requestStartTransition(transitions, transitTokenNotReady); + + mDefaultHandler.setSimulateMerge(true); + mDefaultHandler.mFinishes.get(0).onTransitionFinished(null /* wct */, null /* wctCB */); + + // Make sure that the non-ready transition is not merged. + assertEquals(0, mDefaultHandler.mergeCount()); + } + @Test public void testTransitionOrderMatchesCore() { Transitions transitions = createTestTransitions(); @@ -1036,6 +1063,21 @@ public class ShellTransitionTests extends ShellTestCase { } } + private static void requestStartTransition(Transitions transitions, IBinder token) { + transitions.requestStartTransition(token, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + } + + private static void onTransitionReady(Transitions transitions, IBinder token) { + transitions.onTransitionReady(token, createTransitionInfo(), + mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class)); + } + + private static TransitionInfo createTransitionInfo() { + return new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + } + private static SurfaceControl createMockSurface(boolean valid) { SurfaceControl sc = mock(SurfaceControl.class); doReturn(valid).when(sc).isValid(); diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java index 5ecec4ddd1ad..3125f088c72b 100644 --- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java @@ -72,6 +72,7 @@ public final class LowLightDreamManager { public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2; private final DreamManager mDreamManager; + private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; @Nullable private final ComponentName mLowLightDreamComponent; @@ -81,8 +82,10 @@ public final class LowLightDreamManager { @Inject public LowLightDreamManager( DreamManager dreamManager, + LowLightTransitionCoordinator lowLightTransitionCoordinator, @Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) { mDreamManager = dreamManager; + mLowLightTransitionCoordinator = lowLightTransitionCoordinator; mLowLightDreamComponent = lowLightDreamComponent; } @@ -111,7 +114,9 @@ public final class LowLightDreamManager { mAmbientLightMode = ambientLightMode; - mDreamManager.setSystemDreamComponent(mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT - ? mLowLightDreamComponent : null); + boolean shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT; + mLowLightTransitionCoordinator.notifyBeforeLowLightTransition(shouldEnterLowLight, + () -> mDreamManager.setSystemDreamComponent( + shouldEnterLowLight ? mLowLightDreamComponent : null)); } } diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java new file mode 100644 index 000000000000..874a2d5af75e --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java @@ -0,0 +1,111 @@ +/* + * 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.dream.lowlight; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.Nullable; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Helper class that allows listening and running animations before entering or exiting low light. + */ +@Singleton +public class LowLightTransitionCoordinator { + /** + * Listener that is notified before low light entry. + */ + public interface LowLightEnterListener { + /** + * Callback that is notified before the device enters low light. + * + * @return an optional animator that will be waited upon before entering low light. + */ + Animator onBeforeEnterLowLight(); + } + + /** + * Listener that is notified before low light exit. + */ + public interface LowLightExitListener { + /** + * Callback that is notified before the device exits low light. + * + * @return an optional animator that will be waited upon before exiting low light. + */ + Animator onBeforeExitLowLight(); + } + + private LowLightEnterListener mLowLightEnterListener; + private LowLightExitListener mLowLightExitListener; + + @Inject + public LowLightTransitionCoordinator() { + } + + /** + * Sets the listener for the low light enter event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + public void setLowLightEnterListener(@Nullable LowLightEnterListener lowLightEnterListener) { + mLowLightEnterListener = lowLightEnterListener; + } + + /** + * Sets the listener for the low light exit event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + public void setLowLightExitListener(@Nullable LowLightExitListener lowLightExitListener) { + mLowLightExitListener = lowLightExitListener; + } + + /** + * Notifies listeners that the device is about to enter or exit low light. + * + * @param entering true if listeners should be notified before entering low light, false if this + * is notifying before exiting. + * @param callback callback that will be run after listeners complete. + */ + void notifyBeforeLowLightTransition(boolean entering, Runnable callback) { + Animator animator = null; + + if (entering && mLowLightEnterListener != null) { + animator = mLowLightEnterListener.onBeforeEnterLowLight(); + } else if (!entering && mLowLightExitListener != null) { + animator = mLowLightExitListener.onBeforeExitLowLight(); + } + + // If the listener returned an animator to indicate it was running an animation, run the + // callback after the animation completes, otherwise call the callback directly. + if (animator != null) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + callback.run(); + } + }); + } else { + callback.run(); + } + } +} diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java index 91a170f7ae14..4b95d8c84bac 100644 --- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java @@ -21,7 +21,10 @@ import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -44,44 +47,52 @@ public class LowLightDreamManagerTest { private DreamManager mDreamManager; @Mock + private LowLightTransitionCoordinator mTransitionCoordinator; + + @Mock private ComponentName mDreamComponent; + LowLightDreamManager mLowLightDreamManager; + @Before public void setUp() { MockitoAnnotations.initMocks(this); + + // Automatically run any provided Runnable to mTransitionCoordinator to simplify testing. + doAnswer(invocation -> { + ((Runnable) invocation.getArgument(1)).run(); + return null; + }).when(mTransitionCoordinator).notifyBeforeLowLightTransition(anyBoolean(), + any(Runnable.class)); + + mLowLightDreamManager = new LowLightDreamManager(mDreamManager, mTransitionCoordinator, + mDreamComponent); } @Test public void setAmbientLightMode_lowLight_setSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(true), any()); verify(mDreamManager).setSystemDreamComponent(mDreamComponent); } @Test public void setAmbientLightMode_regularLight_clearSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); + verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(false), any()); verify(mDreamManager).setSystemDreamComponent(null); } @Test public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - // Set to low light first. - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); clearInvocations(mDreamManager); // Return to default unknown mode. - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); verify(mDreamManager).setSystemDreamComponent(null); } @@ -89,7 +100,7 @@ public class LowLightDreamManagerTest { @Test public void setAmbientLightMode_dreamComponentNotSet_doNothing() { final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - null /*dream component*/); + mTransitionCoordinator, null /*dream component*/); lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java new file mode 100644 index 000000000000..81e1e33d6220 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java @@ -0,0 +1,113 @@ +/* + * 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.dream.lowlight; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.animation.Animator; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +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; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class LowLightTransitionCoordinatorTest { + @Mock + private LowLightTransitionCoordinator.LowLightEnterListener mEnterListener; + + @Mock + private LowLightTransitionCoordinator.LowLightExitListener mExitListener; + + @Mock + private Animator mAnimator; + + @Captor + private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor; + + @Mock + private Runnable mRunnable; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void onEnterCalledOnListeners() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightEnterListener(mEnterListener); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + verify(mEnterListener).onBeforeEnterLowLight(); + verify(mRunnable).run(); + } + + @Test + public void onExitCalledOnListeners() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightExitListener(mExitListener); + + coordinator.notifyBeforeLowLightTransition(false, mRunnable); + + verify(mExitListener).onBeforeExitLowLight(); + verify(mRunnable).run(); + } + + @Test + public void listenerNotCalledAfterRemoval() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightEnterListener(mEnterListener); + coordinator.setLowLightEnterListener(null); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + verifyZeroInteractions(mEnterListener); + verify(mRunnable).run(); + } + + @Test + public void runnableCalledAfterAnimationEnds() { + when(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator); + + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + coordinator.setLowLightEnterListener(mEnterListener); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()); + verifyZeroInteractions(mRunnable); + + // Runnable is run once the animation ends. + mAnimatorListenerCaptor.getValue().onAnimationEnd(null); + verify(mRunnable).run(); + } +} diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 536bb49675f1..7228b895ebc4 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -338,6 +338,7 @@ cc_defaults { "jni/android_util_PathParser.cpp", "jni/Bitmap.cpp", + "jni/BufferUtils.cpp", "jni/HardwareBufferHelpers.cpp", "jni/BitmapFactory.cpp", "jni/ByteBufferStreamAdaptor.cpp", @@ -374,6 +375,7 @@ cc_defaults { "jni/text/LineBreaker.cpp", "jni/text/MeasuredText.cpp", "jni/text/TextShaper.cpp", + "jni/text/GraphemeBreak.cpp", ], header_libs: [ diff --git a/libs/hwui/Mesh.h b/libs/hwui/Mesh.h index 983681707415..13e3c8e7bf77 100644 --- a/libs/hwui/Mesh.h +++ b/libs/hwui/Mesh.h @@ -104,33 +104,31 @@ private: class Mesh { public: - Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode, const void* vertexBuffer, - size_t vertexBufferSize, jint vertexCount, jint vertexOffset, + Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode, + std::vector<uint8_t>&& vertexBufferData, jint vertexCount, jint vertexOffset, std::unique_ptr<MeshUniformBuilder> builder, const SkRect& bounds) : mMeshSpec(meshSpec) , mMode(mode) + , mVertexBufferData(std::move(vertexBufferData)) , mVertexCount(vertexCount) , mVertexOffset(vertexOffset) , mBuilder(std::move(builder)) - , mBounds(bounds) { - copyToVector(mVertexBufferData, vertexBuffer, vertexBufferSize); - } + , mBounds(bounds) {} - Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode, const void* vertexBuffer, - size_t vertexBufferSize, jint vertexCount, jint vertexOffset, const void* indexBuffer, - size_t indexBufferSize, jint indexCount, jint indexOffset, + Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode, + std::vector<uint8_t>&& vertexBufferData, jint vertexCount, jint vertexOffset, + std::vector<uint8_t>&& indexBuffer, jint indexCount, jint indexOffset, std::unique_ptr<MeshUniformBuilder> builder, const SkRect& bounds) : mMeshSpec(meshSpec) , mMode(mode) + , mVertexBufferData(std::move(vertexBufferData)) , mVertexCount(vertexCount) , mVertexOffset(vertexOffset) + , mIndexBufferData(std::move(indexBuffer)) , mIndexCount(indexCount) , mIndexOffset(indexOffset) , mBuilder(std::move(builder)) - , mBounds(bounds) { - copyToVector(mVertexBufferData, vertexBuffer, vertexBufferSize); - copyToVector(mIndexBufferData, indexBuffer, indexBufferSize); - } + , mBounds(bounds) {} Mesh(Mesh&&) = default; @@ -180,13 +178,6 @@ public: MeshUniformBuilder* uniformBuilder() { return mBuilder.get(); } private: - void copyToVector(std::vector<uint8_t>& dst, const void* src, size_t srcSize) { - if (src) { - dst.resize(srcSize); - memcpy(dst.data(), src, srcSize); - } - } - sk_sp<SkMeshSpecification> mMeshSpec; int mMode = 0; diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp index b7a15633ff6d..770822a049b7 100644 --- a/libs/hwui/apex/LayoutlibLoader.cpp +++ b/libs/hwui/apex/LayoutlibLoader.cpp @@ -66,6 +66,7 @@ extern int register_android_graphics_fonts_FontFamily(JNIEnv* env); extern int register_android_graphics_text_LineBreaker(JNIEnv* env); extern int register_android_graphics_text_MeasuredText(JNIEnv* env); extern int register_android_graphics_text_TextShaper(JNIEnv* env); +extern int register_android_graphics_text_GraphemeBreak(JNIEnv* env); extern int register_android_util_PathParser(JNIEnv* env); extern int register_android_view_DisplayListCanvas(JNIEnv* env); @@ -125,6 +126,8 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.graphics.text.MeasuredText", REG_JNI(register_android_graphics_text_MeasuredText)}, {"android.graphics.text.TextRunShaper", REG_JNI(register_android_graphics_text_TextShaper)}, + {"android.graphics.text.GraphemeBreak", + REG_JNI(register_android_graphics_text_GraphemeBreak)}, {"android.util.PathParser", REG_JNI(register_android_util_PathParser)}, }; diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index c509ed4dfd21..09ae7e78fe23 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -77,6 +77,7 @@ extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env); extern int register_android_graphics_text_MeasuredText(JNIEnv* env); extern int register_android_graphics_text_LineBreaker(JNIEnv *env); extern int register_android_graphics_text_TextShaper(JNIEnv *env); +extern int register_android_graphics_text_GraphemeBreak(JNIEnv* env); extern int register_android_graphics_MeshSpecification(JNIEnv* env); extern int register_android_graphics_Mesh(JNIEnv* env); @@ -148,6 +149,7 @@ extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env); REG_JNI(register_android_graphics_text_MeasuredText), REG_JNI(register_android_graphics_text_LineBreaker), REG_JNI(register_android_graphics_text_TextShaper), + REG_JNI(register_android_graphics_text_GraphemeBreak), REG_JNI(register_android_graphics_MeshSpecification), REG_JNI(register_android_graphics_Mesh), diff --git a/libs/hwui/jni/BufferUtils.cpp b/libs/hwui/jni/BufferUtils.cpp new file mode 100644 index 000000000000..3eb08d7552da --- /dev/null +++ b/libs/hwui/jni/BufferUtils.cpp @@ -0,0 +1,130 @@ +/* + * 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. + */ +#include "BufferUtils.h" + +#include "graphics_jni_helpers.h" + +static void copyToVector(std::vector<uint8_t>& dst, const void* src, size_t srcSize) { + if (src) { + dst.resize(srcSize); + memcpy(dst.data(), src, srcSize); + } +} + +/** + * This code is taken and modified from com_google_android_gles_jni_GLImpl.cpp to extract data + * from a java.nio.Buffer. + */ +static void* getDirectBufferPointer(JNIEnv* env, jobject buffer) { + if (buffer == nullptr) { + return nullptr; + } + + jint position; + jint limit; + jint elementSizeShift; + jlong pointer; + pointer = jniGetNioBufferFields(env, buffer, &position, &limit, &elementSizeShift); + if (pointer == 0) { + jniThrowException(env, "java/lang/IllegalArgumentException", + "Must use a native order direct Buffer"); + return nullptr; + } + pointer += position << elementSizeShift; + return reinterpret_cast<void*>(pointer); +} + +static void releasePointer(JNIEnv* env, jarray array, void* data, jboolean commit) { + env->ReleasePrimitiveArrayCritical(array, data, commit ? 0 : JNI_ABORT); +} + +static void* getPointer(JNIEnv* env, jobject buffer, jarray* array, jint* remaining, jint* offset) { + jint position; + jint limit; + jint elementSizeShift; + + jlong pointer; + pointer = jniGetNioBufferFields(env, buffer, &position, &limit, &elementSizeShift); + *remaining = (limit - position) << elementSizeShift; + if (pointer != 0L) { + *array = nullptr; + pointer += position << elementSizeShift; + return reinterpret_cast<void*>(pointer); + } + + *array = jniGetNioBufferBaseArray(env, buffer); + *offset = jniGetNioBufferBaseArrayOffset(env, buffer); + return nullptr; +} + +/** + * This is a copy of + * static void android_glBufferData__IILjava_nio_Buffer_2I + * from com_google_android_gles_jni_GLImpl.cpp + */ +static void setIndirectData(JNIEnv* env, size_t size, jobject data_buf, + std::vector<uint8_t>& result) { + jint exception = 0; + const char* exceptionType = nullptr; + const char* exceptionMessage = nullptr; + jarray array = nullptr; + jint bufferOffset = 0; + jint remaining; + void* data = 0; + char* dataBase = nullptr; + + if (data_buf) { + data = getPointer(env, data_buf, (jarray*)&array, &remaining, &bufferOffset); + if (remaining < size) { + exception = 1; + exceptionType = "java/lang/IllegalArgumentException"; + exceptionMessage = "remaining() < size < needed"; + goto exit; + } + } + if (data_buf && data == nullptr) { + dataBase = (char*)env->GetPrimitiveArrayCritical(array, (jboolean*)0); + data = (void*)(dataBase + bufferOffset); + } + + copyToVector(result, data, size); + +exit: + if (array) { + releasePointer(env, array, (void*)dataBase, JNI_FALSE); + } + if (exception) { + jniThrowException(env, exceptionType, exceptionMessage); + } +} + +std::vector<uint8_t> copyJavaNioBufferToVector(JNIEnv* env, jobject buffer, size_t size, + jboolean isDirect) { + std::vector<uint8_t> data; + if (buffer == nullptr) { + jniThrowNullPointerException(env); + } else { + if (isDirect) { + void* directBufferPtr = getDirectBufferPointer(env, buffer); + if (directBufferPtr) { + copyToVector(data, directBufferPtr, size); + } + } else { + setIndirectData(env, size, buffer, data); + } + } + return data; +} diff --git a/libs/hwui/jni/BufferUtils.h b/libs/hwui/jni/BufferUtils.h new file mode 100644 index 000000000000..b43c320b7771 --- /dev/null +++ b/libs/hwui/jni/BufferUtils.h @@ -0,0 +1,32 @@ +/* + * 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. + */ +#ifndef BUFFERUTILS_H_ +#define BUFFERUTILS_H_ + +#include <jni.h> + +#include <vector> + +/** + * Helper method to load a java.nio.Buffer instance into a vector. This handles + * both direct and indirect buffers and promptly releases any critical arrays that + * have been retrieved in order to avoid potential jni exceptions due to interleaved + * jni calls between get/release primitive method invocations. + */ +std::vector<uint8_t> copyJavaNioBufferToVector(JNIEnv* env, jobject buffer, size_t size, + jboolean isDirect); + +#endif // BUFFERUTILS_H_ diff --git a/libs/hwui/jni/android_graphics_Mesh.cpp b/libs/hwui/jni/android_graphics_Mesh.cpp index 04339dc8b9a5..5cb43e54e499 100644 --- a/libs/hwui/jni/android_graphics_Mesh.cpp +++ b/libs/hwui/jni/android_graphics_Mesh.cpp @@ -14,172 +14,18 @@ * limitations under the License. */ -#include <GrDirectContext.h> #include <Mesh.h> #include <SkMesh.h> #include <jni.h> -#include <log/log.h> #include <utility> +#include "BufferUtils.h" #include "GraphicsJNI.h" #include "graphics_jni_helpers.h" #define gIndexByteSize 2 -// A smart pointer that provides read only access to Java.nio.Buffer. This handles both -// direct and indrect buffers, allowing access to the underlying data in both -// situations. If passed a null buffer, we will throw NullPointerException, -// and c_data will return nullptr. -// -// This class draws from com_google_android_gles_jni_GLImpl.cpp for Buffer to void * -// conversion. -class ScopedJavaNioBuffer { -public: - ScopedJavaNioBuffer(JNIEnv* env, jobject buffer, size_t size, jboolean isDirect) - : mEnv(env), mBuffer(buffer) { - if (buffer == nullptr) { - mDataBase = nullptr; - mData = nullptr; - jniThrowNullPointerException(env); - } else { - mArray = (jarray) nullptr; - if (isDirect) { - mData = getDirectBufferPointer(mEnv, mBuffer); - } else { - mData = setIndirectData(size); - } - } - } - - ScopedJavaNioBuffer(ScopedJavaNioBuffer&& rhs) noexcept { *this = std::move(rhs); } - - ~ScopedJavaNioBuffer() { reset(); } - - void reset() { - if (mDataBase) { - releasePointer(mEnv, mArray, mDataBase, JNI_FALSE); - mDataBase = nullptr; - } - } - - ScopedJavaNioBuffer& operator=(ScopedJavaNioBuffer&& rhs) noexcept { - if (this != &rhs) { - reset(); - - mEnv = rhs.mEnv; - mBuffer = rhs.mBuffer; - mDataBase = rhs.mDataBase; - mData = rhs.mData; - mArray = rhs.mArray; - rhs.mEnv = nullptr; - rhs.mData = nullptr; - rhs.mBuffer = nullptr; - rhs.mArray = nullptr; - rhs.mDataBase = nullptr; - } - return *this; - } - - const void* data() const { return mData; } - -private: - /** - * This code is taken and modified from com_google_android_gles_jni_GLImpl.cpp to extract data - * from a java.nio.Buffer. - */ - void* getDirectBufferPointer(JNIEnv* env, jobject buffer) { - if (buffer == nullptr) { - return nullptr; - } - - jint position; - jint limit; - jint elementSizeShift; - jlong pointer; - pointer = jniGetNioBufferFields(env, buffer, &position, &limit, &elementSizeShift); - if (pointer == 0) { - jniThrowException(mEnv, "java/lang/IllegalArgumentException", - "Must use a native order direct Buffer"); - return nullptr; - } - pointer += position << elementSizeShift; - return reinterpret_cast<void*>(pointer); - } - - static void releasePointer(JNIEnv* env, jarray array, void* data, jboolean commit) { - env->ReleasePrimitiveArrayCritical(array, data, commit ? 0 : JNI_ABORT); - } - - static void* getPointer(JNIEnv* env, jobject buffer, jarray* array, jint* remaining, - jint* offset) { - jint position; - jint limit; - jint elementSizeShift; - - jlong pointer; - pointer = jniGetNioBufferFields(env, buffer, &position, &limit, &elementSizeShift); - *remaining = (limit - position) << elementSizeShift; - if (pointer != 0L) { - *array = nullptr; - pointer += position << elementSizeShift; - return reinterpret_cast<void*>(pointer); - } - - *array = jniGetNioBufferBaseArray(env, buffer); - *offset = jniGetNioBufferBaseArrayOffset(env, buffer); - return nullptr; - } - - /** - * This is a copy of - * static void android_glBufferData__IILjava_nio_Buffer_2I - * from com_google_android_gles_jni_GLImpl.cpp - */ - void* setIndirectData(size_t size) { - jint exception; - const char* exceptionType; - const char* exceptionMessage; - jint bufferOffset = (jint)0; - jint remaining; - void* tempData; - - if (mBuffer) { - tempData = - (void*)getPointer(mEnv, mBuffer, (jarray*)&mArray, &remaining, &bufferOffset); - if (remaining < size) { - exception = 1; - exceptionType = "java/lang/IllegalArgumentException"; - exceptionMessage = "remaining() < size < needed"; - goto exit; - } - } - if (mBuffer && tempData == nullptr) { - mDataBase = (char*)mEnv->GetPrimitiveArrayCritical(mArray, (jboolean*)0); - tempData = (void*)(mDataBase + bufferOffset); - } - return tempData; - exit: - if (mArray) { - releasePointer(mEnv, mArray, (void*)(mDataBase), JNI_FALSE); - } - if (exception) { - jniThrowException(mEnv, exceptionType, exceptionMessage); - } - return nullptr; - } - - JNIEnv* mEnv; - - // Java Buffer data - void* mData; - jobject mBuffer; - - // Indirect Buffer Data - jarray mArray; - char* mDataBase; -}; - namespace android { static jlong make(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobject vertexBuffer, @@ -187,9 +33,12 @@ static jlong make(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobject verte jfloat right, jfloat bottom) { auto skMeshSpec = sk_ref_sp(reinterpret_cast<SkMeshSpecification*>(meshSpec)); size_t bufferSize = vertexCount * skMeshSpec->stride(); - auto buff = ScopedJavaNioBuffer(env, vertexBuffer, bufferSize, isDirect); + auto buffer = copyJavaNioBufferToVector(env, vertexBuffer, bufferSize, isDirect); + if (env->ExceptionCheck()) { + return 0; + } auto skRect = SkRect::MakeLTRB(left, top, right, bottom); - auto meshPtr = new Mesh(skMeshSpec, mode, buff.data(), bufferSize, vertexCount, vertexOffset, + auto meshPtr = new Mesh(skMeshSpec, mode, std::move(buffer), vertexCount, vertexOffset, std::make_unique<MeshUniformBuilder>(skMeshSpec), skRect); auto [valid, msg] = meshPtr->validate(); if (!valid) { @@ -205,11 +54,17 @@ static jlong makeIndexed(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobjec auto skMeshSpec = sk_ref_sp(reinterpret_cast<SkMeshSpecification*>(meshSpec)); auto vertexBufferSize = vertexCount * skMeshSpec->stride(); auto indexBufferSize = indexCount * gIndexByteSize; - auto vBuf = ScopedJavaNioBuffer(env, vertexBuffer, vertexBufferSize, isVertexDirect); - auto iBuf = ScopedJavaNioBuffer(env, indexBuffer, indexBufferSize, isIndexDirect); + auto vBuf = copyJavaNioBufferToVector(env, vertexBuffer, vertexBufferSize, isVertexDirect); + if (env->ExceptionCheck()) { + return 0; + } + auto iBuf = copyJavaNioBufferToVector(env, indexBuffer, indexBufferSize, isIndexDirect); + if (env->ExceptionCheck()) { + return 0; + } auto skRect = SkRect::MakeLTRB(left, top, right, bottom); - auto meshPtr = new Mesh(skMeshSpec, mode, vBuf.data(), vertexBufferSize, vertexCount, - vertexOffset, iBuf.data(), indexBufferSize, indexCount, indexOffset, + auto meshPtr = new Mesh(skMeshSpec, mode, std::move(vBuf), vertexCount, vertexOffset, + std::move(iBuf), indexCount, indexOffset, std::make_unique<MeshUniformBuilder>(skMeshSpec), skRect); auto [valid, msg] = meshPtr->validate(); if (!valid) { diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index f17129c8d953..1af60b2f5fae 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -108,8 +108,9 @@ static jlong Font_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, jo std::move(data), std::string_view(fontPath.c_str(), fontPath.size()), fontPtr, fontSize, ttcIndex, builder->axes); if (minikinFont == nullptr) { - jniThrowException(env, "java/lang/IllegalArgumentException", - "Failed to create internal object. maybe invalid font data."); + jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", + "Failed to create internal object. maybe invalid font data. filePath %s", + fontPath.c_str()); return 0; } uint32_t localeListId = minikin::registerLocaleList(langTagStr.c_str()); diff --git a/libs/hwui/jni/text/GraphemeBreak.cpp b/libs/hwui/jni/text/GraphemeBreak.cpp new file mode 100644 index 000000000000..55f03bd9f7b1 --- /dev/null +++ b/libs/hwui/jni/text/GraphemeBreak.cpp @@ -0,0 +1,67 @@ +/* + * 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. + */ + +#undef LOG_TAG +#define LOG_TAG "GraphemeBreaker" + +#include <minikin/GraphemeBreak.h> +#include <nativehelper/ScopedPrimitiveArray.h> + +#include "GraphicsJNI.h" + +namespace android { + +static void nIsGraphemeBreak(JNIEnv* env, jclass, jfloatArray advances, jcharArray text, jint start, + jint end, jbooleanArray isGraphemeBreak) { + if (start > end || env->GetArrayLength(advances) < end || + env->GetArrayLength(isGraphemeBreak) < end - start) { + doThrowAIOOBE(env); + } + + if (start == end) { + return; + } + + ScopedFloatArrayRO advancesArray(env, advances); + ScopedCharArrayRO textArray(env, text); + ScopedBooleanArrayRW isGraphemeBreakArray(env, isGraphemeBreak); + + size_t count = end - start; + for (size_t offset = 0; offset < count; ++offset) { + bool isBreak = minikin::GraphemeBreak::isGraphemeBreak(advancesArray.get(), textArray.get(), + start, end, start + offset); + isGraphemeBreakArray[offset] = isBreak ? JNI_TRUE : JNI_FALSE; + } +} + +static const JNINativeMethod gMethods[] = { + {"nIsGraphemeBreak", + "(" + "[F" // advances + "[C" // text + "I" // start + "I" // end + "[Z" // isGraphemeBreak + ")V", + (void*)nIsGraphemeBreak}, +}; + +int register_android_graphics_text_GraphemeBreak(JNIEnv* env) { + return RegisterMethodsOrDie(env, "android/graphics/text/GraphemeBreak", gMethods, + NELEM(gMethods)); +} + +} // namespace android diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java index f85bdee18967..5f5e214357ea 100644 --- a/media/java/android/media/audiopolicy/AudioMix.java +++ b/media/java/android/media/audiopolicy/AudioMix.java @@ -252,10 +252,10 @@ public class AudioMix { if (o == null || getClass() != o.getClass()) return false; final AudioMix that = (AudioMix) o; - return (this.mRouteFlags == that.mRouteFlags) - && (this.mRule == that.mRule) - && (this.mMixType == that.mMixType) - && (this.mFormat == that.mFormat); + return (mRouteFlags == that.mRouteFlags) + && (mMixType == that.mMixType) + && Objects.equals(mRule, that.mRule) + && Objects.equals(mFormat, that.mFormat); } /** @hide */ diff --git a/media/java/android/media/audiopolicy/AudioPolicyConfig.java b/media/java/android/media/audiopolicy/AudioPolicyConfig.java index 440447e5ec1d..ce9773312a10 100644 --- a/media/java/android/media/audiopolicy/AudioPolicyConfig.java +++ b/media/java/android/media/audiopolicy/AudioPolicyConfig.java @@ -24,6 +24,7 @@ import android.os.Parcelable; import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Objects; @@ -50,7 +51,8 @@ public class AudioPolicyConfig implements Parcelable { mMixes = conf.mMixes; } - AudioPolicyConfig(ArrayList<AudioMix> mixes) { + @VisibleForTesting + public AudioPolicyConfig(ArrayList<AudioMix> mixes) { mMixes = mixes; } diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 9e9012e43111..178a6d97dff8 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -191,20 +191,13 @@ public final class MediaProjection { } else { session = ContentRecordingSession.createTaskSession(launchCookie); } - virtualDisplayConfig.setWindowManagerMirroring(true); + // Pass in the current session details, so they are guaranteed to only be set in WMS + // AFTER a VirtualDisplay is constructed (assuming there are no errors during set-up). + virtualDisplayConfig.setContentRecordingSession(session); + virtualDisplayConfig.setWindowManagerMirroringEnabled(true); final DisplayManager dm = mContext.getSystemService(DisplayManager.class); final VirtualDisplay virtualDisplay = dm.createVirtualDisplay(this, virtualDisplayConfig.build(), callback, handler, windowContext); - if (virtualDisplay == null) { - // Since WM handling a new display and DM creating a new VirtualDisplay is async, - // WM may have tried to start task recording and encountered an error that required - // stopping recording entirely. The VirtualDisplay would then be null when the - // MediaProjection is no longer active. - return null; - } - session.setDisplayId(virtualDisplay.getDisplay().getDisplayId()); - // Successfully set up, so save the current session details. - getProjectionService().setContentRecordingSession(session, mImpl); return virtualDisplay; } catch (RemoteException e) { // Can not capture if WMS is not accessible, so bail out. diff --git a/media/java/android/media/projection/OWNERS b/media/java/android/media/projection/OWNERS index 96532d00831b..2273f816ac60 100644 --- a/media/java/android/media/projection/OWNERS +++ b/media/java/android/media/projection/OWNERS @@ -1,3 +1,4 @@ michaelwr@google.com santoscordon@google.com chaviw@google.com +nmusgrave@google.com diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl index e9aa321018f3..113c85802fa2 100644 --- a/media/java/android/media/tv/ITvInputManager.aidl +++ b/media/java/android/media/tv/ITvInputManager.aidl @@ -118,6 +118,9 @@ interface ITvInputManager { void requestAd(in IBinder sessionToken, in AdRequest request, int userId); void notifyAdBuffer(in IBinder sessionToken, in AdBuffer buffer, int userId); + // For TV Message + void notifyTvMessage(in IBinder sessionToken, in String type, in Bundle data, int userId); + // For TV input hardware binding List<TvInputHardwareInfo> getHardwareList(); ITvInputHardware acquireTvInputHardware(int deviceId, in ITvInputHardwareCallback callback, diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl index 82875e5d72f1..165a9ddc26e9 100644 --- a/media/java/android/media/tv/ITvInputSession.aidl +++ b/media/java/android/media/tv/ITvInputSession.aidl @@ -76,4 +76,7 @@ oneway interface ITvInputSession { // For ad request void requestAd(in AdRequest request); void notifyAdBuffer(in AdBuffer buffer); + + // For TV messages + void notifyTvMessage(in String type, in Bundle data); } diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java index 465b617db1f4..8389706b501c 100644 --- a/media/java/android/media/tv/ITvInputSessionWrapper.java +++ b/media/java/android/media/tv/ITvInputSessionWrapper.java @@ -78,6 +78,7 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand private static final int DO_SELECT_AUDIO_PRESENTATION = 29; private static final int DO_TIME_SHIFT_SET_MODE = 30; private static final int DO_SET_TV_MESSAGE_ENABLED = 31; + private static final int DO_NOTIFY_TV_MESSAGE = 32; private final boolean mIsRecordingSession; private final HandlerCaller mCaller; @@ -277,6 +278,11 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand mTvInputSessionImpl.notifyAdBuffer((AdBuffer) msg.obj); break; } + case DO_NOTIFY_TV_MESSAGE: { + SomeArgs args = (SomeArgs) msg.obj; + mTvInputSessionImpl.onTvMessageReceived((String) args.arg1, (Bundle) args.arg2); + break; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); break; @@ -463,6 +469,11 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_AD_BUFFER, buffer)); } + @Override + public void notifyTvMessage(String type, Bundle data) { + mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_NOTIFY_TV_MESSAGE, type, data)); + } + private final class TvInputEventReceiver extends InputEventReceiver { TvInputEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index 8459538f102d..55a753f663c8 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -144,6 +144,13 @@ public final class TvInputManager { @StringDef({TV_MESSAGE_TYPE_WATERMARK, TV_MESSAGE_TYPE_CLOSED_CAPTION}) public @interface TvMessageType {} + /** + * This constant is used as a {@link Bundle} key for TV messages. The value of the key + * identifies the stream on the TV input source for which the watermark event is relevant to. + */ + public static final String TV_MESSAGE_KEY_STREAM_ID = + "android.media.tv.TvInputManager.stream_id"; + static final int VIDEO_UNAVAILABLE_REASON_START = 0; static final int VIDEO_UNAVAILABLE_REASON_END = 18; @@ -3221,6 +3228,17 @@ public final class TvInputManager { } /** + * Sends TV messages to the service for testing purposes + */ + public void notifyTvMessage(@NonNull @TvMessageType String type, @NonNull Bundle data) { + try { + mService.notifyTvMessage(mToken, type, data, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Starts TV program recording in the current recording session. * * @param programUri The URI for the TV program to record as a hint, built by diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java index 4bc137da6f00..9f40d704f7ac 100644 --- a/media/java/android/media/tv/TvInputService.java +++ b/media/java/android/media/tv/TvInputService.java @@ -1491,7 +1491,18 @@ public abstract class TvInputService extends Service { * {@code false} otherwise. */ public void onSetTvMessageEnabled(@NonNull @TvInputManager.TvMessageType String type, - boolean enabled){ + boolean enabled) { + } + + /** + * Called when a TV message is received + * + * @param type The type of message received, such as + * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK} + * @param data The raw data of the message + */ + public void onTvMessage(@NonNull @TvInputManager.TvMessageType String type, + @NonNull Bundle data) { } /** @@ -2043,6 +2054,10 @@ public abstract class TvInputService extends Service { onAdBuffer(buffer); } + void onTvMessageReceived(String type, Bundle data) { + onTvMessage(type, data); + } + /** * Takes care of dispatching incoming input events and tells whether the event was handled. */ diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java index 3ef61f289e23..5aeed1fbbaf5 100644 --- a/media/java/android/media/tv/TvView.java +++ b/media/java/android/media/tv/TvView.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.content.AttributionSource; import android.content.Context; import android.content.pm.PackageManager; @@ -640,6 +641,20 @@ public class TvView extends ViewGroup { } } + + /** + * Sends TV messages to the session for testing purposes + * + * @hide + */ + @TestApi + public void notifyTvMessage(@TvInputManager.TvMessageType @NonNull String type, + @NonNull Bundle data) { + if (mSession != null) { + mSession.notifyTvMessage(type, data); + } + } + /** * Sets the callback to be invoked when the time shift position is changed. * diff --git a/media/java/android/media/tv/tuner/filter/Filter.java b/media/java/android/media/tv/tuner/filter/Filter.java index 7e9443b467ef..c39a6dbd17da 100644 --- a/media/java/android/media/tv/tuner/filter/Filter.java +++ b/media/java/android/media/tv/tuner/filter/Filter.java @@ -32,6 +32,7 @@ import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.NullPointerException; import java.util.concurrent.Executor; /** @@ -271,7 +272,12 @@ public class Filter implements AutoCloseable { mExecutor.execute(() -> { synchronized (mCallbackLock) { if (mCallback != null) { - mCallback.onFilterStatusChanged(this, status); + try { + mCallback.onFilterStatusChanged(this, status); + } + catch (NullPointerException e) { + Log.d(TAG, "catch exception:" + e); + } } } }); @@ -285,7 +291,12 @@ public class Filter implements AutoCloseable { mExecutor.execute(() -> { synchronized (mCallbackLock) { if (mCallback != null) { - mCallback.onFilterEvent(this, events); + try { + mCallback.onFilterEvent(this, events); + } + catch (NullPointerException e) { + Log.d(TAG, "catch exception:" + e); + } } else { for (FilterEvent event : events) { if (event instanceof MediaEvent) { diff --git a/media/tests/AudioPolicyTest/Android.bp b/media/tests/AudioPolicyTest/Android.bp index 63292ce766f8..4624dfe70756 100644 --- a/media/tests/AudioPolicyTest/Android.bp +++ b/media/tests/AudioPolicyTest/Android.bp @@ -14,6 +14,7 @@ android_test { "androidx.test.ext.junit", "androidx.test.rules", "guava", + "guava-android-testlib", "hamcrest-library", "platform-test-annotations", ], diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixUnitTests.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixUnitTests.java new file mode 100644 index 000000000000..bbca8823dde4 --- /dev/null +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixUnitTests.java @@ -0,0 +1,158 @@ +/* + * 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.audiopolicytest; + +import static android.media.AudioFormat.CHANNEL_OUT_MONO; +import static android.media.AudioFormat.CHANNEL_OUT_STEREO; +import static android.media.AudioFormat.ENCODING_PCM_16BIT; +import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_INJECTOR; +import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_PLAYERS; +import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_AUDIO_SESSION_ID; +import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_UID; + +import android.media.AudioFormat; +import android.media.audiopolicy.AudioMix; +import android.media.audiopolicy.AudioMixingRule; +import android.media.audiopolicy.AudioPolicyConfig; +import android.os.Parcel; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.common.testing.EqualsTester; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests for AudioMix. + * + * Run with "atest AudioMixUnitTests". + */ +@Presubmit +@RunWith(AndroidJUnit4.class) +public class AudioMixUnitTests { + private static final AudioFormat OUTPUT_FORMAT_STEREO_44KHZ_PCM = + new AudioFormat.Builder() + .setSampleRate(44000) + .setChannelMask(CHANNEL_OUT_STEREO) + .setEncoding(ENCODING_PCM_16BIT).build(); + private static final AudioFormat OUTPUT_FORMAT_MONO_16KHZ_PCM = + new AudioFormat.Builder() + .setSampleRate(16000) + .setChannelMask(CHANNEL_OUT_MONO) + .setEncoding(ENCODING_PCM_16BIT).build(); + private static final AudioFormat INPUT_FORMAT_MONO_16KHZ_PCM = + new AudioFormat.Builder() + .setSampleRate(16000) + .setChannelMask(AudioFormat.CHANNEL_IN_MONO) + .setEncoding(ENCODING_PCM_16BIT).build(); + + @Test + public void testEquals() { + final EqualsTester equalsTester = new EqualsTester(); + + // --- Equality group 1 + final AudioMix playbackAudioMixWithSessionId42AndUid123 = + new AudioMix.Builder(new AudioMixingRule.Builder() + .setTargetMixRole(MIX_ROLE_PLAYERS) + .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, 42) + .addMixRule(RULE_MATCH_UID, 123).build()) + .setFormat(OUTPUT_FORMAT_STEREO_44KHZ_PCM) + .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); + final AudioMix playbackAudioMixWithUid123AndSessionId42 = + new AudioMix.Builder(new AudioMixingRule.Builder() + .setTargetMixRole(MIX_ROLE_PLAYERS) + .addMixRule(RULE_MATCH_UID, 123) + .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, 42).build()) + .setFormat(OUTPUT_FORMAT_STEREO_44KHZ_PCM) + .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); + equalsTester.addEqualityGroup( + playbackAudioMixWithSessionId42AndUid123, + playbackAudioMixWithUid123AndSessionId42, + writeToAndFromParcel(playbackAudioMixWithSessionId42AndUid123), + writeToAndFromParcel(playbackAudioMixWithUid123AndSessionId42)); + + // --- Equality group 2 + final AudioMix recordingAudioMixWithSessionId42AndUid123 = + new AudioMix.Builder(new AudioMixingRule.Builder() + .setTargetMixRole(MIX_ROLE_INJECTOR) + .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, 42) + .addMixRule(RULE_MATCH_UID, 123).build()) + .setFormat(INPUT_FORMAT_MONO_16KHZ_PCM) + .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); + final AudioMix recordingAudioMixWithUid123AndSessionId42 = + new AudioMix.Builder(new AudioMixingRule.Builder() + .setTargetMixRole(MIX_ROLE_INJECTOR) + .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, 42) + .addMixRule(RULE_MATCH_UID, 123).build()) + .setFormat(INPUT_FORMAT_MONO_16KHZ_PCM) + .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); + equalsTester.addEqualityGroup(recordingAudioMixWithSessionId42AndUid123, + recordingAudioMixWithUid123AndSessionId42, + writeToAndFromParcel(recordingAudioMixWithSessionId42AndUid123), + writeToAndFromParcel(recordingAudioMixWithUid123AndSessionId42)); + + // --- Equality group 3 + final AudioMix recordingAudioMixWithSessionId42AndUid123Render = + new AudioMix.Builder(new AudioMixingRule.Builder() + .setTargetMixRole(MIX_ROLE_INJECTOR) + .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, 42) + .addMixRule(RULE_MATCH_UID, 123).build()) + .setFormat(INPUT_FORMAT_MONO_16KHZ_PCM) + .setRouteFlags( + AudioMix.ROUTE_FLAG_LOOP_BACK | AudioMix.ROUTE_FLAG_RENDER).build(); + equalsTester.addEqualityGroup(recordingAudioMixWithSessionId42AndUid123Render, + writeToAndFromParcel(recordingAudioMixWithSessionId42AndUid123Render)); + + // --- Equality group 4 + final AudioMix playbackAudioMixWithUid123 = + new AudioMix.Builder(new AudioMixingRule.Builder() + .setTargetMixRole(MIX_ROLE_PLAYERS) + .addMixRule(RULE_MATCH_UID, 123).build()) + .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) + .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); + equalsTester.addEqualityGroup(playbackAudioMixWithUid123, + writeToAndFromParcel(playbackAudioMixWithUid123)); + + // --- Equality group 5 + final AudioMix playbackAudioMixWithUid42 = + new AudioMix.Builder(new AudioMixingRule.Builder() + .setTargetMixRole(MIX_ROLE_PLAYERS) + .addMixRule(RULE_MATCH_UID, 42).build()) + .setFormat(OUTPUT_FORMAT_MONO_16KHZ_PCM) + .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK).build(); + equalsTester.addEqualityGroup(playbackAudioMixWithUid42, + writeToAndFromParcel(playbackAudioMixWithUid42)); + + equalsTester.testEquals(); + } + + private static AudioMix writeToAndFromParcel(AudioMix audioMix) { + AudioPolicyConfig apc = new AudioPolicyConfig(new ArrayList<>(List.of(audioMix))); + Parcel parcel = Parcel.obtain(); + apc.writeToParcel(parcel, /*flags=*/0); + parcel.setDataPosition(0); + AudioMix unmarshalledMix = + AudioPolicyConfig.CREATOR.createFromParcel(parcel).getMixes().get(0); + parcel.recycle(); + return unmarshalledMix; + } +} diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index b8427613d8ce..82e5a7f0682a 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -36,17 +36,19 @@ <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile for singleDevice(type) [CHAR LIMIT=NONE] --> <string name="summary_watch_single_device">The app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to sync info, like the name of someone calling, and access these permissions:</string> - <!-- TODO(b/256140614) To replace all glasses related strings with final versions --> <!-- ================= DEVICE_PROFILE_GLASSES ================= --> + <!-- Title of the device association confirmation dialog for glasses. --> + <string name="confirmation_title_glasses">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to manage <strong><xliff:g id="device_name" example="Glasses">%2$s</xliff:g></strong>?</string> + <!-- The name of the "glasses" device type [CHAR LIMIT=30] --> <string name="profile_name_glasses">glasses</string> <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile (type) [CHAR LIMIT=NONE] --> - <string name="summary_glasses">This app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string> + <string name="summary_glasses_multi_device">This app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string> <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile for singleDevice(type) [CHAR LIMIT=NONE] --> - <string name="summary_glasses_single_device">The app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with these permissions:</string> + <string name="summary_glasses_single_device">This app will be allowed to access these permissions on your phone:</string> <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= --> @@ -81,17 +83,13 @@ <!-- Description of the helper dialog for COMPUTER profile. [CHAR LIMIT=NONE] --> <string name="helper_summary_computer"><xliff:g id="app_name" example="GMS">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="device_type" example="Chromebook">%2$s</xliff:g> to access your phone\u2019s photos, media, and notifications</string> - <!-- TODO(b/256140614) To replace all nearby_device_streaming related strings with final versions --> <!-- ================= DEVICE_PROFILE_NEARBY_DEVICE_STREAMING ================= --> <!-- Confirmation for associating an application with a companion device of NEARBY_DEVICE_STREAMING profile (type) [CHAR LIMIT=NONE] --> - <string name="title_nearby_device_streaming">Allow <strong><xliff:g id="app_name" example="NearbyStreamer">%1$s</xliff:g></strong> to perform this action from your phone</string> - - <!-- Title of the helper dialog for NEARBY_DEVICE_STREAMING profile [CHAR LIMIT=30]. --> - <string name="helper_title_nearby_device_streaming">Cross-device services</string> + <string name="title_nearby_device_streaming">Allow <strong><xliff:g id="device_name" example="NearbyStreamer">%1$s</xliff:g></strong> to take this action?</string> <!-- Description of the helper dialog for NEARBY_DEVICE_STREAMING profile. [CHAR LIMIT=NONE] --> - <string name="helper_summary_nearby_device_streaming"><xliff:g id="app_name" example="NearbyStreamerApp">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="device_type" example="NearbyDevice">%2$s</xliff:g> to stream content to nearby devices</string> + <string name="helper_summary_nearby_device_streaming"><xliff:g id="app_name" example="NearbyStreamerApp">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="device_name" example="NearbyDevice">%2$s</xliff:g> to stream apps and other system features to nearby devices</string> <!-- ================= null profile ================= --> @@ -161,7 +159,7 @@ <string name="permission_app_streaming">Apps</string> <!-- Nearby_device_streaming permission will be granted to the corresponding profile [CHAR LIMIT=45] --> - <string name="permission_nearby_device_streaming">Nearby Device Streaming</string> + <string name="permission_nearby_device_streaming">Streaming</string> <!-- Description of phone permission of corresponding profile [CHAR LIMIT=NONE] --> <string name="permission_phone_summary">Can make and manage phone calls</string> @@ -179,8 +177,7 @@ <string name="permission_calendar_summary">Can access your calendar</string> <!-- Description of microphone permission of corresponding profile [CHAR LIMIT=NONE] --> - <!-- TODO(b/256140614) Need the description for microphone permission --> - <string name="permission_microphone_summary">Can record audio using the microphone</string> + <string name="permission_microphone_summary">Can record audio</string> <!-- Description of nearby devices' permission of corresponding profile [CHAR LIMIT=NONE] --> <string name="permission_nearby_devices_summary">Can find, connect to, and determine the relative position of nearby devices</string> @@ -195,7 +192,6 @@ <string name="permission_storage_summary"></string> <!-- Description of nearby_device_streaming permission of corresponding profile [CHAR LIMIT=NONE] --> - <!-- TODO(b/256140614) Need the description for nearby devices' permission --> - <string name="permission_nearby_device_streaming_summary">Stream content to a nearby device</string> + <string name="permission_nearby_device_streaming_summary">Stream apps and other system features from your phone</string> </resources> diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java index 918f9c681c5a..8316f9df323f 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java @@ -30,6 +30,7 @@ import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService import static com.android.companiondevicemanager.CompanionDeviceResources.MULTI_DEVICES_SUMMARIES; import static com.android.companiondevicemanager.CompanionDeviceResources.PERMISSION_TYPES; import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME; +import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME_MULTI; import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_ICON; import static com.android.companiondevicemanager.CompanionDeviceResources.SUMMARIES; import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_PROFILES; @@ -571,6 +572,7 @@ public class CompanionDeviceActivity extends FragmentActivity implements final String deviceProfile = mRequest.getDeviceProfile(); final String profileName; + final String profileNameMulti; final Spanned summary; final Drawable profileIcon; final int summaryResourceId; @@ -580,6 +582,7 @@ public class CompanionDeviceActivity extends FragmentActivity implements } profileName = getString(PROFILES_NAME.get(deviceProfile)); + profileNameMulti = getString(PROFILES_NAME_MULTI.get(deviceProfile)); profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); summaryResourceId = MULTI_DEVICES_SUMMARIES.get(deviceProfile); @@ -590,7 +593,7 @@ public class CompanionDeviceActivity extends FragmentActivity implements } final Spanned title = getHtmlFromResources( - this, R.string.chooser_title, profileName, appLabel); + this, R.string.chooser_title, profileNameMulti, appLabel); mTitle.setText(title); mSummary.setText(summary); diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java index e3fd3545ca4b..7aed13960b08 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java @@ -59,7 +59,7 @@ final class CompanionDeviceResources { map.put(DEVICE_PROFILE_COMPUTER, R.string.title_computer); map.put(DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, R.string.title_nearby_device_streaming); map.put(DEVICE_PROFILE_WATCH, R.string.confirmation_title); - map.put(DEVICE_PROFILE_GLASSES, R.string.confirmation_title); + map.put(DEVICE_PROFILE_GLASSES, R.string.confirmation_title_glasses); map.put(null, R.string.confirmation_title); TITLES = unmodifiableMap(map); @@ -97,7 +97,7 @@ final class CompanionDeviceResources { static { final Map<String, Integer> map = new ArrayMap<>(); map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch); - map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses); + map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_multi_device); map.put(null, R.string.summary_generic); MULTI_DEVICES_SUMMARIES = unmodifiableMap(map); @@ -113,6 +113,16 @@ final class CompanionDeviceResources { PROFILES_NAME = unmodifiableMap(map); } + static final Map<String, Integer> PROFILES_NAME_MULTI; + static { + final Map<String, Integer> map = new ArrayMap<>(); + map.put(DEVICE_PROFILE_GLASSES, R.string.profile_name_generic); + map.put(DEVICE_PROFILE_WATCH, R.string.profile_name_watch); + map.put(null, R.string.profile_name_generic); + + PROFILES_NAME_MULTI = unmodifiableMap(map); + } + static final Map<String, Integer> PROFILE_ICON; static { final Map<String, Integer> map = new ArrayMap<>(); @@ -133,7 +143,6 @@ final class CompanionDeviceResources { SUPPORTED_PROFILES = unmodifiableSet(set); } - static final Set<String> SUPPORTED_SELF_MANAGED_PROFILES; static { final Set<String> set = new ArraySet<>(); @@ -145,6 +154,4 @@ final class CompanionDeviceResources { SUPPORTED_SELF_MANAGED_PROFILES = unmodifiableSet(set); } - - } diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java index eae14a6ac175..8f32dbb86d04 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionVendorHelperDialogFragment.java @@ -21,6 +21,7 @@ import static android.companion.AssociationRequest.DEVICE_PROFILE_COMPUTER; import static android.companion.AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING; import static com.android.companiondevicemanager.Utils.getApplicationIcon; +import static com.android.companiondevicemanager.Utils.getApplicationLabel; import static com.android.companiondevicemanager.Utils.getHtmlFromResources; import android.annotation.Nullable; @@ -105,9 +106,11 @@ public class CompanionVendorHelperDialogFragment extends DialogFragment { final String packageName = request.getPackageName(); final CharSequence displayName = request.getDisplayName(); final int userId = request.getUserId(); + final CharSequence appLabel; try { applicationIcon = getApplicationIcon(getContext(), packageName); + appLabel = getApplicationLabel(getContext(), packageName, userId); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Package u" + userId + "/" + packageName + " not found."); mListener.onShowHelperDialogFailed(); @@ -119,7 +122,7 @@ public class CompanionVendorHelperDialogFragment extends DialogFragment { mAppIcon = view.findViewById(R.id.app_icon); mButton = view.findViewById(R.id.btn_back); - final Spanned title; + final CharSequence title; final Spanned summary; switch (deviceProfile) { @@ -136,8 +139,7 @@ public class CompanionVendorHelperDialogFragment extends DialogFragment { break; case DEVICE_PROFILE_NEARBY_DEVICE_STREAMING: - title = getHtmlFromResources(getContext(), - R.string.helper_title_nearby_device_streaming); + title = appLabel; summary = getHtmlFromResources( getContext(), R.string.helper_summary_nearby_device_streaming, title, displayName); diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index ee512427fa9c..f655d6b174d5 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -42,7 +42,7 @@ <!-- Title for subsection of "Learn more about passkeys" screen about seamless transition. [CHAR LIMIT=80] --> <string name="seamless_transition_title">Seamless transition</string> <!-- Detail for subsection of "Learn more about passkeys" screen about seamless transition. [CHAR LIMIT=500] --> - <string name="seamless_transition_detail">As we move towards a passwordless future, passwords will still be available alongside passkeys.</string> + <string name="seamless_transition_detail">As we move towards a passwordless future, passwords will still be available alongside passkeys</string> <!-- This appears as the title of the modal bottom sheet which provides all available providers for users to choose. [CHAR LIMIT=200] --> <string name="choose_provider_title">Choose where to save your <xliff:g id="createTypes" example="passkeys">%1$s</xliff:g></string> <!-- This appears as the description body of the modal bottom sheet which provides all available providers for users to choose. [CHAR LIMIT=200] --> @@ -67,9 +67,8 @@ <string name="create_passkey_in_other_device_title">Create passkey in another device?</string> <!-- This appears as the title of the modal bottom sheet for users to confirm whether they should use the selected provider as default or not. [CHAR LIMIT=200] --> <string name="use_provider_for_all_title">Use <xliff:g id="providerInfoDisplayName" example="Google Password Manager">%1$s</xliff:g> for all your sign-ins?</string> - <!-- TODO: Check the wording here. --> - <!-- This appears as the description body of the modal bottom sheet for users to confirm whether they should use the selected provider as default or not. [CHAR LIMIT=200] --> - <string name="use_provider_for_all_description">This password manager will store your passwords and passkeys to help you easily sign in</string> + <!-- This appears as the description body of the modal bottom sheet for users to confirm whether they should use the selected provider as default or not. [CHAR LIMIT=300] --> + <string name="use_provider_for_all_description">This password manager for <xliff:g id="username" example="becket@gmail.com">%1$s</xliff:g> will store your passwords and passkeys to help you easily sign in</string> <!-- This is a label for a button that sets this password manager as the default. [CHAR LIMIT=20] --> <string name="set_as_default">Set as default</string> <!-- This is a label for a button that makes this password manager be used just in this specific case. [CHAR LIMIT=20] --> @@ -111,7 +110,7 @@ <!-- This is a label for a button that takes user to the next screen. [CHAR LIMIT=20] --> <string name="get_dialog_button_label_continue">Continue</string> <!-- Separator for sign-in type and username in a sign-in entry. --> - <string name="get_dialog_sign_in_type_username_separator" translatable="false">" - "</string> + <string name="get_dialog_sign_in_type_username_separator" translatable="false">" • "</string> <!-- This text is followed by a list of one or more options. [CHAR LIMIT=80] --> <string name="get_dialog_title_sign_in_options">Sign-in options</string> <!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=80] --> diff --git a/packages/CredentialManager/res/values/themes.xml b/packages/CredentialManager/res/values/themes.xml index 82aebb7a179d..428c85ac721f 100644 --- a/packages/CredentialManager/res/values/themes.xml +++ b/packages/CredentialManager/res/values/themes.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <style name="Theme.CredentialSelector" parent="@android:style/ThemeOverlay.Material"> + <style name="Theme.CredentialSelector" parent="@*android:style/ThemeOverlay.DeviceDefault.Accent.DayNight"> <item name="android:windowContentOverlay">@null</item> <item name="android:windowNoTitle">true</item> <item name="android:windowBackground">@android:color/transparent</item> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index a2d1e4de8e35..b32fe3fef00d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -42,11 +42,13 @@ import com.android.credentialmanager.createflow.DisabledProviderInfo import com.android.credentialmanager.createflow.EnabledProviderInfo import com.android.credentialmanager.createflow.RequestDisplayInfo import com.android.credentialmanager.getflow.GetCredentialUiState +import com.android.credentialmanager.getflow.findAutoSelectEntry import androidx.credentials.CreateCredentialRequest.DisplayInfo import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.CreatePasswordRequest import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption +import com.android.credentialmanager.common.ProviderActivityState import java.time.Instant @@ -128,10 +130,20 @@ class CredentialManagerRepo( getCredentialUiState = null, ) } - RequestInfo.TYPE_GET -> UiState( - createCredentialUiState = null, - getCredentialUiState = getCredentialInitialUiState(originName)!!, - ) + RequestInfo.TYPE_GET -> { + val getCredentialInitialUiState = getCredentialInitialUiState(originName)!! + val autoSelectEntry = + findAutoSelectEntry(getCredentialInitialUiState.providerDisplayInfo) + UiState( + createCredentialUiState = null, + getCredentialUiState = getCredentialInitialUiState, + selectedEntry = autoSelectEntry, + providerActivityState = + if (autoSelectEntry == null) ProviderActivityState.NOT_APPLICABLE + else ProviderActivityState.READY_TO_LAUNCH, + isAutoSelectFlow = autoSelectEntry != null, + ) + } else -> throw IllegalStateException("Unrecognized request type: ${requestInfo.type}") } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 9b7139ccc26e..e7de8b3f128c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -22,6 +22,7 @@ import android.util.Log import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.result.ActivityResult import androidx.activity.result.IntentSenderRequest +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -36,6 +37,8 @@ import com.android.credentialmanager.createflow.CreateCredentialUiState import com.android.credentialmanager.createflow.CreateScreenState import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.GetScreenState +import com.android.credentialmanager.logging.UIMetrics +import com.android.internal.logging.UiEventLogger.UiEventEnum /** One and only one of create or get state can be active at any given time. */ data class UiState( @@ -44,6 +47,9 @@ data class UiState( val selectedEntry: BaseEntry? = null, val providerActivityState: ProviderActivityState = ProviderActivityState.NOT_APPLICABLE, val dialogState: DialogState = DialogState.ACTIVE, + // True if the UI has one and only one auto selectable entry. Its provider activity will be + // launched immediately, and canceling it will cancel the whole UI flow. + val isAutoSelectFlow: Boolean = false, ) class CredentialSelectorViewModel( @@ -53,6 +59,8 @@ class CredentialSelectorViewModel( var uiState by mutableStateOf(credManRepo.initState()) private set + var uiMetrics: UIMetrics = UIMetrics() + /**************************************************************************/ /***** Shared Callbacks *****/ /**************************************************************************/ @@ -73,6 +81,10 @@ class CredentialSelectorViewModel( fun onNewCredentialManagerRepo(credManRepo: CredentialManagerRepo) { this.credManRepo = credManRepo uiState = credManRepo.initState() + + if (this.credManRepo.requestInfo.token != credManRepo.requestInfo.token) { + this.uiMetrics.resetInstanceId() + } } fun launchProviderUi( @@ -96,13 +108,20 @@ class CredentialSelectorViewModel( val resultCode = providerActivityResult.resultCode val resultData = providerActivityResult.data if (resultCode == Activity.RESULT_CANCELED) { - // Re-display the CredMan UI if the user canceled from the provider UI. - Log.d(Constants.LOG_TAG, "The provider activity was cancelled," + - " re-displaying our UI.") - uiState = uiState.copy( - selectedEntry = null, - providerActivityState = ProviderActivityState.NOT_APPLICABLE, - ) + // Re-display the CredMan UI if the user canceled from the provider UI, or cancel + // the UI if this is the auto select flow. + if (uiState.isAutoSelectFlow) { + Log.d(Constants.LOG_TAG, "The auto selected provider activity was cancelled," + + " ending the credential manager activity.") + onUserCancel() + } else { + Log.d(Constants.LOG_TAG, "The provider activity was cancelled," + + " re-displaying our UI.") + uiState = uiState.copy( + selectedEntry = null, + providerActivityState = ProviderActivityState.NOT_APPLICABLE, + ) + } } else { if (entry != null) { Log.d( @@ -130,6 +149,11 @@ class CredentialSelectorViewModel( onInternalError() } + fun onIllegalUiState(errorMessage: String) { + Log.w(Constants.LOG_TAG, errorMessage) + onInternalError() + } + private fun onInternalError() { Log.w(Constants.LOG_TAG, "UI closed due to illegal internal state") credManRepo.onParsingFailureCancel() @@ -359,4 +383,9 @@ class CredentialSelectorViewModel( onInternalError() } } + + @Composable + fun logUiEvent(uiEventEnum: UiEventEnum) { + this.uiMetrics.log(uiEventEnum, credManRepo.requestInfo.appPackageName) + } }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 43912c26f3c8..b5c8989de618 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -60,8 +60,7 @@ import androidx.credentials.provider.CreateEntry import androidx.credentials.provider.CustomCredentialEntry import androidx.credentials.provider.PasswordCredentialEntry import androidx.credentials.provider.PublicKeyCredentialEntry -import androidx.credentials.provider.RemoteCreateEntry -import androidx.credentials.provider.RemoteCredentialEntry +import androidx.credentials.provider.RemoteEntry import org.json.JSONObject // TODO: remove all !! checks @@ -149,7 +148,10 @@ class GetFlowUtils { icon = providerIcon, displayName = providerLabel, credentialEntryList = getCredentialOptionInfoList( - it.providerFlattenedComponentName, it.credentialEntries, context + providerId = it.providerFlattenedComponentName, + providerLabel = providerLabel, + credentialEntries = it.credentialEntries, + context = context ), authenticationEntryList = getAuthenticationEntryList( it.providerFlattenedComponentName, @@ -202,6 +204,7 @@ class GetFlowUtils { */ private fun getCredentialOptionInfoList( providerId: String, + providerLabel: String, credentialEntries: List<Entry>, context: Context, ): List<CredentialEntryInfo> { @@ -212,6 +215,7 @@ class GetFlowUtils { is PasswordCredentialEntry -> { result.add(CredentialEntryInfo( providerId = providerId, + providerDisplayName = providerLabel, entryKey = it.key, entrySubkey = it.subkey, pendingIntent = credentialEntry.pendingIntent, @@ -221,12 +225,16 @@ class GetFlowUtils { userName = credentialEntry.username.toString(), displayName = credentialEntry.displayName?.toString(), icon = credentialEntry.icon.loadDrawable(context), + shouldTintIcon = credentialEntry.isDefaultIcon ?: false, lastUsedTimeMillis = credentialEntry.lastUsedTime, + isAutoSelectable = credentialEntry.isAutoSelectAllowed && + credentialEntry.autoSelectAllowedFromOption, )) } is PublicKeyCredentialEntry -> { result.add(CredentialEntryInfo( providerId = providerId, + providerDisplayName = providerLabel, entryKey = it.key, entrySubkey = it.subkey, pendingIntent = credentialEntry.pendingIntent, @@ -236,12 +244,16 @@ class GetFlowUtils { userName = credentialEntry.username.toString(), displayName = credentialEntry.displayName?.toString(), icon = credentialEntry.icon.loadDrawable(context), + shouldTintIcon = credentialEntry.isDefaultIcon, lastUsedTimeMillis = credentialEntry.lastUsedTime, + isAutoSelectable = credentialEntry.isAutoSelectAllowed && + credentialEntry.autoSelectAllowedFromOption, )) } is CustomCredentialEntry -> { result.add(CredentialEntryInfo( providerId = providerId, + providerDisplayName = providerLabel, entryKey = it.key, entrySubkey = it.subkey, pendingIntent = credentialEntry.pendingIntent, @@ -251,7 +263,10 @@ class GetFlowUtils { userName = credentialEntry.title.toString(), displayName = credentialEntry.subtitle?.toString(), icon = credentialEntry.icon.loadDrawable(context), + shouldTintIcon = credentialEntry.isDefaultIcon, lastUsedTimeMillis = credentialEntry.lastUsedTime, + isAutoSelectable = credentialEntry.isAutoSelectAllowed && + credentialEntry.autoSelectAllowedFromOption, )) } else -> Log.d( @@ -320,7 +335,7 @@ class GetFlowUtils { if (remoteEntry == null) { return null } - val structuredRemoteEntry = RemoteCredentialEntry.fromSlice(remoteEntry.slice) + val structuredRemoteEntry = RemoteEntry.fromSlice(remoteEntry.slice) ?: return null return RemoteEntryInfo( providerId = providerId, @@ -612,7 +627,7 @@ class CreateFlowUtils { remoteEntry: Entry?, ): RemoteInfo? { return if (remoteEntry != null) { - val structuredRemoteEntry = RemoteCreateEntry.fromSlice(remoteEntry.slice) + val structuredRemoteEntry = RemoteEntry.fromSlice(remoteEntry.slice) ?: return null RemoteInfo( providerId = providerId, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt index 75b12ff756ad..26aadd9d5dee 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt @@ -32,8 +32,7 @@ import androidx.credentials.provider.BeginGetPublicKeyCredentialOption import androidx.credentials.provider.CreateEntry import androidx.credentials.provider.PasswordCredentialEntry import androidx.credentials.provider.PublicKeyCredentialEntry -import androidx.credentials.provider.RemoteCreateEntry -import androidx.credentials.provider.RemoteCredentialEntry +import androidx.credentials.provider.RemoteEntry import java.time.Instant @@ -85,9 +84,7 @@ class GetTestUtils { return Entry( key, subkey, - RemoteCredentialEntry(pendingIntent, BeginGetPublicKeyCredentialOption( - Bundle(), "id", "requestjson" - )).slice + RemoteEntry(pendingIntent).slice ) } @@ -155,21 +152,23 @@ class GetTestUtils { userName: String, userDisplayName: String?, lastUsedTime: Instant?, + isAutoSelectAllowed: Boolean = false, ): Entry { - val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD") - .setPackage("com.androidauth.androidvault") - intent.putExtra("provider_extra_sample", "testprovider") - val pendingIntent = PendingIntent.getActivity( - context, 1, - intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - or PendingIntent.FLAG_ONE_SHOT) + val intent = Intent(Settings.ACTION_SYNC_SETTINGS) + val pendingIntent = + PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val candidateQueryData = Bundle() + candidateQueryData.putBoolean( + "androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED", + isAutoSelectAllowed ) val passkeyEntry = PublicKeyCredentialEntry.Builder( context, userName, pendingIntent, - BeginGetPublicKeyCredentialOption(Bundle(), "id", "requestjson") - ).setDisplayName(userDisplayName).setLastUsedTime(lastUsedTime).build() + BeginGetPublicKeyCredentialOption(candidateQueryData, "id", "requestjson") + ).setDisplayName(userDisplayName).setLastUsedTime(lastUsedTime) + .setAutoSelectAllowed(isAutoSelectAllowed).build() return Entry(key, subkey, passkeyEntry.slice, Intent()) } } @@ -242,7 +241,7 @@ class CreateTestUtils { return Entry( key, subkey, - RemoteCreateEntry(pendingIntent).slice + RemoteEntry(pendingIntent).slice ) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt index 0cb2bb083fb8..edc902e41e9a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt @@ -18,7 +18,6 @@ package com.android.credentialmanager.common.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier @@ -27,6 +26,7 @@ import com.android.credentialmanager.common.material.ModalBottomSheetLayout import com.android.credentialmanager.common.material.ModalBottomSheetValue import com.android.credentialmanager.common.material.rememberModalBottomSheetState import com.android.credentialmanager.ui.theme.EntryShape +import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme /** Draws a modal bottom sheet with the same styles and effects shared by various flows. */ @Composable @@ -39,9 +39,7 @@ fun ModalBottomSheet( skipHalfExpanded = true ) ModalBottomSheetLayout( - sheetBackgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - ElevationTokens.Level1 - ), + sheetBackgroundColor = LocalAndroidColorScheme.current.colorSurfaceBright, modifier = Modifier.background(Color.Transparent), sheetState = state, sheetContent = sheetContent, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt index c535268b8989..3976f9a305ab 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt @@ -25,13 +25,13 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.android.credentialmanager.ui.theme.Shapes +import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme /** * Container card for the whole sheet. @@ -50,9 +50,7 @@ fun SheetContainerCard( modifier = modifier.fillMaxWidth().wrapContentHeight(), border = null, colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - ElevationTokens.Level1 - ), + containerColor = LocalAndroidColorScheme.current.colorSurfaceBright, ), ) { if (topAppBar != null) { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt index 1e2a280cb693..192354258fb2 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -53,6 +53,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import com.android.credentialmanager.R import com.android.credentialmanager.ui.theme.EntryShape +import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme import com.android.credentialmanager.ui.theme.Shapes @Composable @@ -73,6 +74,7 @@ fun Entry( passwordValue: String? = null, /** If true, draws a trailing lock icon. */ isLockedAuthEntry: Boolean = false, + enforceOneLine: Boolean = false, ) { val iconPadding = Modifier.wrapContentSize().padding( // Horizontal padding should be 16dp, but the suggestion chip itself @@ -92,12 +94,18 @@ fun Entry( // has 8dp horizontal elements padding horizontal = 8.dp, vertical = 16.dp, ), + // Make sure the trailing icon and text column are centered vertically. verticalAlignment = Alignment.CenterVertically, ) { - Column(modifier = Modifier.wrapContentSize()) { - SmallTitleText(entryHeadlineText) + // Apply weight so that the trailing icon can always show. + Column(modifier = Modifier.wrapContentHeight().fillMaxWidth().weight(1f)) { + SmallTitleText(text = entryHeadlineText, enforceOneLine = enforceOneLine) if (passwordValue != null) { - Row(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { val visualTransformation = remember { PasswordVisualTransformation() } val originalPassword by remember { mutableStateOf(passwordValue) @@ -109,9 +117,14 @@ fun Entry( ).text.text ) } - BodySmallText(displayedPassword.value) + BodySmallText( + text = displayedPassword.value, + // Apply weight to allow visibility button to render first so that + // it doesn't get squeezed out by a super long password. + modifier = Modifier.wrapContentSize().weight(1f, fill = false), + ) ToggleVisibilityButton( - modifier = Modifier.padding(start = 5.dp).size(24.dp), + modifier = Modifier.padding(start = 12.dp).size(24.dp), onToggle = { if (it) { displayedPassword.value = originalPassword @@ -124,14 +137,14 @@ fun Entry( ) } } else if (entrySecondLineText != null) { - BodySmallText(entrySecondLineText) + BodySmallText(text = entrySecondLineText, enforceOneLine = enforceOneLine) } if (entryThirdLineText != null) { - BodySmallText(entryThirdLineText) + BodySmallText(text = entryThirdLineText, enforceOneLine = enforceOneLine) } } if (isLockedAuthEntry) { - Box(modifier = Modifier.wrapContentSize()) { + Box(modifier = Modifier.wrapContentSize().padding(start = 16.dp)) { Icon( imageVector = Icons.Outlined.Lock, // Decorative purpose only. @@ -198,9 +211,7 @@ fun Entry( }, border = null, colors = SuggestionChipDefaults.suggestionChipColors( - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - ElevationTokens.Level3 - ), + containerColor = LocalAndroidColorScheme.current.colorSurfaceContainerHigh, // TODO: remove? labelColor = MaterialTheme.colorScheme.onSurfaceVariant, iconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, @@ -323,7 +334,7 @@ fun MoreOptionTopAppBar( contentDescription = stringResource( R.string.accessibility_back_arrow_button ), - modifier = Modifier.size(16.dp), + modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt index 2f145847cbbe..a6195237d139 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt @@ -16,11 +16,11 @@ package com.android.credentialmanager.common.ui -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.android.compose.SystemUiController import com.android.credentialmanager.common.material.ModalBottomSheetDefaults +import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme @Composable fun setTransparentSystemBarsColor(sysUiController: SystemUiController) { @@ -34,9 +34,7 @@ fun setBottomSheetSystemBarsColor(sysUiController: SystemUiController) { darkIcons = false ) sysUiController.setNavigationBarColor( - color = MaterialTheme.colorScheme.surfaceColorAtElevation( - ElevationTokens.Level1 - ), + color = LocalAndroidColorScheme.current.colorSurfaceBright, darkIcons = false ) }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt index 8af729ecdc25..22871bcbe767 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow /** * The headline for a screen. E.g. "Create a passkey for X", "Choose a saved sign-in for X". @@ -57,12 +58,14 @@ fun BodyMediumText(text: String, modifier: Modifier = Modifier) { * Body-small typography; on-surface-variant color. */ @Composable -fun BodySmallText(text: String, modifier: Modifier = Modifier) { +fun BodySmallText(text: String, modifier: Modifier = Modifier, enforceOneLine: Boolean = false) { Text( modifier = modifier.wrapContentSize(), text = text, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, + overflow = TextOverflow.Ellipsis, + maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE ) } @@ -83,12 +86,14 @@ fun LargeTitleText(text: String, modifier: Modifier = Modifier) { * Title-small typography; on-surface color. */ @Composable -fun SmallTitleText(text: String, modifier: Modifier = Modifier) { +fun SmallTitleText(text: String, modifier: Modifier = Modifier, enforceOneLine: Boolean = false) { Text( modifier = modifier.wrapContentSize(), text = text, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleSmall, + overflow = TextOverflow.Ellipsis, + maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 5b175bf07abe..524077715b10 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -1,4 +1,18 @@ -@file:OptIn(ExperimentalMaterial3Api::class) +/* + * 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.createflow @@ -15,7 +29,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Divider -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.NewReleases @@ -54,6 +67,8 @@ import com.android.credentialmanager.common.ui.SheetContainerCard import com.android.credentialmanager.common.ui.PasskeyBenefitRow import com.android.credentialmanager.common.ui.HeadlineText import com.android.credentialmanager.common.ui.setBottomSheetSystemBarsColor +import com.android.credentialmanager.logging.CreateCredentialEvent +import com.android.internal.logging.UiEventLogger.UiEventEnum @Composable fun CreateCredentialScreen( @@ -72,72 +87,93 @@ fun CreateCredentialScreen( ProviderActivityState.NOT_APPLICABLE -> { when (createCredentialUiState.currentScreenState) { CreateScreenState.PASSKEY_INTRO -> PasskeyIntroCard( - onConfirm = viewModel::createFlowOnConfirmIntro, - onLearnMore = viewModel::createFlowOnLearnMore, + onConfirm = viewModel::createFlowOnConfirmIntro, + onLearnMore = viewModel::createFlowOnLearnMore, + onLog = { viewModel.logUiEvent(it) }, ) CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard( - requestDisplayInfo = createCredentialUiState.requestDisplayInfo, - disabledProviderList = createCredentialUiState.disabledProviders, - sortedCreateOptionsPairs = - createCredentialUiState.sortedCreateOptionsPairs, - hasRemoteEntry = createCredentialUiState.remoteEntry != null, - onOptionSelected = - viewModel::createFlowOnEntrySelectedFromFirstUseScreen, - onDisabledProvidersSelected = - viewModel::createFlowOnDisabledProvidersSelected, - onMoreOptionsSelected = - viewModel::createFlowOnMoreOptionsSelectedOnProviderSelection, + requestDisplayInfo = createCredentialUiState.requestDisplayInfo, + disabledProviderList = createCredentialUiState + .disabledProviders, + sortedCreateOptionsPairs = + createCredentialUiState.sortedCreateOptionsPairs, + hasRemoteEntry = createCredentialUiState.remoteEntry != null, + onOptionSelected = + viewModel::createFlowOnEntrySelectedFromFirstUseScreen, + onDisabledProvidersSelected = + viewModel::createFlowOnDisabledProvidersSelected, + onMoreOptionsSelected = + viewModel::createFlowOnMoreOptionsSelectedOnProviderSelection, + onLog = { viewModel.logUiEvent(it) }, ) CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard( - requestDisplayInfo = createCredentialUiState.requestDisplayInfo, - enabledProviderList = createCredentialUiState.enabledProviders, - providerInfo = createCredentialUiState.activeEntry?.activeProvider!!, - hasDefaultProvider = createCredentialUiState.hasDefaultProvider, - createOptionInfo = - createCredentialUiState.activeEntry.activeEntryInfo - as CreateOptionInfo, - onOptionSelected = viewModel::createFlowOnEntrySelected, - onConfirm = viewModel::createFlowOnConfirmEntrySelected, - onMoreOptionsSelected = - viewModel::createFlowOnMoreOptionsSelectedOnCreationSelection, + requestDisplayInfo = createCredentialUiState.requestDisplayInfo, + enabledProviderList = createCredentialUiState.enabledProviders, + providerInfo = createCredentialUiState + .activeEntry?.activeProvider!!, + hasDefaultProvider = createCredentialUiState.hasDefaultProvider, + createOptionInfo = + createCredentialUiState.activeEntry.activeEntryInfo + as CreateOptionInfo, + onOptionSelected = viewModel::createFlowOnEntrySelected, + onConfirm = viewModel::createFlowOnConfirmEntrySelected, + onMoreOptionsSelected = + viewModel::createFlowOnMoreOptionsSelectedOnCreationSelection, + onLog = { viewModel.logUiEvent(it) }, ) CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( - requestDisplayInfo = createCredentialUiState.requestDisplayInfo, - enabledProviderList = createCredentialUiState.enabledProviders, - disabledProviderList = createCredentialUiState.disabledProviders, - sortedCreateOptionsPairs = - createCredentialUiState.sortedCreateOptionsPairs, - hasDefaultProvider = createCredentialUiState.hasDefaultProvider, - isFromProviderSelection = - createCredentialUiState.isFromProviderSelection!!, - onBackProviderSelectionButtonSelected = - viewModel::createFlowOnBackProviderSelectionButtonSelected, - onBackCreationSelectionButtonSelected = - viewModel::createFlowOnBackCreationSelectionButtonSelected, - onOptionSelected = - viewModel::createFlowOnEntrySelectedFromMoreOptionScreen, - onDisabledProvidersSelected = - viewModel::createFlowOnDisabledProvidersSelected, - onRemoteEntrySelected = viewModel::createFlowOnEntrySelected, - ) - CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard( - providerInfo = createCredentialUiState.activeEntry?.activeProvider!!, - onChangeDefaultSelected = viewModel::createFlowOnChangeDefaultSelected, - onUseOnceSelected = viewModel::createFlowOnUseOnceSelected, + requestDisplayInfo = createCredentialUiState.requestDisplayInfo, + enabledProviderList = createCredentialUiState.enabledProviders, + disabledProviderList = createCredentialUiState + .disabledProviders, + sortedCreateOptionsPairs = + createCredentialUiState.sortedCreateOptionsPairs, + hasDefaultProvider = createCredentialUiState.hasDefaultProvider, + isFromProviderSelection = + createCredentialUiState.isFromProviderSelection!!, + onBackProviderSelectionButtonSelected = + viewModel::createFlowOnBackProviderSelectionButtonSelected, + onBackCreationSelectionButtonSelected = + viewModel::createFlowOnBackCreationSelectionButtonSelected, + onOptionSelected = + viewModel::createFlowOnEntrySelectedFromMoreOptionScreen, + onDisabledProvidersSelected = + viewModel::createFlowOnDisabledProvidersSelected, + onRemoteEntrySelected = viewModel::createFlowOnEntrySelected, + onLog = { viewModel.logUiEvent(it) }, ) + CreateScreenState.MORE_OPTIONS_ROW_INTRO -> { + if (createCredentialUiState.activeEntry == null) { + viewModel.onIllegalUiState("Expect active entry to be non-null" + + " upon default provider dialog.") + } else { + MoreOptionsRowIntroCard( + selectedEntry = createCredentialUiState.activeEntry, + onIllegalScreenState = viewModel::onIllegalUiState, + onChangeDefaultSelected = + viewModel::createFlowOnChangeDefaultSelected, + onUseOnceSelected = viewModel::createFlowOnUseOnceSelected, + onLog = { viewModel.logUiEvent(it) }, + ) + } + } CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard( - requestDisplayInfo = createCredentialUiState.requestDisplayInfo, - activeRemoteEntry = - createCredentialUiState.activeEntry?.activeEntryInfo!!, - onOptionSelected = viewModel::createFlowOnEntrySelected, - onConfirm = viewModel::createFlowOnConfirmEntrySelected, + requestDisplayInfo = createCredentialUiState.requestDisplayInfo, + activeRemoteEntry = + createCredentialUiState.activeEntry?.activeEntryInfo!!, + onOptionSelected = viewModel::createFlowOnEntrySelected, + onConfirm = viewModel::createFlowOnConfirmEntrySelected, + onLog = { viewModel.logUiEvent(it) }, ) - CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO -> - MoreAboutPasskeysIntroCard( + CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO -> MoreAboutPasskeysIntroCard( onBackPasskeyIntroButtonSelected = viewModel::createFlowOnBackPasskeyIntroButtonSelected, - ) + onLog = { viewModel.logUiEvent(it) }, + ) } + viewModel.uiMetrics.log( + CreateCredentialEvent + .CREDMAN_CREATE_CRED_PROVIDER_ACTIVITY_NOT_APPLICABLE) } ProviderActivityState.READY_TO_LAUNCH -> { // Launch only once per providerActivityState change so that the provider @@ -145,9 +181,14 @@ fun CreateCredentialScreen( LaunchedEffect(viewModel.uiState.providerActivityState) { viewModel.launchProviderUi(providerActivityLauncher) } + viewModel.uiMetrics.log( + CreateCredentialEvent + .CREDMAN_CREATE_CRED_PROVIDER_ACTIVITY_READY_TO_LAUNCH) } ProviderActivityState.PENDING -> { // Hide our content when the provider activity is active. + viewModel.uiMetrics.log( + CreateCredentialEvent.CREDMAN_CREATE_CRED_PROVIDER_ACTIVITY_PENDING) } } }, @@ -159,6 +200,7 @@ fun CreateCredentialScreen( fun PasskeyIntroCard( onConfirm: () -> Unit, onLearnMore: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { SheetContainerCard { item { @@ -223,6 +265,7 @@ fun PasskeyIntroCard( ) } } + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_PASSKEY_INTRO) } @Composable @@ -234,6 +277,7 @@ fun ProviderSelectionCard( onOptionSelected: (ActiveEntry) -> Unit, onDisabledProvidersSelected: () -> Unit, onMoreOptionsSelected: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { SheetContainerCard { item { HeadlineIcon(bitmap = requestDisplayInfo.typeIcon.toBitmap().asImageBitmap()) } @@ -297,21 +341,23 @@ fun ProviderSelectionCard( } } } + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_PROVIDER_SELECTION) } @Composable fun MoreOptionsSelectionCard( - requestDisplayInfo: RequestDisplayInfo, - enabledProviderList: List<EnabledProviderInfo>, - disabledProviderList: List<DisabledProviderInfo>?, - sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, - hasDefaultProvider: Boolean, - isFromProviderSelection: Boolean, - onBackProviderSelectionButtonSelected: () -> Unit, - onBackCreationSelectionButtonSelected: () -> Unit, - onOptionSelected: (ActiveEntry) -> Unit, - onDisabledProvidersSelected: () -> Unit, - onRemoteEntrySelected: (BaseEntry) -> Unit, + requestDisplayInfo: RequestDisplayInfo, + enabledProviderList: List<EnabledProviderInfo>, + disabledProviderList: List<DisabledProviderInfo>?, + sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, + hasDefaultProvider: Boolean, + isFromProviderSelection: Boolean, + onBackProviderSelectionButtonSelected: () -> Unit, + onBackCreationSelectionButtonSelected: () -> Unit, + onOptionSelected: (ActiveEntry) -> Unit, + onDisabledProvidersSelected: () -> Unit, + onRemoteEntrySelected: (BaseEntry) -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { SheetContainerCard(topAppBar = { MoreOptionTopAppBar( @@ -331,7 +377,7 @@ fun MoreOptionsSelectionCard( ) }) { item { Divider(thickness = 8.dp, color = Color.Transparent) } // Top app bar has a 8dp - // bottom padding already + // bottom padding already item { CredentialContainerCard { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { @@ -372,27 +418,37 @@ fun MoreOptionsSelectionCard( } } } + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_MORE_OPTIONS_SELECTION) } @Composable fun MoreOptionsRowIntroCard( - providerInfo: EnabledProviderInfo, - onChangeDefaultSelected: () -> Unit, - onUseOnceSelected: () -> Unit, + selectedEntry: ActiveEntry, + onIllegalScreenState: (String) -> Unit, + onChangeDefaultSelected: () -> Unit, + onUseOnceSelected: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { + val entryInfo = selectedEntry.activeEntryInfo + if (entryInfo !is CreateOptionInfo) { + onIllegalScreenState("Encountered unexpected type of entry during the default provider" + + " dialog: ${entryInfo::class}") + return + } SheetContainerCard { item { HeadlineIcon(imageVector = Icons.Outlined.NewReleases) } item { Divider(thickness = 24.dp, color = Color.Transparent) } item { HeadlineText( text = stringResource( - R.string.use_provider_for_all_title, - providerInfo.displayName - ) + R.string.use_provider_for_all_title, selectedEntry.activeProvider.displayName) ) } item { Divider(thickness = 24.dp, color = Color.Transparent) } - item { BodyMediumText(text = stringResource(R.string.use_provider_for_all_description)) } + item { + BodyMediumText(text = stringResource( + R.string.use_provider_for_all_description, entryInfo.userProviderDisplayName)) + } item { CtaButtonRow( leftButton = { @@ -410,18 +466,20 @@ fun MoreOptionsRowIntroCard( ) } } + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_MORE_OPTIONS_ROW_INTRO) } @Composable fun CreationSelectionCard( - requestDisplayInfo: RequestDisplayInfo, - enabledProviderList: List<EnabledProviderInfo>, - providerInfo: EnabledProviderInfo, - createOptionInfo: CreateOptionInfo, - onOptionSelected: (BaseEntry) -> Unit, - onConfirm: () -> Unit, - onMoreOptionsSelected: () -> Unit, - hasDefaultProvider: Boolean, + requestDisplayInfo: RequestDisplayInfo, + enabledProviderList: List<EnabledProviderInfo>, + providerInfo: EnabledProviderInfo, + createOptionInfo: CreateOptionInfo, + onOptionSelected: (BaseEntry) -> Unit, + onConfirm: () -> Unit, + onMoreOptionsSelected: () -> Unit, + hasDefaultProvider: Boolean, + onLog: @Composable (UiEventEnum) -> Unit, ) { SheetContainerCard { item { @@ -507,14 +565,16 @@ fun CreationSelectionCard( item { BodySmallText(text = createOptionInfo.footerDescription) } } } + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_CREATION_OPTION_SELECTION) } @Composable fun ExternalOnlySelectionCard( - requestDisplayInfo: RequestDisplayInfo, - activeRemoteEntry: BaseEntry, - onOptionSelected: (BaseEntry) -> Unit, - onConfirm: () -> Unit, + requestDisplayInfo: RequestDisplayInfo, + activeRemoteEntry: BaseEntry, + onOptionSelected: (BaseEntry) -> Unit, + onConfirm: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { SheetContainerCard { item { HeadlineIcon(imageVector = Icons.Outlined.QrCodeScanner) } @@ -542,11 +602,13 @@ fun ExternalOnlySelectionCard( ) } } + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_EXTERNAL_ONLY_SELECTION) } @Composable fun MoreAboutPasskeysIntroCard( onBackPasskeyIntroButtonSelected: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { SheetContainerCard( topAppBar = { @@ -582,6 +644,7 @@ fun MoreAboutPasskeysIntroCard( BodyMediumText(text = stringResource(R.string.seamless_transition_detail)) } } + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO) } @Composable @@ -620,6 +683,7 @@ fun PrimaryCreateOptionRow( // This subtitle would never be null for create password requestDisplayInfo.subtitle ?: "" else null, + enforceOneLine = true, ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 192fa15714c6..4332fb34ce79 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -69,7 +69,7 @@ class CreateOptionInfo( entrySubkey: String, pendingIntent: PendingIntent?, fillInIntent: Intent?, - val userProviderDisplayName: String?, + val userProviderDisplayName: String, val profileIcon: Drawable?, val passwordCount: Int?, val passkeyCount: Int?, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 9a826f20fe03..a9f994db430a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -182,12 +182,14 @@ fun PrimarySelectionCard( CredentialEntryRow( credentialEntryInfo = it.sortedCredentialEntryList.first(), onEntrySelected = onEntrySelected, + enforceOneLine = true, ) } authenticationEntryList.forEach { AuthenticationEntryRow( authenticationEntryInfo = it, onEntrySelected = onEntrySelected, + enforceOneLine = true, ) } } else if (usernameForCredentialSize < 4) { @@ -195,12 +197,14 @@ fun PrimarySelectionCard( CredentialEntryRow( credentialEntryInfo = it.sortedCredentialEntryList.first(), onEntrySelected = onEntrySelected, + enforceOneLine = true, ) } authenticationEntryList.take(4 - usernameForCredentialSize).forEach { AuthenticationEntryRow( authenticationEntryInfo = it, onEntrySelected = onEntrySelected, + enforceOneLine = true, ) } } else { @@ -208,6 +212,7 @@ fun PrimarySelectionCard( CredentialEntryRow( credentialEntryInfo = it.sortedCredentialEntryList.first(), onEntrySelected = onEntrySelected, + enforceOneLine = true, ) } } @@ -290,13 +295,6 @@ fun AllSignInOptionCard( ) } } - item { - Divider( - thickness = 1.dp, - color = Color.LightGray, - modifier = Modifier.padding(top = 16.dp) - ) - } // Manage sign-ins (action chips) item { ActionChips( @@ -402,10 +400,12 @@ fun PerUserNameCredentials( fun CredentialEntryRow( credentialEntryInfo: CredentialEntryInfo, onEntrySelected: (BaseEntry) -> Unit, + enforceOneLine: Boolean = false, ) { Entry( onClick = { onEntrySelected(credentialEntryInfo) }, iconImageBitmap = credentialEntryInfo.icon?.toBitmap()?.asImageBitmap(), + shouldApplyIconImageBitmapTint = credentialEntryInfo.shouldTintIcon, // Fall back to iconPainter if iconImageBitmap isn't available iconPainter = if (credentialEntryInfo.icon == null) painterResource(R.drawable.ic_other_sign_in_24) @@ -415,15 +415,17 @@ fun CredentialEntryRow( credentialEntryInfo.credentialType == CredentialType.PASSWORD) { "••••••••••••" } else { - if (TextUtils.isEmpty(credentialEntryInfo.displayName)) - credentialEntryInfo.credentialTypeDisplayName - else - credentialEntryInfo.credentialTypeDisplayName + - stringResource( - R.string.get_dialog_sign_in_type_username_separator - ) + - credentialEntryInfo.displayName + val itemsToDisplay = listOf( + credentialEntryInfo.displayName, + credentialEntryInfo.credentialTypeDisplayName, + credentialEntryInfo.providerDisplayName + ).filterNot(TextUtils::isEmpty) + if (itemsToDisplay.isEmpty()) null + else itemsToDisplay.joinToString( + separator = stringResource(R.string.get_dialog_sign_in_type_username_separator) + ) }, + enforceOneLine = enforceOneLine, ) } @@ -431,6 +433,7 @@ fun CredentialEntryRow( fun AuthenticationEntryRow( authenticationEntryInfo: AuthenticationEntryInfo, onEntrySelected: (BaseEntry) -> Unit, + enforceOneLine: Boolean = false, ) { Entry( onClick = { onEntrySelected(authenticationEntryInfo) }, @@ -442,6 +445,7 @@ fun AuthenticationEntryRow( else R.string.locked_credential_entry_label_subtext_tap_to_unlock ), isLockedAuthEntry = !authenticationEntryInfo.isUnlockedAndEmpty, + enforceOneLine = enforceOneLine, ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index 9727d3f39c4a..263a632ef5ee 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -41,6 +41,23 @@ internal fun hasContentToDisplay(state: GetCredentialUiState): Boolean { !state.requestDisplayInfo.preferImmediatelyAvailableCredentials) } +internal fun findAutoSelectEntry(providerDisplayInfo: ProviderDisplayInfo): CredentialEntryInfo? { + if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) { + return null + } + if (providerDisplayInfo.sortedUserNameToCredentialEntryList.size == 1) { + val entryList = providerDisplayInfo.sortedUserNameToCredentialEntryList.firstOrNull() + ?: return null + if (entryList.sortedCredentialEntryList.size == 1) { + val entry = entryList.sortedCredentialEntryList.firstOrNull() ?: return null + if (entry.isAutoSelectable) { + return entry + } + } + } + return null +} + data class ProviderInfo( /** * Unique id (component name) of this provider. @@ -77,10 +94,13 @@ class CredentialEntryInfo( val credentialType: CredentialType, /** Localized type value of this credential used for display purpose. */ val credentialTypeDisplayName: String, + val providerDisplayName: String, val userName: String, val displayName: String?, val icon: Drawable?, + val shouldTintIcon: Boolean, val lastUsedTimeMillis: Instant?, + val isAutoSelectable: Boolean, ) : BaseEntry( providerId, entryKey, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt new file mode 100644 index 000000000000..daa42be020ce --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt @@ -0,0 +1,60 @@ +/* + * 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.credentialmanager.logging + +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger + +enum class CreateCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum { + + @UiEvent(doc = "The create credential bottomsheet became visible on the screen.") + CREDMAN_CREATE_CRED_BOTTOMSHEET(1318), + + @UiEvent(doc = "The provider activity is launched on the screen.") + CREDMAN_CREATE_CRED_PROVIDER_ACTIVITY_READY_TO_LAUNCH(1319), + + @UiEvent(doc = "The provider activity is launched and we are waiting for its result. " + + "Contents Hidden.") + CREDMAN_CREATE_CRED_PROVIDER_ACTIVITY_PENDING(1320), + + @UiEvent(doc = "The provider activity is not active or ready launched on the screen.") + CREDMAN_CREATE_CRED_PROVIDER_ACTIVITY_NOT_APPLICABLE(1321), + + @UiEvent(doc = "The passkey introduction card is visible on screen.") + CREDMAN_CREATE_CRED_PASSKEY_INTRO(1322), + + @UiEvent(doc = "The provider selection card is visible on screen.") + CREDMAN_CREATE_CRED_PROVIDER_SELECTION(1323), + + @UiEvent(doc = "The creation option selection card is visible on screen.") + CREDMAN_CREATE_CRED_CREATION_OPTION_SELECTION(1324), + + @UiEvent(doc = "The more option selection card is visible on screen.") + CREDMAN_CREATE_CRED_MORE_OPTIONS_SELECTION(1325), + + @UiEvent(doc = "The more options row intro card is visible on screen.") + CREDMAN_CREATE_CRED_MORE_OPTIONS_ROW_INTRO(1326), + + @UiEvent(doc = "The external only selection card is visible on screen.") + CREDMAN_CREATE_CRED_EXTERNAL_ONLY_SELECTION(1327), + + @UiEvent(doc = "The more about passkeys intro card is visible on screen.") + CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO(1328); + + override fun getId(): Int { + return this.id + } +}
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/logging/UIMetrics.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/UIMetrics.kt new file mode 100644 index 000000000000..4351e8406790 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/UIMetrics.kt @@ -0,0 +1,59 @@ +/* + * 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.credentialmanager.logging + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.android.internal.logging.InstanceId +import com.android.internal.logging.InstanceIdSequence +import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.UiEventLoggerImpl + +class UIMetrics() { + private val INSTANCE_ID_MAX = 1 shl 20 + private val mUiEventLogger: UiEventLogger = UiEventLoggerImpl() + val mInstanceIdSequence: InstanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX) + + var mInstanceId: InstanceId = mInstanceIdSequence.newInstanceId() + + fun resetInstanceId() { + this.mInstanceId = mInstanceIdSequence.newInstanceId() + } + + @Composable + fun log(event: UiEventLogger.UiEventEnum) { + val instanceId: InstanceId = mInstanceId + LaunchedEffect(true) { + mUiEventLogger.log(event, instanceId) + } + } + + @Composable + fun log(event: UiEventLogger.UiEventEnum, packageName: String) { + val instanceId: InstanceId = mInstanceId + LaunchedEffect(true) { + mUiEventLogger.logWithInstanceId(event, /*uid=*/0, packageName, instanceId) + } + } + + @Composable + fun log(event: UiEventLogger.UiEventEnum, instanceId: InstanceId, packageName: String) { + LaunchedEffect(true) { + mUiEventLogger.logWithInstanceId(event, /*uid=*/0, packageName, instanceId) + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt index 120e4938c322..8928e1869838 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt @@ -40,32 +40,8 @@ val LocalAndroidColorScheme = * most of the colors in this class will be removed in favor of their M3 counterpart. */ class AndroidColorScheme internal constructor(context: Context) { - val colorPrimary = getColor(context, R.attr.colorPrimary) - val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark) - val colorAccent = getColor(context, R.attr.colorAccent) - val colorAccentPrimary = getColor(context, R.attr.colorAccentPrimary) - val colorAccentSecondary = getColor(context, R.attr.colorAccentSecondary) - val colorAccentTertiary = getColor(context, R.attr.colorAccentTertiary) - val colorAccentPrimaryVariant = getColor(context, R.attr.colorAccentPrimaryVariant) - val colorAccentSecondaryVariant = getColor(context, R.attr.colorAccentSecondaryVariant) - val colorAccentTertiaryVariant = getColor(context, R.attr.colorAccentTertiaryVariant) - val colorSurface = getColor(context, R.attr.colorSurface) - val colorSurfaceHighlight = getColor(context, R.attr.colorSurfaceHighlight) - val colorSurfaceVariant = getColor(context, R.attr.colorSurfaceVariant) - val colorSurfaceHeader = getColor(context, R.attr.colorSurfaceHeader) - val colorError = getColor(context, R.attr.colorError) - val colorBackground = getColor(context, R.attr.colorBackground) - val colorBackgroundFloating = getColor(context, R.attr.colorBackgroundFloating) - val panelColorBackground = getColor(context, R.attr.panelColorBackground) - val textColorPrimary = getColor(context, R.attr.textColorPrimary) - val textColorSecondary = getColor(context, R.attr.textColorSecondary) - val textColorTertiary = getColor(context, R.attr.textColorTertiary) - val textColorPrimaryInverse = getColor(context, R.attr.textColorPrimaryInverse) - val textColorSecondaryInverse = getColor(context, R.attr.textColorSecondaryInverse) - val textColorTertiaryInverse = getColor(context, R.attr.textColorTertiaryInverse) - val textColorOnAccent = getColor(context, R.attr.textColorOnAccent) - val colorForeground = getColor(context, R.attr.colorForeground) - val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse) + val colorSurfaceBright = getColor(context, R.attr.materialColorSurfaceBright) + val colorSurfaceContainerHigh = getColor(context, R.attr.materialColorSurfaceContainerHigh) companion object { fun getColor(context: Context, attr: Int): Color { diff --git a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java index 4ed7e19f341d..10b004e1b243 100644 --- a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java +++ b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java @@ -29,6 +29,7 @@ import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.SparseIntArray; @@ -36,6 +37,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -57,6 +59,7 @@ public final class DeviceStateRotationLockSettingsManager { private final SecureSettings mSecureSettings; private String[] mDeviceStateRotationLockDefaults; private SparseIntArray mDeviceStateRotationLockSettings; + private SparseIntArray mDeviceStateDefaultRotationLockSettings; private SparseIntArray mDeviceStateRotationLockFallbackSettings; private String mLastSettingValue; private List<SettableDeviceState> mSettableDeviceStates; @@ -93,9 +96,7 @@ public final class DeviceStateRotationLockSettingsManager { /** Returns true if device-state based rotation lock settings are enabled. */ public static boolean isDeviceStateRotationLockEnabled(Context context) { return context.getResources() - .getStringArray(R.array.config_perDeviceStateRotationLockDefaults) - .length - > 0; + .getStringArray(R.array.config_perDeviceStateRotationLockDefaults).length > 0; } private void listenForSettingsChange() { @@ -228,6 +229,15 @@ public final class DeviceStateRotationLockSettingsManager { try { key = Integer.parseInt(values[i++]); value = Integer.parseInt(values[i++]); + boolean isPersistedValueIgnored = value == DEVICE_STATE_ROTATION_LOCK_IGNORED; + boolean isDefaultValueIgnored = mDeviceStateDefaultRotationLockSettings.get(key) + == DEVICE_STATE_ROTATION_LOCK_IGNORED; + if (isPersistedValueIgnored != isDefaultValueIgnored) { + Log.w(TAG, "Conflict for ignored device state " + key + + ". Falling back on defaults"); + fallbackOnDefaults(); + return; + } mDeviceStateRotationLockSettings.put(key, value); } catch (NumberFormatException e) { Log.wtf(TAG, "Error deserializing one of the saved settings", e); @@ -276,6 +286,9 @@ public final class DeviceStateRotationLockSettingsManager { } private void persistSettingIfChanged(String newSettingValue) { + Log.v(TAG, "persistSettingIfChanged: " + + "last=" + mLastSettingValue + ", " + + "new=" + newSettingValue); if (TextUtils.equals(mLastSettingValue, newSettingValue)) { return; } @@ -288,6 +301,8 @@ public final class DeviceStateRotationLockSettingsManager { private void loadDefaults() { mSettableDeviceStates = new ArrayList<>(mDeviceStateRotationLockDefaults.length); + mDeviceStateDefaultRotationLockSettings = new SparseIntArray( + mDeviceStateRotationLockDefaults.length); mDeviceStateRotationLockSettings = new SparseIntArray( mDeviceStateRotationLockDefaults.length); mDeviceStateRotationLockFallbackSettings = new SparseIntArray(1); @@ -311,6 +326,7 @@ public final class DeviceStateRotationLockSettingsManager { boolean isSettable = rotationLockSetting != DEVICE_STATE_ROTATION_LOCK_IGNORED; mSettableDeviceStates.add(new SettableDeviceState(deviceState, isSettable)); mDeviceStateRotationLockSettings.put(deviceState, rotationLockSetting); + mDeviceStateDefaultRotationLockSettings.put(deviceState, rotationLockSetting); } catch (NumberFormatException e) { Log.wtf(TAG, "Error parsing settings entry. Entry was: " + entry, e); return; @@ -318,6 +334,22 @@ public final class DeviceStateRotationLockSettingsManager { } } + /** Dumps internal state. */ + public void dump(IndentingPrintWriter pw) { + pw.println("DeviceStateRotationLockSettingsManager"); + pw.increaseIndent(); + pw.println("mDeviceStateRotationLockDefaults: " + Arrays.toString( + mDeviceStateRotationLockDefaults)); + pw.println("mDeviceStateDefaultRotationLockSettings: " + + mDeviceStateDefaultRotationLockSettings); + pw.println("mDeviceStateRotationLockSettings: " + mDeviceStateRotationLockSettings); + pw.println("mDeviceStateRotationLockFallbackSettings: " + + mDeviceStateRotationLockFallbackSettings); + pw.println("mSettableDeviceStates: " + mSettableDeviceStates); + pw.println("mLastSettingValue: " + mLastSettingValue); + pw.decreaseIndent(); + } + /** * Called when the persisted settings have changed, requiring a reinitialization of the * in-memory map. @@ -372,5 +404,13 @@ public final class DeviceStateRotationLockSettingsManager { public int hashCode() { return Objects.hash(mDeviceState, mIsSettable); } + + @Override + public String toString() { + return "SettableDeviceState{" + + "mDeviceState=" + mDeviceState + + ", mIsSettable=" + mIsSettable + + '}'; + } } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaLogger.kt index 78df0f27cc6a..ca88f8da63c9 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaLogger.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaLogger.kt @@ -16,6 +16,7 @@ package com.android.settingslib.spa.framework.common +import android.app.settings.SettingsEnums import android.os.Bundle // Defines the category of the log, for quick filter @@ -31,20 +32,21 @@ enum class LogCategory { } // Defines the log events in Spa. -enum class LogEvent { +enum class LogEvent(val action: Int) { // Page related events. - PAGE_ENTER, - PAGE_LEAVE, + PAGE_ENTER(SettingsEnums.PAGE_VISIBLE), + PAGE_LEAVE(SettingsEnums.PAGE_HIDE), // Entry related events. - ENTRY_CLICK, - ENTRY_SWITCH, + ENTRY_CLICK(SettingsEnums.ACTION_SETTINGS_TILE_CLICK), + ENTRY_SWITCH(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE), } internal const val LOG_DATA_DISPLAY_NAME = "name" -internal const val LOG_DATA_SESSION_NAME = "session" internal const val LOG_DATA_SWITCH_STATUS = "switch" +const val LOG_DATA_SESSION_NAME = "session" + /** * The interface of logger in Spa */ diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt index 2c3c2e003832..d8c35a36d061 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt @@ -22,9 +22,11 @@ import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory +const val SESSION_UNKNOWN = "unknown" const val SESSION_BROWSE = "browse" const val SESSION_SEARCH = "search" const val SESSION_SLICE = "slice" +const val SESSION_EXTERNAL = "external" const val KEY_DESTINATION = "spaActivityDestination" const val KEY_HIGHLIGHT_ENTRY = "highlightEntry" diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt index 1a3c0ab67b93..47bf85d8417b 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt @@ -19,6 +19,7 @@ package com.android.settingslib.spaprivileged.template.app import android.content.Context import android.content.pm.ApplicationInfo import android.text.format.Formatter +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.produceState @@ -30,18 +31,26 @@ import com.android.settingslib.spaprivileged.model.app.userHandle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +private const val TAG = "AppStorageSize" + @Composable fun ApplicationInfo.getStorageSize(): State<String> { val context = LocalContext.current return produceState(initialValue = stringResource(R.string.summary_placeholder)) { withContext(Dispatchers.IO) { - value = Formatter.formatFileSize(context, calculateSizeBytes(context)) + val sizeBytes = calculateSizeBytes(context) + value = if (sizeBytes != null) Formatter.formatFileSize(context, sizeBytes) else "" } } } -private fun ApplicationInfo.calculateSizeBytes(context: Context): Long { +private fun ApplicationInfo.calculateSizeBytes(context: Context): Long? { val storageStatsManager = context.storageStatsManager - val stats = storageStatsManager.queryStatsForPackage(storageUuid, packageName, userHandle) - return stats.codeBytes + stats.dataBytes + return try { + val stats = storageStatsManager.queryStatsForPackage(storageUuid, packageName, userHandle) + stats.codeBytes + stats.dataBytes + } catch (e: Exception) { + Log.w(TAG, "Failed to query stats: $e") + null + } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt index fcacc34ba881..e3af58702445 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt @@ -20,6 +20,7 @@ import android.app.usage.StorageStats import android.app.usage.StorageStatsManager import android.content.Context import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager.NameNotFoundException import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.junit4.createComposeRule @@ -60,9 +61,11 @@ class AppStorageSizeTest { @Before fun setUp() { whenever(context.storageStatsManager).thenReturn(storageStatsManager) - whenever(storageStatsManager.queryStatsForPackage( - app.storageUuid, app.packageName, app.userHandle - )).thenReturn(STATS) + whenever( + storageStatsManager.queryStatsForPackage( + app.storageUuid, app.packageName, app.userHandle + ) + ).thenReturn(STATS) } @Test @@ -78,6 +81,24 @@ class AppStorageSizeTest { composeTestRule.waitUntil { storageSize.value == "120 B" } } + @Test + fun getStorageSize_throwException() { + var storageSize = stateOf("Computing") + whenever( + storageStatsManager.queryStatsForPackage( + app.storageUuid, app.packageName, app.userHandle + ) + ).thenThrow(NameNotFoundException()) + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + storageSize = app.getStorageSize() + } + } + + composeTestRule.waitUntil { storageSize.value == "" } + } + companion object { private val STATS = StorageStats().apply { codeBytes = 100 diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index b92b3d665675..8e7d36edb05a 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1648,4 +1648,7 @@ <item>Move right</item> <item>Move up</item> </string-array> + + <!-- Formatting states for the scale of font size, in percent. Double "%" is required to represent the symbol "%". [CHAR LIMIT=20] --> + <string name="font_scale_percentage"> <xliff:g id="percentage">%1$d</xliff:g> %%</string> </resources> diff --git a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java index fb06976ebfe3..3ec5ebad0de6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java @@ -23,6 +23,7 @@ import android.widget.Switch; import androidx.annotation.Keep; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceViewHolder; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; @@ -132,6 +133,11 @@ public class PrimarySwitchPreference extends RestrictedPreference { } } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public boolean isSwitchEnabled() { + return mEnableSwitch; + } + /** * If admin is not null, disables the switch. * Otherwise, keep it enabled. diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java index 81006dd6b011..0fa15eb6bc0c 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java @@ -33,7 +33,10 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager.SettableDeviceState; +import com.google.common.truth.Expect; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -45,6 +48,8 @@ import java.util.List; @RunWith(AndroidJUnit4.class) public class DeviceStateRotationLockSettingsManagerTest { + @Rule public Expect mExpect = Expect.create(); + @Mock private Context mMockContext; @Mock private Resources mMockResources; @@ -117,4 +122,40 @@ public class DeviceStateRotationLockSettingsManagerTest { new SettableDeviceState(/* deviceState= */ 0, /* isSettable= */ false) ).inOrder(); } + + @Test + public void persistedInvalidIgnoredState_returnsDefaults() { + when(mMockResources.getStringArray( + R.array.config_perDeviceStateRotationLockDefaults)).thenReturn( + new String[]{"0:1", "1:0:2", "2:2"}); + // Here 2 has IGNORED, and in the defaults 1 has IGNORED. + persistSettings("0:2:2:0:1:2"); + DeviceStateRotationLockSettingsManager manager = + new DeviceStateRotationLockSettingsManager(mMockContext, mFakeSecureSettings); + + mExpect.that(manager.getRotationLockSetting(0)).isEqualTo(1); + mExpect.that(manager.getRotationLockSetting(1)).isEqualTo(2); + mExpect.that(manager.getRotationLockSetting(2)).isEqualTo(2); + } + + @Test + public void persistedValidValues_returnsPersistedValues() { + when(mMockResources.getStringArray( + R.array.config_perDeviceStateRotationLockDefaults)).thenReturn( + new String[]{"0:1", "1:0:2", "2:2"}); + persistSettings("0:2:1:0:2:1"); + DeviceStateRotationLockSettingsManager manager = + new DeviceStateRotationLockSettingsManager(mMockContext, mFakeSecureSettings); + + mExpect.that(manager.getRotationLockSetting(0)).isEqualTo(2); + mExpect.that(manager.getRotationLockSetting(1)).isEqualTo(1); + mExpect.that(manager.getRotationLockSetting(2)).isEqualTo(1); + } + + private void persistSettings(String value) { + mFakeSecureSettings.putStringForUser( + Settings.Secure.DEVICE_STATE_ROTATION_LOCK, + value, + UserHandle.USER_CURRENT); + } } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index c9d840a77b80..6a5535d345db 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -97,5 +97,8 @@ public class SystemSettings { Settings.System.TOUCHPAD_NATURAL_SCROLLING, Settings.System.TOUCHPAD_TAP_TO_CLICK, Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE, + Settings.System.CAMERA_FLASH_NOTIFICATION, + Settings.System.SCREEN_FLASH_NOTIFICATION, + Settings.System.SCREEN_FLASH_NOTIFICATION_COLOR, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index c2a3ada53591..3fe12b3dab32 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -16,6 +16,7 @@ package android.provider.settings.validators; +import static android.provider.settings.validators.SettingsValidators.ANY_INTEGER_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.ANY_STRING_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.BOOLEAN_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.COMPONENT_NAME_VALIDATOR; @@ -215,5 +216,8 @@ public class SystemSettingsValidators { VALIDATORS.put(System.UNREAD_NOTIFICATION_DOT_INDICATOR, BOOLEAN_VALIDATOR); VALIDATORS.put(System.AUTO_LAUNCH_MEDIA_CONTROLS, BOOLEAN_VALIDATOR); VALIDATORS.put(System.LOCALE_PREFERENCES, ANY_STRING_VALIDATOR); + VALIDATORS.put(System.CAMERA_FLASH_NOTIFICATION, BOOLEAN_VALIDATOR); + VALIDATORS.put(System.SCREEN_FLASH_NOTIFICATION, BOOLEAN_VALIDATOR); + VALIDATORS.put(System.SCREEN_FLASH_NOTIFICATION_COLOR, ANY_INTEGER_VALIDATOR); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 1a6920803b03..d49627e5334a 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2835,6 +2835,15 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.System.NOTIFICATION_VIBRATION_INTENSITY, SystemSettingsProto.Notification.VIBRATION_INTENSITY); + dumpSetting(s, p, + Settings.System.CAMERA_FLASH_NOTIFICATION, + SystemSettingsProto.Notification.CAMERA_FLASH_NOTIFICATION); + dumpSetting(s, p, + Settings.System.SCREEN_FLASH_NOTIFICATION, + SystemSettingsProto.Notification.SCREEN_FLASH_NOTIFICATION); + dumpSetting(s, p, + Settings.System.SCREEN_FLASH_NOTIFICATION_COLOR, + SystemSettingsProto.Notification.SCREEN_FLASH_NOTIFICATION_COLOR_GLOBAL); // Settings.System.NOTIFICATIONS_USE_RING_VOLUME intentionally excluded since it's deprecated. p.end(notificationToken); diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp index 140c10da922e..f358417e6bea 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp +++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp @@ -18,6 +18,15 @@ package { default_applicable_licenses: ["Android-Apache-2.0"], } +// This filegroup is used by menu tests. +filegroup { + name: "AccessibilityMenuSource", + srcs: [ + "src/**/AccessibilityMenuService.java", + "src/**/A11yMenuShortcut.java", + ], +} + android_app { name: "AccessibilityMenu", diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml index 39e5a8c6876b..a902c5b54b7b 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/shortcutItem" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingTop="@dimen/grid_item_padding" 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 c1f2aa86c6b5..8ca64d2505ce 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 @@ -16,6 +16,7 @@ package com.android.systemui.accessibility.accessibilitymenu; +import android.Manifest; import android.accessibilityservice.AccessibilityButtonController; import android.accessibilityservice.AccessibilityService; import android.content.BroadcastReceiver; @@ -51,8 +52,12 @@ import java.util.List; /** @hide */ public class AccessibilityMenuService extends AccessibilityService implements View.OnTouchListener { - private static final String TAG = "A11yMenuService"; + public static final String PACKAGE_NAME = AccessibilityMenuService.class.getPackageName(); + public static final String INTENT_TOGGLE_MENU = ".toggle_menu"; + public static final String INTENT_HIDE_MENU = ".hide_menu"; + + private static final String TAG = "A11yMenuService"; private static final long BUFFER_MILLISECONDS_TO_PREVENT_UPDATE_FAILURE = 100L; private static final int BRIGHTNESS_UP_INCREMENT_GAMMA = @@ -74,7 +79,8 @@ public class AccessibilityMenuService extends AccessibilityService // TODO(b/136716947): Support multi-display once a11y framework side is ready. private DisplayManager mDisplayManager; - final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { + private final DisplayManager.DisplayListener mDisplayListener = + new DisplayManager.DisplayListener() { int mRotation; @Override @@ -95,13 +101,20 @@ public class AccessibilityMenuService extends AccessibilityService } }; - final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + private final BroadcastReceiver mHideMenuReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mA11yMenuLayout.hideMenu(); } }; + private final BroadcastReceiver mToggleMenuReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mA11yMenuLayout.toggleVisibility(); + } + }; + /** * Update a11y menu layout when large button setting is changed. */ @@ -172,7 +185,19 @@ public class AccessibilityMenuService extends AccessibilityService protected void onServiceConnected() { mA11yMenuLayout = new A11yMenuOverlayLayout(this); - registerReceiver(mBroadcastReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); + IntentFilter hideMenuFilter = new IntentFilter(); + hideMenuFilter.addAction(Intent.ACTION_SCREEN_OFF); + hideMenuFilter.addAction(PACKAGE_NAME + INTENT_HIDE_MENU); + + // Including WRITE_SECURE_SETTINGS enforces that we only listen to apps + // with the restricted WRITE_SECURE_SETTINGS permission who broadcast this intent. + registerReceiver(mHideMenuReceiver, hideMenuFilter, + Manifest.permission.WRITE_SECURE_SETTINGS, null, + Context.RECEIVER_EXPORTED); + registerReceiver(mToggleMenuReceiver, + new IntentFilter(PACKAGE_NAME + INTENT_TOGGLE_MENU), + Manifest.permission.WRITE_SECURE_SETTINGS, null, + Context.RECEIVER_EXPORTED); mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs.registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); @@ -260,7 +285,8 @@ public class AccessibilityMenuService extends AccessibilityService * @param increment The increment amount in gamma-space */ private void adjustBrightness(int increment) { - BrightnessInfo info = getDisplay().getBrightnessInfo(); + Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); + BrightnessInfo info = display.getBrightnessInfo(); int gamma = BrightnessUtils.convertLinearToGammaFloat( info.brightness, info.brightnessMinimum, @@ -275,7 +301,7 @@ public class AccessibilityMenuService extends AccessibilityService info.brightnessMinimum, info.brightnessMaximum ); - mDisplayManager.setBrightness(getDisplayId(), brightness); + mDisplayManager.setBrightness(display.getDisplayId(), brightness); mA11yMenuLayout.showSnackbar( getString(R.string.brightness_percentage_label, (gamma / (BrightnessUtils.GAMMA_SPACE_MAX / 100)))); @@ -310,7 +336,8 @@ public class AccessibilityMenuService extends AccessibilityService @Override public boolean onUnbind(Intent intent) { - unregisterReceiver(mBroadcastReceiver); + unregisterReceiver(mHideMenuReceiver); + unregisterReceiver(mToggleMenuReceiver); mPrefs.unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); sInitialized = false; return super.onUnbind(intent); 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 index 6f0fe374d6f6..6ae65cb6d8f6 100644 --- 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 @@ -21,6 +21,7 @@ import android.view.LayoutInflater; import android.view.TouchDelegate; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.BaseAdapter; import android.widget.ImageButton; import android.widget.TextView; @@ -146,6 +147,15 @@ public class A11yMenuAdapter extends BaseAdapter { shortcutIconButton.setBackground( mShortcutDrawableUtils.createAdaptiveIconDrawable(shortcutItem.imageColor)); + + shortcutIconButton.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo( + View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setUniqueId(host.getTag().toString()); + } + }); } } } diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp new file mode 100644 index 000000000000..1757dda84eef --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp @@ -0,0 +1,43 @@ +// +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "AccessibilityMenuServiceTests", + certificate: "platform", + libs: [ + "android.test.runner", + "android.test.base", + ], + static_libs: [ + "androidx.test.core", + "androidx.test.runner", + "androidx.test.ext.junit", + "compatibility-device-util-axt", + "platform-test-annotations", + "truth-prebuilt", + ], + srcs: [ + "src/**/*.java", + ":AccessibilityMenuSource", + ], + platform_apis: true, + test_suites: ["device-tests"], + instrumentation_for: "AccessibilityMenu", +} diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidManifest.xml b/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidManifest.xml new file mode 100644 index 000000000000..7be6ca742376 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidManifest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.systemui.accessibility.accessibilitymenu.tests"> + + <!-- Needed to write to Settings.Secure to enable and disable the service under test. --> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS"/> + + <application android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.systemui.accessibility.accessibilitymenu.tests" + android:label="AccessibilityMenu Test Cases"> + </instrumentation> +</manifest>
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidTest.xml b/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidTest.xml new file mode 100644 index 000000000000..39bee5392720 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<configuration description="Runs AccessibilityMenu Test Cases."> + <option name="test-tag" value="AccessibilityMenuServiceTests" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="AccessibilityMenuServiceTests.apk" /> + <option name="aapt-version" value="AAPT2" /> + </target_preparer> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.systemui.accessibility.accessibilitymenu.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + </test> +</configuration>
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/TEST_MAPPING b/packages/SystemUI/accessibility/accessibilitymenu/tests/TEST_MAPPING new file mode 100644 index 000000000000..2bd52b552698 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/TEST_MAPPING @@ -0,0 +1,15 @@ +{ + "presubmit": [ + { + "name": "AccessibilityMenuServiceTests", + "options": [ + { + "include-annotation": "android.platform.test.annotations.Presubmit" + }, + { + "exclude-annotation": "android.support.test.filters.FlakyTest" + } + ] + } + ] +}
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java new file mode 100644 index 000000000000..529a70c1ab18 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java @@ -0,0 +1,183 @@ +/* + * 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.tests; + +import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU; +import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU; +import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.content.Context; +import android.content.Intent; +import android.hardware.display.BrightnessInfo; +import android.hardware.display.DisplayManager; +import android.provider.Settings; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.compatibility.common.util.TestUtils; +import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut.ShortcutId; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class AccessibilityMenuServiceTest { + private static final String TAG = "A11yMenuServiceTest"; + + private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5; + private static final int TIMEOUT_UI_CHANGE_S = 5; + + private static Instrumentation sInstrumentation; + private static UiAutomation sUiAutomation; + + private static AccessibilityManager sAccessibilityManager; + + @BeforeClass + public static void classSetup() throws Throwable { + final String serviceName = PACKAGE_NAME + "/.AccessibilityMenuService"; + sInstrumentation = InstrumentationRegistry.getInstrumentation(); + sUiAutomation = sInstrumentation.getUiAutomation( + UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES); + final Context context = sInstrumentation.getContext(); + sAccessibilityManager = context.getSystemService(AccessibilityManager.class); + + // Disable all a11yServices if any are active. + if (!sAccessibilityManager.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK).isEmpty()) { + Settings.Secure.putString(context.getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, ""); + TestUtils.waitUntil("Failed to disable all services", + TIMEOUT_SERVICE_STATUS_CHANGE_S, + () -> sAccessibilityManager.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK).isEmpty()); + } + + // Enable a11yMenu service. + Settings.Secure.putString(context.getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, serviceName); + + TestUtils.waitUntil("Failed to enable service", + TIMEOUT_SERVICE_STATUS_CHANGE_S, + () -> sAccessibilityManager.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK).stream().filter( + info -> info.getId().contains(serviceName)).count() == 1); + } + + @AfterClass + public static void classTeardown() throws Throwable { + Settings.Secure.putString(sInstrumentation.getTargetContext().getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, ""); + } + + private boolean isMenuVisible() { + return sUiAutomation.getRootInActiveWindow() != null + && sUiAutomation.getRootInActiveWindow().getPackageName().toString().equals( + PACKAGE_NAME); + } + + private void openMenu() throws Throwable { + if (isMenuVisible()) { + return; + } + Intent intent = new Intent(PACKAGE_NAME + INTENT_TOGGLE_MENU); + sInstrumentation.getContext().sendBroadcast(intent); + TestUtils.waitUntil("Timed out before menu could appear.", + TIMEOUT_UI_CHANGE_S, () -> isMenuVisible()); + } + + private void closeMenu() throws Throwable { + if (!isMenuVisible()) { + return; + } + Intent intent = new Intent(PACKAGE_NAME + INTENT_HIDE_MENU); + sInstrumentation.getContext().sendBroadcast(intent); + TestUtils.waitUntil("Timed out before menu could close.", + TIMEOUT_UI_CHANGE_S, () -> !isMenuVisible()); + } + + private List<AccessibilityNodeInfo> getGridButtonList() { + return sUiAutomation.getRootInActiveWindow() + .findAccessibilityNodeInfosByViewId(PACKAGE_NAME + ":id/shortcutIconBtn"); + } + + private AccessibilityNodeInfo findGridButtonInfo( + List<AccessibilityNodeInfo> buttons, String text) { + for (AccessibilityNodeInfo button: buttons) { + if (button.getUniqueId().equals(text)) { + return button; + } + } + return null; + } + + @Test + public void testAdjustBrightness() throws Throwable { + openMenu(); + + Context context = sInstrumentation.getTargetContext(); + DisplayManager displayManager = context.getSystemService( + DisplayManager.class); + float resetBrightness = displayManager.getBrightness(context.getDisplayId()); + + List<AccessibilityNodeInfo> buttons = getGridButtonList(); + AccessibilityNodeInfo brightnessUpButton = findGridButtonInfo(buttons, + String.valueOf(ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal())); + AccessibilityNodeInfo brightnessDownButton = findGridButtonInfo(buttons, + String.valueOf(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal())); + + int clickId = AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.getId(); + BrightnessInfo brightnessInfo = displayManager.getDisplay( + context.getDisplayId()).getBrightnessInfo(); + + try { + displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum); + TestUtils.waitUntil("Could not change to minimum brightness", + TIMEOUT_UI_CHANGE_S, + () -> displayManager.getBrightness(context.getDisplayId()) + == brightnessInfo.brightnessMinimum); + brightnessUpButton.performAction(clickId); + TestUtils.waitUntil("Did not detect an increase in brightness.", + TIMEOUT_UI_CHANGE_S, + () -> displayManager.getBrightness(context.getDisplayId()) + > brightnessInfo.brightnessMinimum); + + displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum); + TestUtils.waitUntil("Could not change to maximum brightness", + TIMEOUT_UI_CHANGE_S, + () -> displayManager.getBrightness(context.getDisplayId()) + == brightnessInfo.brightnessMaximum); + brightnessDownButton.performAction(clickId); + TestUtils.waitUntil("Did not detect a decrease in brightness.", + TIMEOUT_UI_CHANGE_S, + () -> displayManager.getBrightness(context.getDisplayId()) + < brightnessInfo.brightnessMaximum); + } finally { + displayManager.setBrightness(context.getDisplayId(), resetBrightness); + closeMenu(); + } + } +} diff --git a/packages/SystemUI/animation/.gitignore b/packages/SystemUI/animation/.gitignore new file mode 100644 index 000000000000..f9a33dbbcc7e --- /dev/null +++ b/packages/SystemUI/animation/.gitignore @@ -0,0 +1,9 @@ +.idea/ +.gradle/ +gradle/ +build/ +gradlew* +local.properties +*.iml +android.properties +buildSrc
\ No newline at end of file diff --git a/packages/SystemUI/animation/build.gradle b/packages/SystemUI/animation/build.gradle new file mode 100644 index 000000000000..939455fa44ac --- /dev/null +++ b/packages/SystemUI/animation/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +// TODO: Pull out surfaceeffects outside of src and have separate build files there. +android { + sourceSets { + main { + java.srcDirs = ["${SYS_UI_DIR}/animation/src/com/android/systemui/surfaceeffects/"] + manifest.srcFile "${SYS_UI_DIR}/animation/AndroidManifest.xml" + } + } + + compileSdk 33 + + defaultConfig { + minSdk 33 + targetSdk 33 + } + + lintOptions { + abortOnError false + } + tasks.lint.enabled = false + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + } + kotlinOptions { + jvmTarget = '1.8' + freeCompilerArgs = ["-Xjvm-default=all"] + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0" + implementation 'androidx.core:core-animation:1.0.0-alpha02' + implementation 'androidx.core:core-ktx:1.9.0' +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 17a94b8639d0..296c2ae5cf99 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -419,7 +419,7 @@ class ActivityLaunchAnimator( internal val delegate: AnimationDelegate init { - delegate = AnimationDelegate(controller, callback, launchAnimator, listener) + delegate = AnimationDelegate(controller, callback, listener, launchAnimator) } @BinderThread @@ -446,10 +446,10 @@ class ActivityLaunchAnimator( constructor( private val controller: Controller, private val callback: Callback, - /** The animator to use to animate the window launch. */ - private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, /** Listener for animation lifecycle events. */ - private val listener: Listener? = null + private val listener: Listener? = null, + /** The animator to use to animate the window launch. */ + private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> { private val launchContainer = controller.launchContainer private val context = launchContainer.context diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt new file mode 100644 index 000000000000..f64ea4561906 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt @@ -0,0 +1,77 @@ +/* + * 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.internal.systemui.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UElement + +class DemotingTestWithoutBugDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List<Class<out UElement>> { + return listOf(UAnnotation::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitAnnotation(node: UAnnotation) { + if (node.qualifiedName !in DEMOTING_ANNOTATION) { + return + } + val bugId = node.findAttributeValue("bugId")!!.evaluate() as Int + if (bugId <= 0) { + val location = context.getLocation(node) + val message = "Please attach a bug id to track demoted test" + context.report(ISSUE, node, location, message) + } + } + } + } + + companion object { + val DEMOTING_ANNOTATION = + listOf("androidx.test.filters.FlakyTest", "android.platform.test.annotations.FlakyTest") + + @JvmField + val ISSUE: Issue = + Issue.create( + id = "DemotingTestWithoutBug", + briefDescription = "Demoting a test without attaching a bug.", + explanation = + """ + Annotations (`@FlakyTest`) demote tests to an unmonitored \ + test suite. Please set the `bugId` field in such annotations to track \ + the test status. + """, + category = Category.TESTING, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + DemotingTestWithoutBugDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index 84f70502fa45..387b67d231cd 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -39,7 +39,8 @@ class SystemUIIssueRegistry : IssueRegistry() { RegisterReceiverViaContextDetector.ISSUE, SoftwareBitmapDetector.ISSUE, NonInjectedServiceDetector.ISSUE, - StaticSettingsProviderDetector.ISSUE + StaticSettingsProviderDetector.ISSUE, + DemotingTestWithoutBugDetector.ISSUE ) override val api: Int diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt new file mode 100644 index 000000000000..557c300635eb --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt @@ -0,0 +1,151 @@ +/* + * 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.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +class DemotingTestWithoutBugDetectorTest : SystemUILintDetectorTest() { + + override fun getDetector(): Detector = DemotingTestWithoutBugDetector() + override fun getIssues(): List<Issue> = listOf(DemotingTestWithoutBugDetector.ISSUE) + + @Test + fun testMarkFlaky_withBugId() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import androidx.test.filters.FlakyTest; + + @FlakyTest(bugId = 123) + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expectClean() + + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.platform.test.annotations.FlakyTest; + + @FlakyTest(bugId = 123) + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testMarkFlaky_withoutBugId() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import androidx.test.filters.FlakyTest; + + @FlakyTest + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:4: Warning: Please attach a bug id to track demoted test [DemotingTestWithoutBug] + @FlakyTest + ~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.platform.test.annotations.FlakyTest; + + @FlakyTest + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:4: Warning: Please attach a bug id to track demoted test [DemotingTestWithoutBug] + @FlakyTest + ~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + private val filtersFlakyTestStub: TestFile = + java( + """ + package androidx.test.filters; + + public @interface FlakyTest { + int bugId() default -1; + } + """ + ) + private val annotationsFlakyTestStub: TestFile = + java( + """ + package android.platform.test.annotations; + + public @interface FlakyTest { + int bugId() default -1; + } + """ + ) + private val stubs = arrayOf(filtersFlakyTestStub, annotationsFlakyTestStub) +} diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index b6a78f56ec5f..caf32331b81f 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -82,7 +82,7 @@ <!-- The vertical margin between the date and the owner info. --> <!-- The translation for disappearing security views after having solved them. --> - <dimen name="disappear_y_translation">-32dp</dimen> + <dimen name="disappear_y_translation">-50dp</dimen> <!-- Dimens for animation for the Bouncer PIN view --> <dimen name="pin_view_trans_y_entry">120dp</dimen> diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_background.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_background.xml index 58fe368ce4e1..97bd18e0442b 100644 --- a/packages/SystemUI/res/drawable/accessibility_window_magnification_background.xml +++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_background.xml @@ -27,7 +27,7 @@ <shape android:shape="rectangle"> <corners android:radius="@dimen/magnifier_outer_corner_radius" /> <stroke - android:color="@android:color/black" + android:color="@color/magnification_drag_handle_stroke" android:width="@dimen/magnifier_stroke_width"/> </shape> </item> diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background.xml index a52e8053d8a0..66617e16b33a 100644 --- a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background.xml +++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background.xml @@ -16,7 +16,7 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <stroke - android:color="@android:color/black" + android:color="@color/magnification_drag_handle_stroke" android:width="@dimen/magnifier_stroke_width"/> <corners android:radius="@dimen/magnifier_corner_radius" /> <solid android:color="@color/magnification_border_color" /> diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change.xml new file mode 100644 index 000000000000..e367f50632a9 --- /dev/null +++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?><!-- + ~ 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. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <stroke + android:color="@color/magnification_border_color" + android:width="@dimen/magnifier_stroke_width"/> + <corners android:radius="@dimen/magnifier_corner_radius" /> + <solid android:color="@color/magnification_drag_handle_background_change" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_move_magnification.xml b/packages/SystemUI/res/drawable/ic_move_magnification.xml index 641bb4381fc8..079600734bc8 100644 --- a/packages/SystemUI/res/drawable/ic_move_magnification.xml +++ b/packages/SystemUI/res/drawable/ic_move_magnification.xml @@ -20,6 +20,6 @@ android:viewportHeight="24" android:tint="?attr/colorControlNormal"> <path - android:fillColor="@color/magnification_drag_handle_tint" + android:fillColor="@color/magnification_drag_handle_stroke" android:pathData="M12,15Q10.75,15 9.875,14.125Q9,13.25 9,12Q9,10.75 9.875,9.875Q10.75,9 12,9Q13.25,9 14.125,9.875Q15,10.75 15,12Q15,13.25 14.125,14.125Q13.25,15 12,15ZM12,22 L7.75,17.75 9.15,16.35 12,19.15 14.85,16.35 16.25,17.75ZM6.25,16.25 L2,12 6.25,7.75 7.65,9.15 4.85,12 7.65,14.85ZM9.15,7.65 L7.75,6.25 12,2 16.25,6.25 14.85,7.65 12,4.85ZM17.75,16.25 L16.35,14.85 19.15,12 16.35,9.15 17.75,7.75 22,12Z"/> </vector> diff --git a/packages/SystemUI/res/drawable/qs_media_rec_scrim.xml b/packages/SystemUI/res/drawable/qs_media_rec_scrim.xml new file mode 100644 index 000000000000..de0a6201cb09 --- /dev/null +++ b/packages/SystemUI/res/drawable/qs_media_rec_scrim.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <!-- gradient from 25% in the center to 100% at edges --> + <gradient + android:type="radial" + android:gradientRadius="40%p" + android:startColor="#AE000000" + android:endColor="#00000000" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_recommendation_view.xml b/packages/SystemUI/res/layout/media_recommendation_view.xml index c54c4e48d13d..a4aeba1dbcd6 100644 --- a/packages/SystemUI/res/layout/media_recommendation_view.xml +++ b/packages/SystemUI/res/layout/media_recommendation_view.xml @@ -22,9 +22,10 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:translationZ="0dp" - android:scaleType="centerCrop" + android:scaleType="matrix" android:adjustViewBounds="true" android:clipToOutline="true" + android:layerType="hardware" android:background="@drawable/bg_smartspace_media_item"/> <!-- App icon --> diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml index 7dfe7c4c72be..ae0f8f46599d 100644 --- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml +++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml @@ -21,7 +21,9 @@ android:layout_height="wrap_content" android:background="@drawable/accessibility_magnification_setting_view_bg" android:orientation="vertical" - android:padding="@dimen/magnification_setting_background_padding"> + android:padding="@dimen/magnification_setting_background_padding" + android:focusable="true" + android:contentDescription="@string/accessibility_magnification_settings_panel_description"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index c6cc0bc01116..d4ebd100a91d 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -163,8 +163,8 @@ <color name="magnification_border_color">#F29900</color> <color name="magnification_switch_button_color">#7F000000</color> <color name="magnification_drag_corner_background">#E5FFFFFF</color> - <color name="magnification_drag_handle_color">#B3000000</color> - <color name="magnification_drag_handle_tint">#111111</color> + <color name="magnification_drag_handle_stroke">#000000</color> + <color name="magnification_drag_handle_background_change">#111111</color> <color name="accessibility_magnifier_bg">#FCFCFC</color> <color name="accessibility_magnifier_bg_stroke">#E0E0E0</color> <color name="accessibility_magnifier_icon_color">#252525</color> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index e65c327736e1..8f90724c09b9 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -782,7 +782,7 @@ <!-- Duration in milliseconds of the dream in complications fade-in animation. --> <integer name="config_dreamOverlayInComplicationsDurationMs">250</integer> <!-- Duration in milliseconds of the y-translation animation when entering a dream --> - <integer name="config_dreamOverlayInTranslationYDurationMs">917</integer> + <integer name="config_dreamOverlayInTranslationYDurationMs">1167</integer> <!-- Delay in milliseconds before switching to the dock user and dreaming if a secondary user is active when the device is locked and docked. 0 indicates disabled. Default is 1 minute. --> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index aba3fc4615c9..0f2ce444f225 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -196,9 +196,6 @@ <!-- Increased height of a small notification in the status bar --> <dimen name="notification_min_height_increased">146dp</dimen> - <!-- Increased height of a collapsed media notification in the status bar --> - <dimen name="notification_min_height_media">160dp</dimen> - <!-- Height of a small notification in the status bar which was used before android N --> <dimen name="notification_min_height_legacy">64dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 4d989a67bffd..f4b3b87aab16 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2337,6 +2337,8 @@ <string name="magnification_mode_switch_state_window">Magnify part of screen</string> <!-- Click action label for magnification switch. [CHAR LIMIT=NONE] --> <string name="magnification_mode_switch_click_label">Switch</string> + <!-- Click action label for magnification settings panel. [CHAR LIMIT=NONE] --> + <string name="magnification_open_settings_click_label">Open magnification settings</string> <!-- Label of the corner of a rectangle that you can tap and drag to resize the magnification area. [CHAR LIMIT=NONE] --> <string name="magnification_drag_corner_to_resize">Drag corner to resize</string> @@ -2358,6 +2360,8 @@ <!-- Description of the window magnification Bottom handle [CHAR LIMIT=NONE]--> <string name="accessibility_magnification_bottom_handle">Bottom handle</string> + <!-- Description of the window magnification panel [CHAR LIMIT=NONE]--> + <string name="accessibility_magnification_settings_panel_description">Magnification settings</string> <!-- Title of the window magnification panel option Magnifier size [CHAR LIMIT=NONE]--> <string name="accessibility_magnifier_size">Magnifier size</string> <!-- Title of the window magnification panel option Zoom [CHAR LIMIT=NONE]--> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index 6dd359cb6351..45a5ce34f830 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -18,7 +18,6 @@ package com.android.systemui.shared.system; import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; -import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; import static android.app.ActivityTaskManager.getService; import android.annotation.NonNull; @@ -45,6 +44,7 @@ import android.os.ServiceManager; import android.os.SystemClock; import android.provider.Settings; import android.util.Log; +import android.view.Display; import android.view.IRecentsAnimationController; import android.view.IRecentsAnimationRunner; import android.view.RemoteAnimationTarget; @@ -112,6 +112,13 @@ public class ActivityManagerWrapper { } /** + * @see #getRunningTasks(boolean , int) + */ + public ActivityManager.RunningTaskInfo[] getRunningTasks(boolean filterOnlyVisibleRecents) { + return getRunningTasks(filterOnlyVisibleRecents, Display.INVALID_DISPLAY); + } + + /** * We ask for {@link #NUM_RECENT_ACTIVITIES_REQUEST} activities because when in split screen, * we'll get back 2 activities for each split app and one for launcher. Launcher might be more * "recently" used than one of the split apps so if we only request 2 tasks, then we might miss @@ -120,10 +127,12 @@ public class ActivityManagerWrapper { * @return an array of up to {@link #NUM_RECENT_ACTIVITIES_REQUEST} running tasks * filtering only for tasks that can be visible in the recent tasks list. */ - public ActivityManager.RunningTaskInfo[] getRunningTasks(boolean filterOnlyVisibleRecents) { + public ActivityManager.RunningTaskInfo[] getRunningTasks(boolean filterOnlyVisibleRecents, + int displayId) { // Note: The set of running tasks from the system is ordered by recency List<ActivityManager.RunningTaskInfo> tasks = - mAtm.getTasks(NUM_RECENT_ACTIVITIES_REQUEST, filterOnlyVisibleRecents); + mAtm.getTasks(NUM_RECENT_ACTIVITIES_REQUEST, + filterOnlyVisibleRecents, /* keepInExtras= */ false, displayId); return tasks.toArray(new RunningTaskInfo[tasks.size()]); } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java index fca55b1c69b4..6f7d66d03cab 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; +import static android.view.WindowManager.TRANSIT_SLEEP; import android.annotation.SuppressLint; import android.app.ActivityManager; @@ -46,6 +47,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.util.TransitionUtil; import java.util.ArrayList; +import java.util.HashMap; /** * Helper class to build {@link RemoteTransition} objects @@ -205,6 +207,12 @@ public class RemoteTransitionCompat { @SuppressLint("NewApi") boolean merge(TransitionInfo info, SurfaceControl.Transaction t) { + if (info.getType() == TRANSIT_SLEEP) { + // A sleep event means we need to stop animations immediately, so cancel here. + mListener.onAnimationCanceled(new HashMap<>()); + finish(mWillFinishToHome, false /* userLeaveHint */); + return false; + } ArrayList<TransitionInfo.Change> openingTasks = null; ArrayList<TransitionInfo.Change> closingTasks = null; mAppearedTargets = null; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt index fe8b8c944d13..c98e9b40e7ab 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt @@ -40,7 +40,7 @@ data class KeyguardFaceListenModel( var keyguardGoingAway: Boolean = false, var listeningForFaceAssistant: Boolean = false, var occludingAppRequestingFaceAuth: Boolean = false, - val postureAllowsListening: Boolean = false, + var postureAllowsListening: Boolean = false, var primaryUser: Boolean = false, var secureCameraLaunched: Boolean = false, var supportsDetect: Boolean = false, @@ -70,6 +70,7 @@ data class KeyguardFaceListenModel( listeningForFaceAssistant.toString(), occludingAppRequestingFaceAuth.toString(), primaryUser.toString(), + postureAllowsListening.toString(), secureCameraLaunched.toString(), supportsDetect.toString(), switchingUser.toString(), @@ -109,6 +110,7 @@ data class KeyguardFaceListenModel( listeningForFaceAssistant = model.listeningForFaceAssistant occludingAppRequestingFaceAuth = model.occludingAppRequestingFaceAuth primaryUser = model.primaryUser + postureAllowsListening = model.postureAllowsListening secureCameraLaunched = model.secureCameraLaunched supportsDetect = model.supportsDetect switchingUser = model.switchingUser @@ -152,6 +154,7 @@ data class KeyguardFaceListenModel( "listeningForFaceAssistant", "occludingAppRequestingFaceAuth", "primaryUser", + "postureAllowsListening", "secureCameraLaunched", "supportsDetect", "switchingUser", diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 29496169e04f..66d5d097ab04 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -39,7 +39,6 @@ import static java.lang.Integer.max; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; @@ -1068,13 +1067,10 @@ public class KeyguardSecurityContainer extends ConstraintLayout { int yTranslation = mResources.getDimensionPixelSize(R.dimen.disappear_y_translation); - AnimatorSet anims = new AnimatorSet(); ObjectAnimator yAnim = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, yTranslation); - ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mView, View.ALPHA, 0f); - - anims.setInterpolator(Interpolators.STANDARD_ACCELERATE); - anims.playTogether(alphaAnim, yAnim); - anims.start(); + yAnim.setInterpolator(Interpolators.STANDARD_ACCELERATE); + yAnim.setDuration(500); + yAnim.start(); } private void setupUserSwitcher() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index f1abdc68f97e..06258b20e06f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -637,12 +637,17 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard public void startAppearAnimation() { if (mCurrentSecurityMode != SecurityMode.None) { - mView.setAlpha(1f); + setAlpha(1f); mView.startAppearAnimation(mCurrentSecurityMode); getCurrentSecurityController().startAppearAnimation(); } } + /** Set the alpha of the security container view */ + public void setAlpha(float alpha) { + mView.setAlpha(alpha); + } + public boolean startDisappearAnimation(Runnable onFinishRunnable) { boolean didRunAnimation = false; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 866b502e00ac..30e2a0b613ee 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -160,6 +160,7 @@ 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.statusbar.policy.DevicePostureController.DevicePostureInt; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.Assert; import com.android.systemui.util.settings.SecureSettings; @@ -368,7 +369,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final FaceManager mFaceManager; private final LockPatternUtils mLockPatternUtils; @VisibleForTesting - @DevicePostureController.DevicePostureInt + @DevicePostureInt protected int mConfigFaceAuthSupportedPosture; private KeyguardBypassController mKeyguardBypassController; @@ -876,7 +877,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private void reportSuccessfulBiometricUnlock(boolean isStrongBiometric, int userId) { mBackgroundExecutor.execute( - () -> mLockPatternUtils.reportSuccessfulBiometricUnlock(isStrongBiometric, userId)); + () -> { + mLogger.logReportSuccessfulBiometricUnlock(isStrongBiometric, userId); + mLockPatternUtils.reportSuccessfulBiometricUnlock(isStrongBiometric, userId); + }); } private void handleFingerprintAuthFailed() { @@ -1862,10 +1866,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final DevicePostureController.Callback mPostureCallback = new DevicePostureController.Callback() { @Override - public void onPostureChanged(int posture) { + public void onPostureChanged(@DevicePostureInt int posture) { + boolean currentPostureAllowsFaceAuth = doesPostureAllowFaceAuth(mPostureState); + boolean newPostureAllowsFaceAuth = doesPostureAllowFaceAuth(posture); mPostureState = posture; - updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, - FACE_AUTH_UPDATED_POSTURE_CHANGED); + if (currentPostureAllowsFaceAuth && !newPostureAllowsFaceAuth) { + mLogger.d("New posture does not allow face auth, stopping it"); + updateFaceListeningState(BIOMETRIC_ACTION_STOP, + FACE_AUTH_UPDATED_POSTURE_CHANGED); + } } }; @@ -2524,11 +2533,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // If this message exists, we should not authenticate again until this message is // consumed by the handler if (mHandler.hasMessages(MSG_BIOMETRIC_AUTHENTICATION_CONTINUE)) { + mLogger.logHandlerHasAuthContinueMsgs(action); return; } // don't start running fingerprint until they're registered if (!mAuthController.areAllFingerprintAuthenticatorsRegistered()) { + mLogger.d("All FP authenticators not registered, skipping FP listening state update"); return; } final boolean shouldListenForFingerprint = shouldListenForFingerprint(isUdfpsSupported()); @@ -2901,9 +2912,7 @@ 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); + final boolean isPostureAllowedForFaceAuth = doesPostureAllowFaceAuth(mPostureState); // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware. final boolean shouldListen = @@ -2952,6 +2961,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return shouldListen; } + private boolean doesPostureAllowFaceAuth(@DevicePostureInt int posture) { + return mConfigFaceAuthSupportedPosture == DEVICE_POSTURE_UNKNOWN + || (posture == mConfigFaceAuthSupportedPosture); + } + private void logListenerModelData(@NonNull KeyguardListenModel model) { mLogger.logKeyguardListenerModel(model); if (model instanceof KeyguardFingerprintListenModel) { diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt index bc0bd8c53d26..20f90072161b 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt @@ -16,6 +16,7 @@ package com.android.keyguard.logging +import android.hardware.biometrics.BiometricSourceType import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.dagger.BiometricLog import com.android.systemui.plugins.log.LogBuffer @@ -157,6 +158,36 @@ class BiometricUnlockLogger @Inject constructor(@BiometricLog private val logBuf } ) } + + fun deferringAuthenticationDueToSleep( + userId: Int, + biometricSourceType: BiometricSourceType, + alreadyPendingAuth: Boolean + ) { + logBuffer.log( + TAG, + DEBUG, + { + int1 = userId + str1 = biometricSourceType.name + bool2 = alreadyPendingAuth + }, + { + "onBiometricAuthenticated, deferring auth: userId: $int1, " + + "biometricSourceType: $str1, " + + "goingToSleep: true, " + + "mPendingAuthentication != null: $bool2" + } + ) + } + + fun finishedGoingToSleepWithPendingAuth() { + logBuffer.log( + TAG, + LogLevel.DEBUG, + "onFinishedGoingToSleep with pendingAuthenticated != null" + ) + } } private fun wakeAndUnlockModeToString(mode: Int): String { diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt index 379c78ad8d0e..51aca070b180 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -16,6 +16,7 @@ package com.android.keyguard.logging +import com.android.systemui.biometrics.AuthRippleController import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController import com.android.systemui.log.dagger.KeyguardLog import com.android.systemui.plugins.log.LogBuffer @@ -120,4 +121,29 @@ constructor( "type=${KeyguardIndicationRotateTextViewController.indicationTypeToString(type)}" } } + + fun notShowingUnlockRipple(keyguardNotShowing: Boolean, unlockNotAllowed: Boolean) { + buffer.log( + AuthRippleController.TAG, + LogLevel.DEBUG, + { + bool1 = keyguardNotShowing + bool2 = unlockNotAllowed + }, + { "Not showing unlock ripple: keyguardNotShowing: $bool1, unlockNotAllowed: $bool2" } + ) + } + + fun showingUnlockRippleAt(x: Int, y: Int, context: String) { + buffer.log( + AuthRippleController.TAG, + LogLevel.DEBUG, + { + int1 = x + int2 = y + str1 = context + }, + { "Showing unlock ripple with center (x, y): ($int1, $int2), context: $str1" } + ) + } } diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index fb2c02ad8c48..2403d1116360 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -584,6 +584,30 @@ constructor(@KeyguardUpdateMonitorLog private val logBuffer: LogBuffer) { ) } + fun logReportSuccessfulBiometricUnlock(isStrongBiometric: Boolean, userId: Int) { + logBuffer.log( + TAG, + DEBUG, + { + bool1 = isStrongBiometric + int1 = userId + }, + { "reporting successful biometric unlock: isStrongBiometric: $bool1, userId: $int1" } + ) + } + + fun logHandlerHasAuthContinueMsgs(action: Int) { + logBuffer.log( + TAG, + DEBUG, + { int1 = action }, + { + "MSG_BIOMETRIC_AUTHENTICATION_CONTINUE already queued up, " + + "ignoring updating FP listening state to $int1" + } + ) + } + fun logFaceEnrolledUpdated(oldValue: Boolean, newValue: Boolean) { logBuffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index b111e1f72f1d..d811d30b5693 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -37,6 +37,8 @@ import android.content.res.Resources; import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; @@ -1398,6 +1400,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mWindowMagnifierCallback.onPerformScaleAction(mDisplayId, A11Y_ACTION_SCALE_RANGE.clamp(scale)); } + + @Override + public void onSettingsPanelVisibilityChanged(boolean shown) { + updateDragHandleResourcesIfNeeded(/* settingsPanelIsShown= */ shown); + } }; @Override @@ -1436,6 +1443,20 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } } + private void updateDragHandleResourcesIfNeeded(boolean settingsPanelIsShown) { + mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown + ? R.drawable.accessibility_window_magnification_drag_handle_background_change + : R.drawable.accessibility_window_magnification_drag_handle_background)); + + PorterDuffColorFilter filter = new PorterDuffColorFilter( + mContext.getColor(settingsPanelIsShown + ? R.color.magnification_border_color + : R.color.magnification_drag_handle_stroke), + PorterDuff.Mode.SRC_ATOP); + + mDragView.setColorFilter(filter); + } + private void animateBounceEffect() { final ObjectAnimator scaleAnimator = ObjectAnimator.ofPropertyValuesHolder(mMirrorView, PropertyValuesHolder.ofFloat(View.SCALE_X, 1, mBounceEffectAnimationScale, 1), @@ -1468,7 +1489,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold super.onInitializeAccessibilityNodeInfo(host, info); final AccessibilityAction clickAction = new AccessibilityAction( AccessibilityAction.ACTION_CLICK.getId(), mContext.getResources().getString( - R.string.magnification_mode_switch_click_label)); + R.string.magnification_open_settings_click_label)); info.addAction(clickAction); info.setClickable(true); info.addAction( diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index 15264e64a241..9ad64e293fe5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -215,9 +215,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private boolean performA11yAction(View view, int action) { final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); - if (action == AccessibilityAction.ACTION_CLICK.getId()) { - handleSingleTap(view); - } else if (action == R.id.accessibility_action_move_up) { + if (action == R.id.accessibility_action_move_up) { moveButton(0, -windowBounds.height()); } else if (action == R.id.accessibility_action_move_down) { moveButton(0, windowBounds.height()); @@ -264,8 +262,6 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest } else if (id == R.id.magnifier_full_button) { hideSettingPanel(); toggleMagnificationMode(); - } else { - hideSettingPanel(); } } }; @@ -273,7 +269,6 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest @Override public boolean onSingleTap(View view) { mSingleTapDetected = true; - handleSingleTap(view); return true; } @@ -328,6 +323,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest } mContext.unregisterReceiver(mScreenOffReceiver); + mCallback.onSettingsPanelVisibilityChanged(/* shown= */ false); } public void showSettingPanel() { @@ -358,10 +354,15 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest } mWindowManager.addView(mSettingView, mParams); + if (resetPosition) { + // Request focus on the settings panel when position of the panel is reset. + mSettingView.requestFocus(); + } // Exclude magnification switch button from system gesture area. setSystemGestureExclusion(); mIsVisible = true; + mCallback.onSettingsPanelVisibilityChanged(/* shown= */ true); } mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); } @@ -385,8 +386,8 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mSettingView = (LinearLayout) View.inflate(mContext, R.layout.window_magnification_settings_view, null); - mSettingView.setClickable(true); mSettingView.setFocusable(true); + mSettingView.setFocusableInTouchMode(true); mSettingView.setOnTouchListener(this::onTouch); mPanelView = mSettingView.findViewById(R.id.magnifier_panel_view); @@ -499,22 +500,6 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest } } - private void handleSingleTap(View view) { - int id = view.getId(); - if (id == R.id.magnifier_small_button) { - setMagnifierSize(MagnificationSize.SMALL); - } else if (id == R.id.magnifier_medium_button) { - setMagnifierSize(MagnificationSize.MEDIUM); - } else if (id == R.id.magnifier_large_button) { - setMagnifierSize(MagnificationSize.LARGE); - } else if (id == R.id.magnifier_full_button) { - hideSettingPanel(); - toggleMagnificationMode(); - } else { - hideSettingPanel(); - } - } - public void editMagnifierSizeMode(boolean enable) { setEditMagnifierSizeMode(enable); updateSelectedButton(MagnificationSize.NONE); @@ -551,7 +536,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, - LayoutParams.FLAG_NOT_FOCUSABLE, + /* _flags= */ 0, PixelFormat.TRANSPARENT); params.gravity = Gravity.TOP | Gravity.START; params.accessibilityTitle = getAccessibilityWindowTitle(context); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettingsCallback.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettingsCallback.java index 22ec65001101..1d833402b1f4 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettingsCallback.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettingsCallback.java @@ -61,4 +61,11 @@ public interface WindowMagnificationSettingsCallback { * 1 : ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN, 2 : ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW */ void onModeSwitch(int newMode); + + /** + * Called when the visibility of the magnification settings panel changed. + * + * @param shown The visibility of the magnification settings panel. + */ + void onSettingsPanelVisibilityChanged(boolean shown); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt index 54f933ae6d09..53a421d9eccc 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt @@ -29,6 +29,7 @@ import com.android.systemui.R import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.settings.SystemSettings +import kotlin.math.roundToInt /** The Dialog that contains a seekbar for changing the font size. */ class FontScalingDialog(context: Context, private val systemSettings: SystemSettings) : @@ -56,6 +57,16 @@ class FontScalingDialog(context: Context, private val systemSettings: SystemSett doneButton = requireViewById(com.android.internal.R.id.button1) seekBarWithIconButtonsView = requireViewById(R.id.font_scaling_slider) + val labelArray = arrayOfNulls<String>(strEntryValues.size) + for (i in strEntryValues.indices) { + labelArray[i] = + context.resources.getString( + com.android.settingslib.R.string.font_scale_percentage, + (strEntryValues[i].toFloat() * 100).roundToInt() + ) + } + seekBarWithIconButtonsView.setProgressStateLabels(labelArray) + seekBarWithIconButtonsView.setMax((strEntryValues).size - 1) val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, 1.0f) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index d561cd7af7f0..93b57dc127dd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -26,6 +26,7 @@ import android.hardware.biometrics.BiometricSourceType import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.keyguard.logging.KeyguardLogger import com.android.settingslib.Utils import com.android.systemui.R import com.android.systemui.animation.Interpolators @@ -74,6 +75,7 @@ class AuthRippleController @Inject constructor( private val udfpsControllerProvider: Provider<UdfpsController>, private val statusBarStateController: StatusBarStateController, private val featureFlags: FeatureFlags, + private val logger: KeyguardLogger, rippleView: AuthRippleView? ) : ViewController<AuthRippleView>(rippleView), KeyguardStateController.Callback, WakefulnessLifecycle.Observer { @@ -120,8 +122,11 @@ class AuthRippleController @Inject constructor( } fun showUnlockRipple(biometricSourceType: BiometricSourceType) { - if (!keyguardStateController.isShowing || - !keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(biometricSourceType)) { + val keyguardNotShowing = !keyguardStateController.isShowing + val unlockNotAllowed = !keyguardUpdateMonitor + .isUnlockingWithBiometricAllowed(biometricSourceType) + if (keyguardNotShowing || unlockNotAllowed) { + logger.notShowingUnlockRipple(keyguardNotShowing, unlockNotAllowed) return } @@ -138,6 +143,7 @@ class AuthRippleController @Inject constructor( Math.max(it.y, centralSurfaces.displayHeight.toInt() - it.y) ) ) + logger.showingUnlockRippleAt(it.x, it.y, "FP sensor radius: $udfpsRadius") showUnlockedRipple() } } else if (biometricSourceType == BiometricSourceType.FACE) { @@ -155,6 +161,7 @@ class AuthRippleController @Inject constructor( Math.max(it.y, centralSurfaces.displayHeight.toInt() - it.y) ) ) + logger.showingUnlockRippleAt(it.x, it.y, "Face unlock ripple") showUnlockedRipple() } } @@ -391,5 +398,6 @@ class AuthRippleController @Inject constructor( companion object { const val RIPPLE_ANIMATION_DURATION: Long = 1533 + const val TAG = "AuthRippleController" } } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java index bc0f9950f865..f83885b7bb32 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java @@ -38,6 +38,7 @@ import javax.inject.Inject; public class FalsingDataProvider { private static final long MOTION_EVENT_AGE_MS = 1000; + private static final long DROP_EVENT_THRESHOLD_MS = 50; private static final float THREE_HUNDRED_SIXTY_DEG = (float) (2 * Math.PI); private final int mWidthPixels; @@ -60,6 +61,7 @@ public class FalsingDataProvider { private float mAngle = 0; private MotionEvent mFirstRecentMotionEvent; private MotionEvent mLastMotionEvent; + private boolean mDropLastEvent; private boolean mJustUnlockedWithFace; private boolean mA11YAction; @@ -95,6 +97,12 @@ public class FalsingDataProvider { // Ensure prior gesture was completed. May be a no-op. completePriorGesture(); } + + // Drop the gesture closing event if it is close in time to a previous ACTION_MOVE event. + // The reason is that the closing ACTION_UP event of a swipe can be a bit offseted from the + // previous ACTION_MOVE event and when it happens, it makes some classifiers fail. + mDropLastEvent = shouldDropEvent(motionEvent); + mRecentMotionEvents.addAll(motionEvents); FalsingClassifier.logVerbose("Size: " + mRecentMotionEvents.size()); @@ -129,6 +137,7 @@ public class FalsingDataProvider { mPriorMotionEvents = mRecentMotionEvents; mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); } + mDropLastEvent = false; mA11YAction = false; } @@ -150,8 +159,18 @@ public class FalsingDataProvider { return mYdpi; } + /** + * Get the {@link MotionEvent}s of the most recent gesture. + * + * Note that this list may not include the last recorded event. + * @see #mDropLastEvent + */ public List<MotionEvent> getRecentMotionEvents() { - return mRecentMotionEvents; + if (!mDropLastEvent || mRecentMotionEvents.isEmpty()) { + return mRecentMotionEvents; + } else { + return mRecentMotionEvents.subList(0, mRecentMotionEvents.size() - 1); + } } public List<MotionEvent> getPriorMotionEvents() { @@ -169,7 +188,12 @@ public class FalsingDataProvider { return mFirstRecentMotionEvent; } - /** Get the last recorded {@link MotionEvent}. */ + /** + * Get the last {@link MotionEvent} of the most recent gesture. + * + * Note that this may be the event prior to the last recorded event. + * @see #mDropLastEvent + */ public MotionEvent getLastMotionEvent() { recalculateData(); return mLastMotionEvent; @@ -236,12 +260,13 @@ public class FalsingDataProvider { return; } - if (mRecentMotionEvents.isEmpty()) { + List<MotionEvent> recentMotionEvents = getRecentMotionEvents(); + if (recentMotionEvents.isEmpty()) { mFirstRecentMotionEvent = null; mLastMotionEvent = null; } else { - mFirstRecentMotionEvent = mRecentMotionEvents.get(0); - mLastMotionEvent = mRecentMotionEvents.get(mRecentMotionEvents.size() - 1); + mFirstRecentMotionEvent = recentMotionEvents.get(0); + mLastMotionEvent = recentMotionEvents.get(recentMotionEvents.size() - 1); } calculateAngleInternal(); @@ -249,6 +274,17 @@ public class FalsingDataProvider { mDirty = false; } + private boolean shouldDropEvent(MotionEvent event) { + if (mRecentMotionEvents.size() < 3) return false; + + MotionEvent lastEvent = mRecentMotionEvents.get(mRecentMotionEvents.size() - 1); + boolean isCompletingGesture = event.getActionMasked() == MotionEvent.ACTION_UP + && lastEvent.getActionMasked() == MotionEvent.ACTION_MOVE; + boolean isRecentEvent = + event.getEventTime() - lastEvent.getEventTime() < DROP_EVENT_THRESHOLD_MS; + return isCompletingGesture && isRecentEvent; + } + private void calculateAngleInternal() { if (mRecentMotionEvents.size() < 2) { mAngle = Float.MAX_VALUE; diff --git a/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedMotionEventBuffer.java b/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedMotionEventBuffer.java index 4773f2a3b13e..51aede79b581 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedMotionEventBuffer.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedMotionEventBuffer.java @@ -183,7 +183,7 @@ public class TimeLimitedMotionEventBuffer implements List<MotionEvent> { @Override public List<MotionEvent> subList(int fromIndex, int toIndex) { - throw new UnsupportedOperationException(); + return mMotionEvents.subList(fromIndex, toIndex); } class Iter implements ListIterator<MotionEvent> { diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index c214f5341450..e049ae09b1de 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -263,10 +263,11 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv @Override // ClipboardListener.ClipboardOverlay public void setClipData(ClipData data, String source) { ClipboardModel model = ClipboardModel.fromClipData(mContext, mClipboardUtils, data, source); - if (mExitAnimator != null && mExitAnimator.isRunning()) { + boolean wasExiting = (mExitAnimator != null && mExitAnimator.isRunning()); + if (wasExiting) { mExitAnimator.cancel(); } - boolean shouldAnimate = !model.dataMatches(mClipboardModel); + boolean shouldAnimate = !model.dataMatches(mClipboardModel) || wasExiting; mClipboardModel = model; mClipboardLogger.setClipSource(mClipboardModel.getSource()); if (shouldAnimate) { @@ -313,15 +314,19 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv mOnPreviewTapped = this::editText; break; case IMAGE: - if (model.isSensitive() || model.loadThumbnail(mContext) != null) { - mView.showImagePreview( - model.isSensitive() ? null : model.loadThumbnail(mContext)); - mView.setEditAccessibilityAction(true); - mOnPreviewTapped = () -> editImage(model.getUri()); - } else { - // image loading failed - mView.showDefaultTextPreview(); - } + mBgExecutor.execute(() -> { + if (model.isSensitive() || model.loadThumbnail(mContext) != null) { + mView.post(() -> { + mView.showImagePreview( + model.isSensitive() ? null : model.loadThumbnail(mContext)); + mView.setEditAccessibilityAction(true); + }); + mOnPreviewTapped = () -> editImage(model.getUri()); + } else { + // image loading failed + mView.post(mView::showDefaultTextPreview); + } + }); break; case URI: case OTHER: @@ -346,9 +351,20 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv } private void animateFromMinimized() { - mIsMinimized = false; - setExpandedView(); - animateIn(); + if (mEnterAnimator != null && mEnterAnimator.isRunning()) { + mEnterAnimator.cancel(); + } + mEnterAnimator = mView.getMinimizedFadeoutAnimation(); + mEnterAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mIsMinimized = false; + setExpandedView(); + animateIn(); + } + }); + mEnterAnimator.start(); } private String getAccessibilityAnnouncement(ClipboardModel.Type type) { @@ -363,15 +379,15 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv private void classifyText(ClipboardModel model) { mBgExecutor.execute(() -> { - Optional<RemoteAction> remoteAction = mClipboardUtils.getAction( - model.getText(), model.getTextLinks(), model.getSource()); + Optional<RemoteAction> remoteAction = + mClipboardUtils.getAction(model.getTextLinks(), model.getSource()); if (model.equals(mClipboardModel)) { remoteAction.ifPresent(action -> { mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN); - mView.setActionChip(action, () -> { + mView.post(() -> mView.setActionChip(action, () -> { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); animateOut(); - }); + })); }); } }); diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java index a85f8b9357f5..25caaeac2c38 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java @@ -39,6 +39,9 @@ import javax.inject.Inject; class ClipboardOverlayUtils { + // minimum proportion of entire text an entity must take up, to be considered for smart actions + private static final float MINIMUM_ENTITY_PROPORTION = .8f; + private final TextClassifier mTextClassifier; @Inject @@ -65,19 +68,23 @@ class ClipboardOverlayUtils { return false; } - public Optional<RemoteAction> getAction(CharSequence text, TextLinks textLinks, String source) { - return getActions(text, textLinks).stream().filter(remoteAction -> { + public Optional<RemoteAction> getAction(TextLinks textLinks, String source) { + return getActions(textLinks).stream().filter(remoteAction -> { ComponentName component = remoteAction.getActionIntent().getIntent().getComponent(); return component != null && !TextUtils.equals(source, component.getPackageName()); }).findFirst(); } - private ArrayList<RemoteAction> getActions(CharSequence text, TextLinks textLinks) { + private ArrayList<RemoteAction> getActions(TextLinks textLinks) { ArrayList<RemoteAction> actions = new ArrayList<>(); for (TextLinks.TextLink link : textLinks.getLinks()) { - TextClassification classification = mTextClassifier.classifyText( - text, link.getStart(), link.getEnd(), null); - actions.addAll(classification.getActions()); + // skip classification for incidental entities + if (link.getEnd() - link.getStart() + >= textLinks.getText().length() * MINIMUM_ENTITY_PROPORTION) { + TextClassification classification = mTextClassifier.classifyText( + textLinks.getText(), link.getStart(), link.getEnd(), null); + actions.addAll(classification.getActions()); + } } return actions; } @@ -92,9 +99,13 @@ class ClipboardOverlayUtils { private ArrayList<RemoteAction> getActions(ClipData.Item item) { ArrayList<RemoteAction> actions = new ArrayList<>(); for (TextLinks.TextLink link : item.getTextLinks().getLinks()) { - TextClassification classification = mTextClassifier.classifyText( - item.getText(), link.getStart(), link.getEnd(), null); - actions.addAll(classification.getActions()); + // skip classification for incidental entities + if (link.getEnd() - link.getStart() + >= item.getText().length() * MINIMUM_ENTITY_PROPORTION) { + TextClassification classification = mTextClassifier.classifyText( + item.getText(), link.getStart(), link.getEnd(), null); + actions.addAll(classification.getActions()); + } } return actions; } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java index f372bb4bc7f2..28c57d31a4f3 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java @@ -21,6 +21,7 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.Nullable; @@ -286,6 +287,20 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { mActionChips.clear(); } + Animator getMinimizedFadeoutAnimation() { + ObjectAnimator anim = ObjectAnimator.ofFloat(mMinimizedPreview, "alpha", 1, 0); + anim.setDuration(66); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mMinimizedPreview.setVisibility(View.GONE); + mMinimizedPreview.setAlpha(1); + } + }); + return anim; + } + Animator getEnterAnimation() { if (mAccessibilityManager.isEnabled()) { mDismissButton.setVisibility(View.VISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java index 24f6296de122..de3a9901b8c4 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java @@ -45,6 +45,7 @@ public class SeekBarWithIconButtonsView extends LinearLayout { private SeekBar mSeekbar; private SeekBarChangeListener mSeekBarListener = new SeekBarChangeListener(); + private String[] mStateLabels = null; public SeekBarWithIconButtonsView(Context context) { this(context, null); @@ -132,6 +133,30 @@ public class SeekBarWithIconButtonsView extends LinearLayout { } /** + * Stores the String array we would like to use for describing the state of seekbar progress + * and updates the state description with current progress. + * + * @param labels The state descriptions to be announced for each progress. + */ + public void setProgressStateLabels(String[] labels) { + mStateLabels = labels; + if (mStateLabels != null) { + setSeekbarStateDescription(); + } + } + + /** + * Sets the state of seekbar based on current progress. The progress of seekbar is + * corresponding to the index of the string array. If the progress is larger than or equals + * to the length of the array, the state description is set to an empty string. + */ + private void setSeekbarStateDescription() { + mSeekbar.setStateDescription( + (mSeekbar.getProgress() < mStateLabels.length) + ? mStateLabels[mSeekbar.getProgress()] : ""); + } + + /** * Sets a onSeekbarChangeListener to the seekbar in the layout. * We update the Start Icon and End Icon if needed when the seekbar progress is changed. */ @@ -173,6 +198,9 @@ public class SeekBarWithIconButtonsView extends LinearLayout { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (mStateLabels != null) { + setSeekbarStateDescription(); + } if (mOnSeekBarChangeListener != null) { mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java index a5beb4e85058..3cf26b381d7d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java @@ -23,15 +23,16 @@ import com.android.systemui.hdmi.HdmiCecSetMenuLanguageActivity; import com.android.systemui.keyguard.WorkLockActivity; import com.android.systemui.people.PeopleSpaceActivity; import com.android.systemui.people.widget.LaunchConversationActivity; -import com.android.systemui.screenshot.AppClipsActivity; -import com.android.systemui.screenshot.AppClipsTrampolineActivity; import com.android.systemui.screenshot.LongScreenshotActivity; +import com.android.systemui.screenshot.appclips.AppClipsActivity; +import com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity; import com.android.systemui.sensorprivacy.SensorUseStartedActivity; import com.android.systemui.sensorprivacy.television.TvSensorPrivacyChangedActivity; import com.android.systemui.sensorprivacy.television.TvUnblockSensorActivity; import com.android.systemui.settings.brightness.BrightnessDialog; import com.android.systemui.statusbar.tv.notifications.TvNotificationPanelActivity; import com.android.systemui.tuner.TunerActivity; +import com.android.systemui.usb.UsbAccessoryUriActivity; import com.android.systemui.usb.UsbConfirmActivity; import com.android.systemui.usb.UsbDebuggingActivity; import com.android.systemui.usb.UsbDebuggingSecondaryUserActivity; @@ -97,6 +98,12 @@ public abstract class DefaultActivityBinder { @ClassKey(UsbConfirmActivity.class) public abstract Activity bindUsbConfirmActivity(UsbConfirmActivity activity); + /** Inject into UsbAccessoryUriActivity. */ + @Binds + @IntoMap + @ClassKey(UsbAccessoryUriActivity.class) + public abstract Activity bindUsbAccessoryUriActivity(UsbAccessoryUriActivity activity); + /** Inject into CreateUserActivity. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index ca1cef385755..d0a92f0846d0 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -43,7 +43,6 @@ import com.android.systemui.util.concurrency.DelayableExecutor import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch @@ -131,9 +130,17 @@ constructor( } } - /** Starts the dream content and dream overlay entry animations. */ + /** + * Starts the dream content and dream overlay entry animations. + * + * @param downwards if true, the entry animation translations downwards into position rather + * than upwards. + */ @JvmOverloads - fun startEntryAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }) { + fun startEntryAnimations( + downwards: Boolean, + animatorBuilder: () -> AnimatorSet = { AnimatorSet() } + ) { cancelAnimations() mAnimator = @@ -153,7 +160,7 @@ constructor( interpolator = Interpolators.LINEAR ), translationYAnimator( - from = mDreamInTranslationYDistance.toFloat(), + from = mDreamInTranslationYDistance.toFloat() * (if (downwards) -1 else 1), to = 0f, durationMs = mDreamInTranslationYDurationMs, interpolator = Interpolators.EMPHASIZED_DECELERATE @@ -167,6 +174,71 @@ constructor( } } + /** + * Starts the dream content and dream overlay exit animations. + * + * This should only be used when the low light dream is entering, animations to/from other SysUI + * views is controlled by `transitionViewModel`. + */ + // TODO(b/256916668): integrate with the keyguard transition model once dream surfaces work is + // done. + @JvmOverloads + fun startExitAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }): Animator { + cancelAnimations() + + mAnimator = + animatorBuilder().apply { + playTogether( + translationYAnimator( + from = 0f, + to = -mDreamInTranslationYDistance.toFloat(), + durationMs = mDreamInTranslationYDurationMs, + delayMs = 0, + interpolator = Interpolators.EMPHASIZED + ), + alphaAnimator( + from = + mCurrentAlphaAtPosition.getOrDefault( + key = POSITION_BOTTOM, + defaultValue = 1f + ), + to = 0f, + durationMs = mDreamInComplicationsAnimDurationMs, + delayMs = 0, + positions = POSITION_BOTTOM + ) + .apply { + doOnEnd { + // The logical end of the animation is once the alpha and blur + // animations finish, end the animation so that any listeners are + // notified. The Y translation animation is much longer than all of + // the other animations due to how the spec is defined, but is not + // expected to run to completion. + mAnimator?.end() + } + }, + alphaAnimator( + from = + mCurrentAlphaAtPosition.getOrDefault( + key = POSITION_TOP, + defaultValue = 1f + ), + to = 0f, + durationMs = mDreamInComplicationsAnimDurationMs, + delayMs = 0, + positions = POSITION_TOP + ) + ) + doOnEnd { + mAnimator = null + mOverlayStateController.setExitAnimationsRunning(false) + } + start() + } + mOverlayStateController.setExitAnimationsRunning(true) + return mAnimator as AnimatorSet + } + /** Starts the dream content and dream overlay exit animations. */ fun wakeUp(doneCallback: Runnable, executor: DelayableExecutor) { cancelAnimations() @@ -182,19 +254,6 @@ constructor( } } - /** - * Ends the dream content and dream overlay animations, if they're currently running. - * - * @see [AnimatorSet.end] - */ - fun endAnimations() { - mAnimator = - mAnimator?.let { - it.end() - null - } - } - private fun blurAnimator( view: View, fromBlurRadius: Float, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 50cfb6a905c9..4b478cdca9f9 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -23,6 +23,7 @@ import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; import static com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_BOTTOM; import static com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_TOP; +import android.animation.Animator; import android.content.res.Resources; import android.os.Handler; import android.util.MathUtils; @@ -31,6 +32,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; +import com.android.dream.lowlight.LowLightTransitionCoordinator; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.dagger.qualifiers.Main; @@ -54,11 +56,14 @@ import javax.inject.Named; * View controller for {@link DreamOverlayContainerView}. */ @DreamOverlayComponent.DreamOverlayScope -public class DreamOverlayContainerViewController extends ViewController<DreamOverlayContainerView> { +public class DreamOverlayContainerViewController extends + ViewController<DreamOverlayContainerView> implements + LowLightTransitionCoordinator.LowLightEnterListener { private final DreamOverlayStatusBarViewController mStatusBarViewController; private final BlurUtils mBlurUtils; private final DreamOverlayAnimationsController mDreamOverlayAnimationsController; private final DreamOverlayStateController mStateController; + private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; private final ComplicationHostViewController mComplicationHostViewController; @@ -143,19 +148,18 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve }; /** - * If true, overlay entry animations should be skipped once. - * - * This is turned on when exiting low light and should be turned off once the entry animations - * are skipped once. + * If {@code true}, the dream has just transitioned from the low light dream back to the user + * dream and we should play an entry animation where the overlay slides in downwards from the + * top instead of the typicla slide in upwards from the bottom. */ - private boolean mSkipEntryAnimations; + private boolean mExitingLowLight; private final DreamOverlayStateController.Callback mDreamOverlayStateCallback = new DreamOverlayStateController.Callback() { @Override public void onExitLowLight() { - mSkipEntryAnimations = true; + mExitingLowLight = true; } }; @@ -165,6 +169,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve ComplicationHostViewController complicationHostViewController, @Named(DreamOverlayModule.DREAM_OVERLAY_CONTENT_VIEW) ViewGroup contentView, DreamOverlayStatusBarViewController statusBarViewController, + LowLightTransitionCoordinator lowLightTransitionCoordinator, BlurUtils blurUtils, @Main Handler handler, @Main Resources resources, @@ -182,6 +187,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve mBlurUtils = blurUtils; mDreamOverlayAnimationsController = animationsController; mStateController = stateController; + mLowLightTransitionCoordinator = lowLightTransitionCoordinator; mBouncerlessScrimController = bouncerlessScrimController; mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback); @@ -208,6 +214,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve mStatusBarViewController.init(); mComplicationHostViewController.init(); mDreamOverlayAnimationsController.init(mView); + mLowLightTransitionCoordinator.setLowLightEnterListener(this); } @Override @@ -219,14 +226,10 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve // Start dream entry animations. Skip animations for low light clock. if (!mStateController.isLowLightActive()) { - mDreamOverlayAnimationsController.startEntryAnimations(); - - if (mSkipEntryAnimations) { - // If we're transitioning from the low light dream back to the user dream, skip the - // overlay animations and show immediately. - mDreamOverlayAnimationsController.endAnimations(); - mSkipEntryAnimations = false; - } + // If this is transitioning from the low light dream to the user dream, the overlay + // should translate in downwards instead of upwards. + mDreamOverlayAnimationsController.startEntryAnimations(mExitingLowLight); + mExitingLowLight = false; } } @@ -310,4 +313,12 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve mDreamOverlayAnimationsController.wakeUp(onAnimationEnd, callbackExecutor); } + + @Override + public Animator onBeforeEnterLowLight() { + // Return the animator so that the transition coordinator waits for the overlay exit + // animations to finish before entering low light, as otherwise the default DreamActivity + // animation plays immediately and there's no time for this animation to play. + return mDreamOverlayAnimationsController.startExitAnimations(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java index a2e11b21ea59..24e90f066622 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java @@ -22,14 +22,18 @@ import static com.android.systemui.dreams.complication.dagger.ComplicationModule import android.graphics.Rect; import android.graphics.Region; import android.os.Debug; +import android.os.UserHandle; +import android.provider.Settings; import android.util.Log; import android.view.View; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.lifecycle.LifecycleOwner; +import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.util.ViewController; +import com.android.systemui.util.settings.SecureSettings; import java.util.Collection; import java.util.HashMap; @@ -54,6 +58,8 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay private final LifecycleOwner mLifecycleOwner; private final ComplicationCollectionViewModel mComplicationCollectionViewModel; private final HashMap<ComplicationId, Complication.ViewHolder> mComplications = new HashMap<>(); + @VisibleForTesting + boolean mIsAnimationEnabled; // Whether dream entry animations are finished. private boolean mEntryAnimationsFinished = false; @@ -64,7 +70,8 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay ComplicationLayoutEngine layoutEngine, DreamOverlayStateController dreamOverlayStateController, LifecycleOwner lifecycleOwner, - @Named(SCOPED_COMPLICATIONS_MODEL) ComplicationCollectionViewModel viewModel) { + @Named(SCOPED_COMPLICATIONS_MODEL) ComplicationCollectionViewModel viewModel, + SecureSettings secureSettings) { super(view); mLayoutEngine = layoutEngine; mLifecycleOwner = lifecycleOwner; @@ -78,6 +85,10 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay mDreamOverlayStateController.areEntryAnimationsFinished(); } }); + + // Whether animations are enabled. + mIsAnimationEnabled = secureSettings.getFloatForUser( + Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f, UserHandle.USER_CURRENT) != 0.0f; } @Override @@ -148,7 +159,7 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay // Complications to be added before dream entry animations are finished are set // to invisible and are animated in. - if (!mEntryAnimationsFinished) { + if (!mEntryAnimationsFinished && mIsAnimationEnabled) { view.setVisibility(View.INVISIBLE); } mComplications.put(id, viewHolder); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java index 73c2289ad6bd..a7b3bbcbc37b 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java @@ -254,7 +254,10 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { mCurrentScrimController = mScrimManager.getCurrentController(); session.registerCallback(() -> { - mVelocityTracker.recycle(); + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } mScrimManager.removeCallback(mScrimManagerCallback); mCapture = null; mNotificationShadeWindowController.setForcePluginOpen(false, this); diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 3608b91aa91a..9a6d90a9a88c 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -71,8 +71,12 @@ object Flags { val NOTIFICATION_MEMORY_MONITOR_ENABLED = releasedFlag(112, "notification_memory_monitor_enabled") + /** + * This flag is server-controlled and should stay as [unreleasedFlag] since we never want to + * enable it on release builds. + */ val NOTIFICATION_MEMORY_LOGGING_ENABLED = - unreleasedFlag(119, "notification_memory_logging_enabled", teamfood = true) + unreleasedFlag(119, "notification_memory_logging_enabled") // TODO(b/254512731): Tracking Bug @JvmField val NOTIFICATION_DISMISSAL_FADE = releasedFlag(113, "notification_dismissal_fade") @@ -104,6 +108,10 @@ object Flags { val NOTIFICATION_ANIMATE_BIG_PICTURE = releasedFlag(120, "notification_animate_big_picture", teamfood = true) + @JvmField + val ANIMATED_NOTIFICATION_SHADE_INSETS = + unreleasedFlag(270682168, "animated_notification_shade_insets", teamfood = true) + // 200 - keyguard/lockscreen // ** Flag retired ** // public static final BooleanFlag KEYGUARD_LAYOUT = @@ -438,7 +446,9 @@ object Flags { ) // TODO(b/256873975): Tracking Bug - @JvmField @Keep val WM_BUBBLE_BAR = unreleasedFlag(1111, "wm_bubble_bar") + @JvmField + @Keep + val WM_BUBBLE_BAR = sysPropBooleanFlag(1111, "persist.wm.debug.bubble_bar", default = false) // TODO(b/260271148): Tracking bug @Keep @@ -461,13 +471,13 @@ object Flags { @Keep @JvmField val ENABLE_PIP_SIZE_LARGE_SCREEN = - sysPropBooleanFlag(1114, "persist.wm.debug.enable_pip_size_large_screen", default = false) + sysPropBooleanFlag(1114, "persist.wm.debug.enable_pip_size_large_screen", default = true) // TODO(b/265998256): Tracking bug @Keep @JvmField val ENABLE_PIP_APP_ICON_OVERLAY = - sysPropBooleanFlag(1115, "persist.wm.debug.enable_pip_app_icon_overlay", default = false) + sysPropBooleanFlag(1115, "persist.wm.debug.enable_pip_app_icon_overlay", default = true) // 1200 - predictive back @Keep diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt index b0f9c4edb073..d078688e5944 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt @@ -21,6 +21,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyboard.backlight.ui.KeyboardBacklightDialogCoordinator import javax.inject.Inject /** A [CoreStartable] that launches components interested in physical keyboard interaction. */ @@ -28,11 +29,12 @@ import javax.inject.Inject class PhysicalKeyboardCoreStartable @Inject constructor( + private val keyboardBacklightDialogCoordinator: KeyboardBacklightDialogCoordinator, private val featureFlags: FeatureFlags, ) : CoreStartable { override fun start() { if (featureFlags.isEnabled(Flags.KEYBOARD_BACKLIGHT_INDICATOR)) { - // TODO(b/268645743) start listening for keyboard backlight brightness + keyboardBacklightDialogCoordinator.startListening() } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt new file mode 100644 index 000000000000..65e70b319923 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt @@ -0,0 +1,41 @@ +/* + * 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.keyboard.backlight.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.data.repository.KeyboardRepository +import com.android.systemui.keyboard.shared.model.BacklightModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +/** Allows listening to changes to keyboard backlight level */ +@SysUISingleton +class KeyboardBacklightInteractor +@Inject +constructor( + private val keyboardRepository: KeyboardRepository, +) { + + /** Emits current backlight level as [BacklightModel] or null if keyboard is not connected */ + val backlight: Flow<BacklightModel?> = + keyboardRepository.keyboardConnected.flatMapLatest { connected -> + if (connected) keyboardRepository.backlight else flowOf(null) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt new file mode 100644 index 000000000000..85d0379a77db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt @@ -0,0 +1,59 @@ +/* + * 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.keyboard.backlight.ui + +import android.content.Context +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyboard.backlight.ui.view.KeyboardBacklightDialog +import com.android.systemui.keyboard.backlight.ui.viewmodel.BacklightDialogViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Based on the state produced from [BacklightDialogViewModel] shows or hides keyboard backlight + * indicator + */ +@SysUISingleton +class KeyboardBacklightDialogCoordinator +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val context: Context, + private val viewModel: BacklightDialogViewModel, +) { + + var dialog: KeyboardBacklightDialog? = null + + fun startListening() { + applicationScope.launch { + viewModel.dialogContent.collect { dialogViewModel -> + if (dialogViewModel != null) { + if (dialog == null) { + dialog = KeyboardBacklightDialog(context, dialogViewModel) + // pass viewModel and show + } + } else { + dialog?.dismiss() + dialog = null + } + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ColorScheme.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt index b2489fd57b5d..b68a2a84b5d1 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ColorScheme.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt @@ -12,19 +12,21 @@ * 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.common.ui +package com.android.systemui.keyboard.backlight.ui.view + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import com.android.systemui.keyboard.backlight.ui.viewmodel.BacklightDialogContentViewModel -import androidx.compose.material3.ColorScheme -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import kotlin.math.ln +class KeyboardBacklightDialog(context: Context, val viewModel: BacklightDialogContentViewModel) : + Dialog(context) { -fun ColorScheme.surfaceColorAtElevation(elevation: Dp): Color { - if (elevation == 0.dp) return surface - val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f - return surfaceTint.copy(alpha = alpha).compositeOver(surface) -}
\ No newline at end of file + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // TODO(b/268650355) Implement the dialog + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogContentViewModel.kt new file mode 100644 index 000000000000..3ef0ca39b8f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogContentViewModel.kt @@ -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. + * + */ + +package com.android.systemui.keyboard.backlight.ui.viewmodel + +data class BacklightDialogContentViewModel(val currentValue: Int, val maxValue: Int) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModel.kt new file mode 100644 index 000000000000..86abca5faaf3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModel.kt @@ -0,0 +1,72 @@ +/* + * 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.keyboard.backlight.ui.viewmodel + +import android.view.accessibility.AccessibilityManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.backlight.domain.interactor.KeyboardBacklightInteractor +import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper +import javax.inject.Inject +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map + +/** + * Responsible for dialog visibility and content - emits [BacklightDialogContentViewModel] if dialog + * should be shown and hidden otherwise + */ +@SysUISingleton +class BacklightDialogViewModel +@Inject +constructor( + interactor: KeyboardBacklightInteractor, + private val accessibilityManagerWrapper: AccessibilityManagerWrapper, +) { + + private val timeoutMillis: Long + get() = + accessibilityManagerWrapper + .getRecommendedTimeoutMillis( + DEFAULT_DIALOG_TIMEOUT_MILLIS, + AccessibilityManager.FLAG_CONTENT_ICONS + ) + .toLong() + + val dialogContent: Flow<BacklightDialogContentViewModel?> = + interactor.backlight + .filterNotNull() + .map { BacklightDialogContentViewModel(it.level, it.maxLevel) } + .timeout(timeoutMillis, emitAfterTimeout = null) + + private fun <T> Flow<T>.timeout(timeoutMillis: Long, emitAfterTimeout: T): Flow<T> { + return flatMapLatest { + flow { + emit(it) + delay(timeoutMillis) + emit(emitAfterTimeout) + } + } + } + + private companion object { + const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3000 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt index dd5c5d31fae3..b86083abad21 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt @@ -25,7 +25,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.keyboard.data.model.BacklightModel +import com.android.systemui.keyboard.shared.model.BacklightModel import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shared/model/BacklightModel.kt index ea15a9f18584..4a32f79285e3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shared/model/BacklightModel.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.keyboard.data.model +package com.android.systemui.keyboard.shared.model /** * Model for current state of keyboard backlight brightness. [level] indicates current level of diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 2ad1ab722d55..377a136920f0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -966,13 +966,24 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { + if (!handleOnAnimationStart( + transit, apps, wallpapers, nonApps, finishedCallback)) { + // Usually we rely on animation completion to synchronize occluded status, + // but there was no animation to play, so just update it now. + setOccluded(true /* isOccluded */, false /* animate */); + } + } + + private boolean handleOnAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { if (apps == null || apps.length == 0 || apps[0] == null) { if (DEBUG) { Log.d(TAG, "No apps provided to the OccludeByDream runner; " + "skipping occluding animation."); } finishedCallback.onAnimationFinished(); - return; + return false; } final RemoteAnimationTarget primary = apps[0]; @@ -982,7 +993,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Log.w(TAG, "The occluding app isn't Dream; " + "finishing up. Please check that the config is correct."); finishedCallback.onAnimationFinished(); - return; + return false; } final SyncRtSurfaceTransactionApplier applier = @@ -1031,6 +1042,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mOccludeByDreamAnimator.start(); }); + return true; } }; @@ -1917,20 +1929,24 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // 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. + // discrepancy between flags. If we're in the middle of hiding, do not short circuit so that + // we explicitly re-set state. if (mShowing && mKeyguardStateController.isShowing()) { - if (mPM.isInteractive()) { + if (mPM.isInteractive() && !mHiding) { // 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"); + if (DEBUG) Log.d(TAG, "doKeyguard: not showing (instead, resetting) because it is " + + "already showing, we're interactive, and we were not previously hiding. " + + "It should be safe to short-circuit here."); 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. + // We are trying to show the keyguard while the screen is off or while we were in + // the middle of hiding - 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"); + "doKeyguard: already showing, but re-showing because we're interactive or " + + "were in the middle of hiding."); } } @@ -2424,11 +2440,19 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (DEBUG) Log.d(TAG, "handleShow"); } - mHiding = false; mKeyguardExitAnimationRunner = null; mWakeAndUnlocking = false; setPendingLock(false); - setShowingLocked(true); + + // Force if we we're showing in the middle of hiding, to ensure we end up in the correct + // state. + setShowingLocked(true, mHiding /* force */); + if (mHiding) { + Log.d(TAG, "Forcing setShowingLocked because mHiding=true, which means we're " + + "showing in the middle of hiding."); + } + mHiding = false; + mKeyguardViewControllerLazy.get().show(options); resetKeyguardDonePendingLocked(); mHideAnimationRun = false; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 47ef0fac17ab..cb891063385f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -46,6 +46,8 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffor import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger; +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLoggerImpl; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; @@ -64,6 +66,8 @@ import java.util.concurrent.Executor; import dagger.Lazy; import dagger.Module; import dagger.Provides; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.CoroutineScope; /** * Dagger Module providing keyguard. @@ -153,4 +157,10 @@ public class KeyguardModule { public ViewMediatorCallback providesViewMediatorCallback(KeyguardViewMediator viewMediator) { return viewMediator.getViewMediatorCallback(); } + + /** */ + @Provides + public KeyguardQuickAffordancesMetricsLogger providesKeyguardQuickAffordancesMetricsLogger() { + return new KeyguardQuickAffordancesMetricsLoggerImpl(); + } } 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 a3b3d0fd0681..76f20d25b0ec 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 @@ -80,6 +80,9 @@ interface KeyguardRepository { */ val isKeyguardShowing: Flow<Boolean> + /** Is the keyguard in a unlocked state? */ + val isKeyguardUnlocked: Flow<Boolean> + /** Is an activity showing over the keyguard? */ val isKeyguardOccluded: Flow<Boolean> @@ -278,6 +281,31 @@ constructor( } .distinctUntilChanged() + override val isKeyguardUnlocked: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : KeyguardStateController.Callback { + override fun onUnlockedChanged() { + trySendWithFailureLogging( + keyguardStateController.isUnlocked, + TAG, + "updated isKeyguardUnlocked" + ) + } + } + + keyguardStateController.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + keyguardStateController.isUnlocked, + TAG, + "initial isKeyguardUnlocked" + ) + + awaitClose { keyguardStateController.removeCallback(callback) } + } + .distinctUntilChanged() + override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow { val callback = object : KeyguardStateController.Callback { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 0c4bca616e12..100bc596103d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -68,8 +68,11 @@ interface KeyguardTransitionRepository { /** * Begin a transition from one state to another. Transitions are interruptible, and will issue a * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one. + * + * When canceled, there are two options: to continue from the current position of the prior + * transition, or to reset the position. When [resetIfCanceled] == true, it will do the latter. */ - fun startTransition(info: TransitionInfo): UUID? + fun startTransition(info: TransitionInfo, resetIfCanceled: Boolean = false): UUID? /** * Allows manual control of a transition. When calling [startTransition], the consumer must pass @@ -130,7 +133,10 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio ) } - override fun startTransition(info: TransitionInfo): UUID? { + override fun startTransition( + info: TransitionInfo, + resetIfCanceled: Boolean, + ): UUID? { if (lastStep.from == info.from && lastStep.to == info.to) { Log.i(TAG, "Duplicate call to start the transition, rejecting: $info") return null @@ -138,7 +144,11 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio val startingValue = if (lastStep.transitionState != TransitionState.FINISHED) { Log.i(TAG, "Transition still active: $lastStep, canceling") - lastStep.value + if (resetIfCanceled) { + 0f + } else { + lastStep.value + } } else { 0f } @@ -227,10 +237,7 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio } private fun trace(step: TransitionStep, isManual: Boolean) { - if ( - step.transitionState != TransitionState.STARTED && - step.transitionState != TransitionState.FINISHED - ) { + if (step.transitionState == TransitionState.RUNNING) { return } val traceName = @@ -243,7 +250,10 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio val traceCookie = traceName.hashCode() if (step.transitionState == TransitionState.STARTED) { Trace.beginAsyncSection(traceName, traceCookie) - } else if (step.transitionState == TransitionState.FINISHED) { + } else if ( + step.transitionState == TransitionState.FINISHED || + step.transitionState == TransitionState.CANCELED + ) { Trace.endAsyncSection(traceName, traceCookie) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 8715d1f55069..3beac0b1322e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -34,7 +34,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -57,14 +56,7 @@ constructor( private fun listenForDreamingToLockscreen() { scope.launch { - // Dependending on the dream, either dream state or occluded change will change first, - // so listen for both - combine(keyguardInteractor.isAbleToDream, keyguardInteractor.isKeyguardOccluded) { - isAbleToDream, - isKeyguardOccluded -> - isAbleToDream && isKeyguardOccluded - } - .distinctUntilChanged() + keyguardInteractor.isAbleToDream .sample( combine( keyguardInteractor.dozeTransitionModel, 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 d01f48970c97..911861ddde47 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 @@ -130,55 +130,59 @@ constructor( shadeRepository.shadeModel .sample( combine( - keyguardTransitionInteractor.finishedKeyguardState, + keyguardTransitionInteractor.startedKeyguardTransitionStep, keyguardInteractor.statusBarState, - ::Pair + keyguardInteractor.isKeyguardUnlocked, + ::toTriple ), - ::toTriple + ::toQuad ) - .collect { (shadeModel, keyguardState, statusBarState) -> + .collect { (shadeModel, keyguardState, statusBarState, isKeyguardUnlocked) -> val id = transitionId if (id != null) { - // An existing `id` means a transition is started, and calls to - // `updateTransition` will control it until FINISHED or CANCELED - var nextState = - if (shadeModel.expansionAmount == 0f) { - TransitionState.FINISHED - } else if (shadeModel.expansionAmount == 1f) { - TransitionState.CANCELED - } else { - TransitionState.RUNNING - } - keyguardTransitionRepository.updateTransition( - id, - 1f - shadeModel.expansionAmount, - nextState, - ) + if (keyguardState.to == KeyguardState.PRIMARY_BOUNCER) { + // An existing `id` means a transition is started, and calls to + // `updateTransition` will control it until FINISHED or CANCELED + var nextState = + if (shadeModel.expansionAmount == 0f) { + TransitionState.FINISHED + } else if (shadeModel.expansionAmount == 1f) { + TransitionState.CANCELED + } else { + TransitionState.RUNNING + } + keyguardTransitionRepository.updateTransition( + id, + 1f - shadeModel.expansionAmount, + nextState, + ) - if ( - nextState == TransitionState.CANCELED || - nextState == TransitionState.FINISHED - ) { - transitionId = null - } + if ( + nextState == TransitionState.CANCELED || + nextState == TransitionState.FINISHED + ) { + transitionId = null + } - // If canceled, just put the state back - if (nextState == TransitionState.CANCELED) { - keyguardTransitionRepository.startTransition( - TransitionInfo( - ownerName = name, - from = KeyguardState.PRIMARY_BOUNCER, - to = KeyguardState.LOCKSCREEN, - animator = getAnimator(0.milliseconds) + // If canceled, just put the state back + if (nextState == TransitionState.CANCELED) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.LOCKSCREEN, + animator = getAnimator(0.milliseconds) + ) ) - ) + } } } else { // TODO (b/251849525): Remove statusbarstate check when that state is // integrated into KeyguardTransitionRepository if ( - keyguardState == KeyguardState.LOCKSCREEN && + keyguardState.to == KeyguardState.LOCKSCREEN && shadeModel.isUserDragging && + !isKeyguardUnlocked && statusBarState == KEYGUARD ) { transitionId = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index b59b413d7a40..94961cbf4240 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -17,6 +17,9 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator +import com.android.keyguard.KeyguardSecurityModel +import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Password +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -26,6 +29,8 @@ 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 @@ -37,7 +42,8 @@ constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, - private val keyguardTransitionInteractor: KeyguardTransitionInteractor + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val keyguardSecurityModel: KeyguardSecurityModel, ) : TransitionInteractor(FromPrimaryBouncerTransitionInteractor::class.simpleName!!) { override fun start() { @@ -93,31 +99,47 @@ constructor( private fun listenForPrimaryBouncerToGone() { scope.launch { keyguardInteractor.isKeyguardGoingAway - .sample(keyguardTransitionInteractor.finishedKeyguardState) { a, b -> Pair(a, b) } - .collect { pair -> - val (isKeyguardGoingAway, keyguardState) = pair - if (isKeyguardGoingAway && keyguardState == KeyguardState.PRIMARY_BOUNCER) { + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { (isKeyguardGoingAway, lastStartedTransitionStep) -> + if ( + isKeyguardGoingAway && + lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER + ) { + val securityMode = + keyguardSecurityModel.getSecurityMode( + KeyguardUpdateMonitor.getCurrentUser() + ) + // IME for password requires a slightly faster animation + val duration = + if (securityMode == Password) { + TO_GONE_SHORT_DURATION + } else { + TO_GONE_DURATION + } keyguardTransitionRepository.startTransition( TransitionInfo( ownerName = name, from = KeyguardState.PRIMARY_BOUNCER, to = KeyguardState.GONE, - animator = getAnimator(), - ) + animator = getAnimator(duration), + ), + resetIfCanceled = true, ) } } } } - 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 = 300L + private val DEFAULT_DURATION = 300.milliseconds + val TO_GONE_DURATION = 250.milliseconds + val TO_GONE_SHORT_DURATION = 200.milliseconds } } 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 d25aff0add86..ec99049b42e3 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 @@ -33,7 +33,9 @@ import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDoz import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay @@ -95,6 +97,9 @@ constructor( awaitClose { commandQueue.removeCallback(callback) } } + /** The device wake/sleep state */ + val wakefulnessModel: Flow<WakefulnessModel> = repository.wakefulness + /** * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means * that doze mode is not running and DREAMING is ok to commence. @@ -109,6 +114,12 @@ constructor( isDreaming && isDozeOff(dozeTransitionModel.to) } ) + .sample( + wakefulnessModel, + { isAbleToDream, wakefulnessModel -> + isAbleToDream && isWakingOrStartingToWake(wakefulnessModel) + } + ) .flatMapLatest { isAbleToDream -> flow { delay(50) @@ -119,6 +130,8 @@ constructor( /** Whether the keyguard is showing or not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing + /** Whether the keyguard is unlocked or not. */ + val isKeyguardUnlocked: Flow<Boolean> = repository.isKeyguardUnlocked /** Whether the keyguard is occluded (covered by an activity). */ val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded /** Whether the keyguard is going away. */ @@ -127,8 +140,6 @@ constructor( val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerVisible /** Whether the alternate bouncer is showing or not. */ val alternateBouncerShowing: Flow<Boolean> = bouncerRepository.alternateBouncerVisible - /** The device wake/sleep state */ - val wakefulnessModel: Flow<WakefulnessModel> = repository.wakefulness /** Observable for the [StatusBarState] */ val statusBarState: Flow<StatusBarState> = repository.statusBarState /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index bc3c7203ce3d..1735b5dba094 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -37,6 +37,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardPickerFlag import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract @@ -68,6 +69,7 @@ constructor( private val featureFlags: FeatureFlags, private val repository: Lazy<KeyguardQuickAffordanceRepository>, private val launchAnimator: DialogLaunchAnimator, + private val logger: KeyguardQuickAffordancesMetricsLogger, private val devicePolicyManager: DevicePolicyManager, @Background private val backgroundDispatcher: CoroutineDispatcher, ) { @@ -122,10 +124,12 @@ constructor( * @param configKey The configuration key corresponding to the [KeyguardQuickAffordanceModel] of * the affordance that was clicked * @param expandable An optional [Expandable] for the activity- or dialog-launch animation + * @param slotId The id of the lockscreen slot that the affordance is in */ fun onQuickAffordanceTriggered( configKey: String, expandable: Expandable?, + slotId: String, ) { @Suppress("UNCHECKED_CAST") val config = @@ -139,6 +143,7 @@ constructor( Log.e(TAG, "Affordance config with key of \"$configKey\" not found!") return } + logger.logOnShortcutTriggered(slotId, configKey) when (val result = config.onTriggered(expandable)) { is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> @@ -191,6 +196,7 @@ constructor( affordanceIds = selections, ) + logger.logOnShortcutSelected(slotId, affordanceId) return true } 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 51b02779a89f..e650b9fc0e47 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 @@ -61,7 +61,15 @@ constructor( } scope.launch { - keyguardInteractor.isDreaming.collect { logger.log(TAG, VERBOSE, "isDreaming", it) } + keyguardInteractor.isAbleToDream.collect { + logger.log(TAG, VERBOSE, "isAbleToDream", it) + } + } + + scope.launch { + keyguardInteractor.isKeyguardOccluded.collect { + logger.log(TAG, VERBOSE, "isOccluded", it) + } } scope.launch { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 1b7da5b65a03..3c0ec350c5c5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -78,6 +78,10 @@ constructor( val occludedToLockscreenTransition: Flow<TransitionStep> = repository.transition(OCCLUDED, LOCKSCREEN) + /** PRIMARY_BOUNCER->GONE transition information. */ + val primaryBouncerToGoneTransition: Flow<TransitionStep> = + repository.transition(PRIMARY_BOUNCER, GONE) + /** * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> * Lockscreen (0f). diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancesMetricsLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancesMetricsLogger.kt new file mode 100644 index 000000000000..0b0225a51412 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancesMetricsLogger.kt @@ -0,0 +1,58 @@ +/* + * 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.keyguard.shared.quickaffordance + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shared.system.SysUiStatsLog + +interface KeyguardQuickAffordancesMetricsLogger { + + /** + * Logs shortcut Triggered + * @param slotId The id of the lockscreen slot that the affordance is in + * @param affordanceId The id of the lockscreen affordance + */ + fun logOnShortcutTriggered(slotId: String, affordanceId: String) + + /** + * Logs shortcut Selected + * @param slotId The id of the lockscreen slot that the affordance is in + * @param affordanceId The id of the lockscreen affordance + */ + fun logOnShortcutSelected(slotId: String, affordanceId: String) + +} + +@SysUISingleton +class KeyguardQuickAffordancesMetricsLoggerImpl : KeyguardQuickAffordancesMetricsLogger { + + override fun logOnShortcutTriggered(slotId: String, affordanceId: String) { + SysUiStatsLog.write( + SysUiStatsLog.LOCKSCREEN_SHORTCUT_TRIGGERED, + slotId, + affordanceId, + ) + } + + override fun logOnShortcutSelected(slotId: String, affordanceId: String) { + SysUiStatsLog.write( + SysUiStatsLog.LOCKSCREEN_SHORTCUT_SELECTED, + slotId, + affordanceId, + ) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt index ca1e27c9d19c..38b9d508f81c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -47,6 +47,7 @@ class KeyguardTransitionAnimationFlow( duration: Duration, onStep: (Float) -> Float, startTime: Duration = 0.milliseconds, + onStart: (() -> Unit)? = null, onCancel: (() -> Float)? = null, onFinish: (() -> Float)? = null, interpolator: Interpolator = LINEAR, @@ -73,6 +74,7 @@ class KeyguardTransitionAnimationFlow( // the ViewModels of the last update STARTED -> { isComplete = false + onStart?.invoke() max(0f, min(1f, value)) } // Always send a final value of 1. Because of rounding, [value] may never be 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 2a9060f6db47..d63636c6fccc 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 @@ -502,6 +502,7 @@ object KeyguardBottomAreaViewBinder { KeyguardQuickAffordanceViewModel.OnClickedParameters( configKey = configKey, expandable = Expandable.fromView(view), + slotId = viewModel.slotId, ) ) } @@ -568,6 +569,7 @@ object KeyguardBottomAreaViewBinder { KeyguardQuickAffordanceViewModel.OnClickedParameters( configKey = viewModel.configKey, expandable = Expandable.fromView(view), + slotId = viewModel.slotId, ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt index 7db567b2a0e9..2337ffc35fa6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt @@ -31,6 +31,7 @@ import com.android.settingslib.Utils import com.android.systemui.keyguard.data.BouncerViewDelegate import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.ActivityStarter import kotlinx.coroutines.awaitCancellation @@ -44,6 +45,7 @@ object KeyguardBouncerViewBinder { fun bind( view: ViewGroup, viewModel: KeyguardBouncerViewModel, + primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, componentFactory: KeyguardBouncerComponent.Factory ) { // Builds the KeyguardSecurityContainerController from bouncer view group. @@ -145,6 +147,12 @@ object KeyguardBouncerViewBinder { } launch { + primaryBouncerToGoneTransitionViewModel.bouncerAlpha.collect { alpha -> + securityContainerController.setAlpha(alpha) + } + } + + launch { viewModel.bouncerExpansionAmount .filter { it == EXPANSION_VISIBLE } .collect { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt index ab9e6a4ce045..a8e346477690 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt @@ -175,7 +175,8 @@ constructor( areQuickAffordancesFullyOpaque, selectedPreviewSlotId, ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId -> - val isSelected = selectedPreviewSlotId == position.toSlotId() + val slotId = position.toSlotId() + val isSelected = selectedPreviewSlotId == slotId model.toViewModel( animateReveal = !previewMode.isInPreviewMode && animateReveal, isClickable = isFullyOpaque && !previewMode.isInPreviewMode, @@ -187,7 +188,8 @@ constructor( previewMode.isInPreviewMode && previewMode.shouldHighlightSelectedAffordance && !isSelected, - forceInactive = previewMode.isInPreviewMode + forceInactive = previewMode.isInPreviewMode, + slotId = slotId, ) } .distinctUntilChanged() @@ -200,6 +202,7 @@ constructor( isSelected: Boolean, isDimmed: Boolean, forceInactive: Boolean, + slotId: String, ): KeyguardQuickAffordanceViewModel { return when (this) { is KeyguardQuickAffordanceModel.Visible -> @@ -212,6 +215,7 @@ constructor( quickAffordanceInteractor.onQuickAffordanceTriggered( configKey = parameters.configKey, expandable = parameters.expandable, + slotId = parameters.slotId, ) }, isClickable = isClickable, @@ -219,8 +223,11 @@ constructor( isSelected = isSelected, useLongPress = quickAffordanceInteractor.useLongPress, isDimmed = isDimmed, + slotId = slotId, ) - is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel() + is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel( + slotId = slotId, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt index cb68a82118e2..38d1db6d5768 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt @@ -32,9 +32,11 @@ data class KeyguardQuickAffordanceViewModel( val isSelected: Boolean = false, val useLongPress: Boolean = false, val isDimmed: Boolean = false, + val slotId: String, ) { data class OnClickedParameters( val configKey: String, val expandable: Expandable?, + val slotId: String, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt new file mode 100644 index 000000000000..92038e24edf3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt @@ -0,0 +1,69 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.statusbar.SysuiStatusBarStateController +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow + +/** + * Breaks down PRIMARY_BOUNCER->GONE transition into discrete steps for corresponding views to + * consume. + */ +@SysUISingleton +class PrimaryBouncerToGoneTransitionViewModel +@Inject +constructor( + private val interactor: KeyguardTransitionInteractor, + private val statusBarStateController: SysuiStatusBarStateController, +) { + private val transitionAnimation = + KeyguardTransitionAnimationFlow( + transitionDuration = TO_GONE_DURATION, + transitionFlow = interactor.primaryBouncerToGoneTransition, + ) + + private var leaveShadeOpen: Boolean = false + + /** Bouncer container alpha */ + val bouncerAlpha: Flow<Float> = + transitionAnimation.createFlow( + duration = 200.milliseconds, + onStep = { 1f - it }, + ) + + /** Scrim behind alpha */ + val scrimBehindAlpha: Flow<Float> = + transitionAnimation.createFlow( + duration = TO_GONE_DURATION, + interpolator = EMPHASIZED_ACCELERATE, + onStart = { leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() }, + onStep = { + if (leaveShadeOpen) { + 1f + } else { + 1f - it + } + }, + ) +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ElevationTokens.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/DeviceStateAutoRotationLog.java index e1e666ef908b..beb725e61e4a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ElevationTokens.kt +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DeviceStateAutoRotationLog.java @@ -14,16 +14,17 @@ * limitations under the License. */ -package com.android.credentialmanager.common.ui +package com.android.systemui.log.dagger; -import androidx.compose.ui.unit.dp +import static java.lang.annotation.RetentionPolicy.RUNTIME; -/** Copied from androidx.compose.material3.tokens. */ -internal object ElevationTokens { - val Level0 = 0.0.dp - val Level1 = 1.0.dp - val Level2 = 3.0.dp - val Level3 = 6.0.dp - val Level4 = 8.0.dp - val Level5 = 12.0.dp -}
\ No newline at end of file +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface DeviceStateAutoRotationLog { +} 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 ca1ed1f5b0be..d246b35ea397 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -370,6 +370,16 @@ public class LogModule { } /** + * Provides a {@link LogBuffer} for Device State Auto-Rotation logs. + */ + @Provides + @SysUISingleton + @DeviceStateAutoRotationLog + public static LogBuffer provideDeviceStateAutoRotationLogBuffer(LogBufferFactory factory) { + return factory.create("DeviceStateAutoRotationLog", 100); + } + + /** * Provides a {@link LogBuffer} for bluetooth-related logs. */ @Provides 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 680a8b6603d6..67d3be4a3ad2 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 @@ -197,7 +197,6 @@ constructor( private val configListener = object : ConfigurationController.ConfigurationListener { - var lastOrientation = -1 override fun onDensityOrFontScaleChanged() { // System font changes should only happen when UMO is offscreen or a flicker may @@ -214,13 +213,6 @@ constructor( override fun onConfigChanged(newConfig: Configuration?) { if (newConfig == null) return isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL - val newOrientation = newConfig.orientation - if (lastOrientation != newOrientation) { - // The players actually depend on the orientation possibly, so we have to - // recreate them (at least on large screen devices) - lastOrientation = newOrientation - updatePlayers(recreateMedia = true) - } } override fun onUiModeChanged() { 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 097cc3eef304..a31c1e566018 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 @@ -32,12 +32,15 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.graphics.Bitmap; import android.graphics.BlendMode; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; +import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Animatable; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; @@ -63,7 +66,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; -import androidx.appcompat.content.res.AppCompatResources; import androidx.constraintlayout.widget.ConstraintSet; import com.android.internal.annotations.VisibleForTesting; @@ -146,6 +148,12 @@ public class MediaControlPanel { private static final int SMARTSPACE_CARD_CLICK_EVENT = 760; protected static final int SMARTSPACE_CARD_DISMISS_EVENT = 761; + private static final float REC_MEDIA_COVER_SCALE_FACTOR = 1.25f; + private static final float MEDIA_SCRIM_START_ALPHA = 0.25f; + private static final float MEDIA_REC_SCRIM_START_ALPHA = 0.15f; + private static final float MEDIA_PLAYER_SCRIM_END_ALPHA = 0.9f; + private static final float MEDIA_REC_SCRIM_END_ALPHA = 1.0f; + private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS); // Buttons to show in small player when using semantic actions @@ -779,7 +787,7 @@ public class MediaControlPanel { WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon); if (wallpaperColors != null) { mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT); - artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height); + artwork = addGradientToPlayerAlbum(artworkIcon, mutableColorScheme, width, height); isArtworkBound = true; } else { // If there's no artwork, use colors from the app icon @@ -869,8 +877,9 @@ public class MediaControlPanel { Trace.beginAsyncSection(traceName, traceCookie); // Capture width & height from views in foreground for artwork scaling in background - int width = mRecommendationViewHolder.getMediaCoverContainers().get(0).getMeasuredWidth(); - int height = mRecommendationViewHolder.getMediaCoverContainers().get(0).getMeasuredHeight(); + int width = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_rec_album_width); + int height = mContext.getResources().getDimensionPixelSize( + R.dimen.qs_media_rec_album_height_expanded); mBackgroundExecutor.execute(() -> { // Album art @@ -880,7 +889,8 @@ public class MediaControlPanel { WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon); if (wallpaperColors != null) { mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT); - artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height); + artwork = addGradientToRecommendationAlbum(artworkIcon, mutableColorScheme, width, + height); } else { artwork = new ColorDrawable(Color.TRANSPARENT); } @@ -889,6 +899,11 @@ public class MediaControlPanel { // Bind the artwork drawable to media cover. ImageView mediaCover = mRecommendationViewHolder.getMediaCoverItems().get(itemIndex); + // Rescale media cover + Matrix coverMatrix = new Matrix(mediaCover.getImageMatrix()); + coverMatrix.postScale(REC_MEDIA_COVER_SCALE_FACTOR, REC_MEDIA_COVER_SCALE_FACTOR, + 0.5f * width, 0.5f * height); + mediaCover.setImageMatrix(coverMatrix); mediaCover.setImageDrawable(artwork); // Set up the app icon. @@ -910,40 +925,62 @@ public class MediaControlPanel { // This method should be called from a background thread. WallpaperColors.fromBitmap takes a // good amount of time. We do that work on the background executor to avoid stalling animations // on the UI Thread. - private WallpaperColors getWallpaperColor(Icon artworkIcon) { + @VisibleForTesting + protected WallpaperColors getWallpaperColor(Icon artworkIcon) { if (artworkIcon != null) { if (artworkIcon.getType() == Icon.TYPE_BITMAP || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) { // Avoids extra processing if this is already a valid bitmap - return WallpaperColors - .fromBitmap(artworkIcon.getBitmap()); + Bitmap artworkBitmap = artworkIcon.getBitmap(); + if (artworkBitmap.isRecycled()) { + Log.d(TAG, "Cannot load wallpaper color from a recycled bitmap"); + return null; + } + return WallpaperColors.fromBitmap(artworkBitmap); } else { Drawable artworkDrawable = artworkIcon.loadDrawable(mContext); if (artworkDrawable != null) { - return WallpaperColors - .fromDrawable(artworkIcon.loadDrawable(mContext)); + return WallpaperColors.fromDrawable(artworkDrawable); } } } return null; } - private LayerDrawable addGradientToIcon( - Icon artworkIcon, - ColorScheme mutableColorScheme, - int width, - int height - ) { + @VisibleForTesting + protected LayerDrawable addGradientToPlayerAlbum(Icon artworkIcon, + ColorScheme mutableColorScheme, int width, int height) { Drawable albumArt = getScaledBackground(artworkIcon, width, height); - GradientDrawable gradient = (GradientDrawable) AppCompatResources - .getDrawable(mContext, R.drawable.qs_media_scrim); + GradientDrawable gradient = (GradientDrawable) mContext.getDrawable( + R.drawable.qs_media_scrim).mutate(); + return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme, + MEDIA_SCRIM_START_ALPHA, MEDIA_PLAYER_SCRIM_END_ALPHA); + } + + @VisibleForTesting + protected LayerDrawable addGradientToRecommendationAlbum(Icon artworkIcon, + ColorScheme mutableColorScheme, int width, int height) { + // First try scaling rec card using bitmap drawable. + // If returns null, set drawable bounds. + Drawable albumArt = getScaledRecommendationCover(artworkIcon, width, height); + if (albumArt == null) { + albumArt = getScaledBackground(artworkIcon, width, height); + } + GradientDrawable gradient = (GradientDrawable) mContext.getDrawable( + R.drawable.qs_media_rec_scrim).mutate(); + return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme, + MEDIA_REC_SCRIM_START_ALPHA, MEDIA_REC_SCRIM_END_ALPHA); + } + + private LayerDrawable setupGradientColorOnDrawable(Drawable albumArt, GradientDrawable gradient, + ColorScheme mutableColorScheme, float startAlpha, float endAlpha) { gradient.setColors(new int[] { ColorUtilKt.getColorWithAlpha( MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme), - 0.25f), + startAlpha), ColorUtilKt.getColorWithAlpha( MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme), - 0.9f), + endAlpha), }); return new LayerDrawable(new Drawable[] { albumArt, gradient }); } @@ -1589,6 +1626,29 @@ public class MediaControlPanel { } /** + * Scale artwork to fill the background of media covers in recommendation card. + */ + @UiThread + private Drawable getScaledRecommendationCover(Icon artworkIcon, int width, int height) { + if (width == 0 || height == 0) { + return null; + } + if (artworkIcon != null) { + Bitmap bitmap; + if (artworkIcon.getType() == Icon.TYPE_BITMAP + || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) { + Bitmap artworkBitmap = artworkIcon.getBitmap(); + if (artworkBitmap != null) { + bitmap = Bitmap.createScaledBitmap(artworkIcon.getBitmap(), width, + height, false); + return new BitmapDrawable(mContext.getResources(), bitmap); + } + } + } + return null; + } + + /** * Get the current media controller * * @return the controller 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 0788e6172a78..b4724ddebb9a 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 @@ -154,9 +154,11 @@ constructor( return transitionLayout?.translationY ?: 0.0f } - /** A callback for RTL config changes */ + /** A callback for config changes */ private val configurationListener = object : ConfigurationController.ConfigurationListener { + var lastOrientation = -1 + override fun onConfigChanged(newConfig: Configuration?) { // Because the TransitionLayout is not always attached (and calculates/caches layout // results regardless of attach state), we have to force the layoutDirection of the @@ -169,6 +171,13 @@ constructor( transitionLayout?.layoutDirection = layoutDirection refreshState() } + val newOrientation = newConfig.orientation + if (lastOrientation != newOrientation) { + // Layout dimensions are possibly changing, so we need to update them. (at + // least on large screen devices) + lastOrientation = newOrientation + loadLayoutForType(type) + } } } } @@ -195,13 +204,14 @@ constructor( * The expanded constraint set used to render a expanded player. If it is modified, make sure to * call [refreshState] */ - val collapsedLayout = ConstraintSet() - + var collapsedLayout = ConstraintSet() + @VisibleForTesting set /** * The expanded constraint set used to render a collapsed player. If it is modified, make sure * to call [refreshState] */ - val expandedLayout = ConstraintSet() + var expandedLayout = ConstraintSet() + @VisibleForTesting set /** Whether the guts are visible for the associated player. */ var isGutsVisible = false @@ -483,7 +493,7 @@ constructor( */ fun attach(transitionLayout: TransitionLayout, type: TYPE) = traceSection("MediaViewController#attach") { - updateMediaViewControllerType(type) + loadLayoutForType(type) logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation) this.transitionLayout = transitionLayout layoutController.attach(transitionLayout) @@ -641,7 +651,7 @@ constructor( return result } - private fun updateMediaViewControllerType(type: TYPE) { + private fun loadLayoutForType(type: TYPE) { this.type = type // These XML resources contain ConstraintSets that will apply to this player type's layout diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 0dfb9b1f13c7..006cedfe36b4 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -1733,41 +1733,26 @@ public class NavigationBar extends ViewController<NavigationBarView> implements bottomTappableProvider = new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT); } - if (!mEdgeBackGestureHandler.isHandlingGestures()) { - // 2/3 button navigation is on. Do not provide any gesture insets here. But need to keep - // the provider to support runtime update. - return new InsetsFrameProvider[] { - navBarProvider, - new InsetsFrameProvider( - ITYPE_BOTTOM_MANDATORY_GESTURES, Insets.NONE), - new InsetsFrameProvider(ITYPE_LEFT_GESTURES, InsetsFrameProvider.SOURCE_DISPLAY, - Insets.NONE, null), - new InsetsFrameProvider(ITYPE_RIGHT_GESTURES, - InsetsFrameProvider.SOURCE_DISPLAY, - Insets.NONE, null), - bottomTappableProvider - }; - } else { - // Gesture navigation - final int gestureHeight = userContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.navigation_bar_gesture_height); - final DisplayCutout cutout = userContext.getDisplay().getCutout(); - final int safeInsetsLeft = cutout != null ? cutout.getSafeInsetLeft() : 0; - final int safeInsetsRight = cutout != null ? cutout.getSafeInsetRight() : 0; - return new InsetsFrameProvider[] { - navBarProvider, - new InsetsFrameProvider( - ITYPE_BOTTOM_MANDATORY_GESTURES, Insets.of(0, 0, 0, gestureHeight)), - new InsetsFrameProvider(ITYPE_LEFT_GESTURES, InsetsFrameProvider.SOURCE_DISPLAY, - Insets.of(safeInsetsLeft - + mEdgeBackGestureHandler.getEdgeWidthLeft(), 0, 0, 0), null), - new InsetsFrameProvider(ITYPE_RIGHT_GESTURES, - InsetsFrameProvider.SOURCE_DISPLAY, - Insets.of(0, 0, safeInsetsRight - + mEdgeBackGestureHandler.getEdgeWidthRight(), 0), null), - bottomTappableProvider - }; - } + final DisplayCutout cutout = userContext.getDisplay().getCutout(); + final int safeInsetsLeft = cutout != null ? cutout.getSafeInsetLeft() : 0; + final int safeInsetsRight = cutout != null ? cutout.getSafeInsetRight() : 0; + final int gestureHeight = userContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_gesture_height); + final boolean handlingGesture = mEdgeBackGestureHandler.isHandlingGestures(); + final int gestureInsetsLeft = handlingGesture + ? mEdgeBackGestureHandler.getEdgeWidthLeft() + safeInsetsLeft : 0; + final int gestureInsetsRight = handlingGesture + ? mEdgeBackGestureHandler.getEdgeWidthRight() + safeInsetsRight : 0; + return new InsetsFrameProvider[] { + navBarProvider, + new InsetsFrameProvider( + ITYPE_BOTTOM_MANDATORY_GESTURES, Insets.of(0, 0, 0, gestureHeight)), + new InsetsFrameProvider(ITYPE_LEFT_GESTURES, InsetsFrameProvider.SOURCE_DISPLAY, + Insets.of(gestureInsetsLeft, 0, 0, 0), null), + new InsetsFrameProvider(ITYPE_RIGHT_GESTURES, InsetsFrameProvider.SOURCE_DISPLAY, + Insets.of(0, 0, gestureInsetsRight, 0), null), + bottomTappableProvider + }; } private boolean canShowSecondaryHandle() { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt index f335733b430c..70040c75d123 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt @@ -2,19 +2,15 @@ package com.android.systemui.navigationbar.gestural import android.content.Context import android.content.res.Configuration -import android.content.res.TypedArray import android.graphics.Canvas import android.graphics.Paint import android.graphics.Path import android.graphics.RectF import android.util.MathUtils.min -import android.util.TypedValue import android.view.View -import androidx.appcompat.view.ContextThemeWrapper import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce -import com.android.internal.R.style.Theme_DeviceDefault import com.android.internal.util.LatencyTracker import com.android.settingslib.Utils import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOnAnimationEndListener @@ -159,26 +155,21 @@ class BackPanel( val isDeviceInNightTheme = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES - val colorControlActivated = ContextThemeWrapper(context, Theme_DeviceDefault) - .run { - val typedValue = TypedValue() - val a: TypedArray = obtainStyledAttributes(typedValue.data, - intArrayOf(android.R.attr.colorControlActivated)) - val color = a.getColor(0, 0) - a.recycle() - color + arrowPaint.color = Utils.getColorAttrDefaultColor(context, + if (isDeviceInNightTheme) { + com.android.internal.R.attr.colorAccentPrimary + } else { + com.android.internal.R.attr.textColorPrimary } + ) - val colorPrimary = - Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary) - - arrowPaint.color = Utils.getColorAccentDefaultColor(context) - - arrowBackgroundPaint.color = if (isDeviceInNightTheme) { - colorPrimary - } else { - colorControlActivated - } + arrowBackgroundPaint.color = Utils.getColorAttrDefaultColor(context, + if (isDeviceInNightTheme) { + com.android.internal.R.attr.materialColorOnSecondary + } else { + com.android.internal.R.attr.colorAccentSecondary + } + ) } inner class AnimatedFloat( @@ -414,9 +405,9 @@ class BackPanel( ) { horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation) scale.updateRestingPosition(restingParams.scale) - arrowAlpha.updateRestingPosition(restingParams.arrowDimens.alpha) backgroundAlpha.updateRestingPosition(restingParams.backgroundDimens.alpha) + arrowAlpha.updateRestingPosition(restingParams.arrowDimens.alpha, animate) arrowLength.updateRestingPosition(restingParams.arrowDimens.length, animate) arrowHeight.updateRestingPosition(restingParams.arrowDimens.height, animate) scalePivotX.updateRestingPosition(restingParams.backgroundDimens.width, animate) diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index f409b23cf4e2..80ed08c901af 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -55,12 +55,12 @@ private const val PX_PER_MS = 1 internal const val MIN_DURATION_ACTIVE_ANIMATION = 300L private const val MIN_DURATION_CANCELLED_ANIMATION = 200L -private const val MIN_DURATION_COMMITTED_ANIMATION = 200L +private const val MIN_DURATION_COMMITTED_ANIMATION = 120L private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L private const val MIN_DURATION_CONSIDERED_AS_FLING = 100L private const val FAILSAFE_DELAY_MS = 350L -private const val POP_ON_FLING_DELAY = 160L +private const val POP_ON_FLING_DELAY = 140L internal val VIBRATE_ACTIVATED_EFFECT = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) @@ -148,8 +148,6 @@ class BackPanelController internal constructor( private var gestureSinceActionDown = 0L private var gestureEntryTime = 0L private var gestureActiveTime = 0L - private var gestureInactiveOrEntryTime = 0L - private var gestureArrowStrokeVisibleTime = 0L private val elapsedTimeSinceActionDown get() = SystemClock.uptimeMillis() - gestureSinceActionDown @@ -441,34 +439,44 @@ class BackPanelController internal constructor( updateArrowStateOnMove(yTranslation, xTranslation) - when (currentState) { - GestureState.ACTIVE -> { - stretchActiveBackIndicator(fullScreenProgress(xTranslation)) - } - GestureState.ENTRY -> { - val progress = staticThresholdProgress(xTranslation) - stretchEntryBackIndicator(progress) - - params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let { - mView.popArrowAlpha(0f, it.value) - } - } - GestureState.INACTIVE -> { - val progress = reactivationThresholdProgress(totalTouchDelta) - stretchInactiveBackIndicator(progress) + val gestureProgress = when (currentState) { + GestureState.ACTIVE -> fullScreenProgress(xTranslation) + GestureState.ENTRY -> staticThresholdProgress(xTranslation) + GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDelta) + else -> null + } - params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let { - gestureArrowStrokeVisibleTime = SystemClock.uptimeMillis() - mView.popArrowAlpha(0f, it.value) - } + gestureProgress?.let { + when (currentState) { + GestureState.ACTIVE -> stretchActiveBackIndicator(gestureProgress) + GestureState.ENTRY -> stretchEntryBackIndicator(gestureProgress) + GestureState.INACTIVE -> stretchInactiveBackIndicator(gestureProgress) + else -> {} } - else -> {} } - // set y translation + setArrowStrokeAlpha(gestureProgress) setVerticalTranslation(yOffset) } + private fun setArrowStrokeAlpha(gestureProgress: Float?) { + val strokeAlphaProgress = when (currentState) { + GestureState.ENTRY -> gestureProgress + GestureState.INACTIVE -> gestureProgress + GestureState.ACTIVE, + GestureState.FLUNG, + GestureState.COMMITTED -> 1f + GestureState.CANCELLED, + GestureState.GONE -> 0f + } + + strokeAlphaProgress?.let { progress -> + params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let { + mView.popArrowAlpha(0f, it.value) + } + } + } + private fun setVerticalTranslation(yOffset: Float) { val yTranslation = abs(yOffset) val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f @@ -599,7 +607,7 @@ class BackPanelController internal constructor( private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean { val minDistanceConsideredForFling = ViewConfiguration.get(context).scaledTouchSlop - val flingDistance = abs(endX - startX) + val flingDistance = if (mView.isLeftPanel) endX - startX else startX - endX val isPastFlingVelocity = isDragAwayFromEdge( velocityPxPerSecThreshold = ViewConfiguration.get(context).scaledMinimumFlingVelocity) @@ -764,7 +772,7 @@ class BackPanelController internal constructor( GestureState.ENTRY, GestureState.INACTIVE -> params.entryIndicator.arrowDimens GestureState.ACTIVE -> params.activeIndicator.arrowDimens - GestureState.FLUNG, + GestureState.FLUNG -> params.flungIndicator.arrowDimens GestureState.COMMITTED -> params.committedIndicator.arrowDimens GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens }, @@ -825,7 +833,6 @@ class BackPanelController internal constructor( updateRestingArrowDimens() gestureEntryTime = SystemClock.uptimeMillis() - gestureInactiveOrEntryTime = SystemClock.uptimeMillis() } GestureState.ACTIVE -> { previousXTranslationOnActiveOffset = previousXTranslation @@ -857,7 +864,13 @@ class BackPanelController internal constructor( } GestureState.INACTIVE -> { - gestureInactiveOrEntryTime = SystemClock.uptimeMillis() + + // Typically entering INACTIVE means + // totalTouchDelta <= deactivationSwipeTriggerThreshold + // but because we can also independently enter this state + // if touch Y >> touch X, we force it to deactivationSwipeTriggerThreshold + // so that gesture progress in this state is consistent regardless of entry + totalTouchDelta = params.deactivationSwipeTriggerThreshold val startingVelocity = convertVelocityToSpringStartingVelocity( valueOnFastVelocity = -1.05f, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt index 0c0002221244..d46333a50c8d 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt @@ -9,8 +9,8 @@ import com.android.systemui.R data class EdgePanelParams(private var resources: Resources) { data class ArrowDimens( - val length: Float = 0f, - val height: Float = 0f, + val length: Float? = 0f, + val height: Float? = 0f, val alpha: Float = 0f, var alphaSpring: SpringForce? = null, val heightSpring: SpringForce? = null, @@ -139,17 +139,17 @@ data class EdgePanelParams(private var resources: Resources) { entryWidthInterpolator = PathInterpolator(.19f, 1.27f, .71f, .86f) entryWidthTowardsEdgeInterpolator = PathInterpolator(1f, -3f, 1f, 1.2f) - activeWidthInterpolator = PathInterpolator(.15f, .48f, .46f, .89f) + activeWidthInterpolator = PathInterpolator(.32f, 0f, .16f, .94f) arrowAngleInterpolator = entryWidthInterpolator translationInterpolator = PathInterpolator(0.2f, 1.0f, 1.0f, 1.0f) farCornerInterpolator = PathInterpolator(.03f, .19f, .14f, 1.09f) edgeCornerInterpolator = PathInterpolator(0f, 1.11f, .85f, .84f) heightInterpolator = PathInterpolator(1f, .05f, .9f, -0.29f) - val showArrowOnProgressValue = .2f + val showArrowOnProgressValue = .23f val showArrowOnProgressValueFactor = 1.05f - val entryActiveHorizontalTranslationSpring = createSpring(675f, 0.8f) + val entryActiveHorizontalTranslationSpring = createSpring(800f, 0.8f) val activeCommittedArrowLengthSpring = createSpring(1500f, 0.29f) val activeCommittedArrowHeightSpring = createSpring(1500f, 0.29f) val flungCommittedEdgeCornerSpring = createSpring(10000f, 1f) @@ -178,7 +178,7 @@ data class EdgePanelParams(private var resources: Resources) { height = getDimen(R.dimen.navigation_edge_entry_background_height), edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners), farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners), - alphaSpring = createSpring(900f, 1f), + alphaSpring = createSpring(1100f, 1f), widthSpring = createSpring(450f, 0.65f), heightSpring = createSpring(1500f, 0.45f), farCornerRadiusSpring = createSpring(300f, 0.5f), @@ -232,7 +232,7 @@ data class EdgePanelParams(private var resources: Resources) { getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners), farCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_far_corners), - widthSpring = createSpring(200f, 0.65f), + widthSpring = createSpring(250f, 0.65f), heightSpring = createSpring(1500f, 0.45f), farCornerRadiusSpring = createSpring(200f, 1f), edgeCornerRadiusSpring = createSpring(150f, 0.5f), @@ -244,6 +244,8 @@ data class EdgePanelParams(private var resources: Resources) { arrowDimens = activeIndicator.arrowDimens.copy( lengthSpring = activeCommittedArrowLengthSpring, heightSpring = activeCommittedArrowHeightSpring, + length = null, + height = null, ), backgroundDimens = activeIndicator.backgroundDimens.copy( alpha = 0f, @@ -255,13 +257,15 @@ data class EdgePanelParams(private var resources: Resources) { farCornerRadiusSpring = flungCommittedFarCornerSpring, ), scale = 0.85f, - scaleSpring = createSpring(650f, 1f), + scaleSpring = createSpring(1150f, 1f), ) flungIndicator = committedIndicator.copy( arrowDimens = committedIndicator.arrowDimens.copy( lengthSpring = createSpring(850f, 0.46f), heightSpring = createSpring(850f, 0.46f), + length = activeIndicator.arrowDimens.length, + height = activeIndicator.arrowDimens.height ), backgroundDimens = committedIndicator.backgroundDimens.copy( widthSpring = flungCommittedWidthSpring, diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index c65f0aaab91f..5b36e93ec5e1 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -22,6 +22,9 @@ import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.PackageManager import android.os.Build import android.os.UserManager @@ -54,8 +57,8 @@ constructor( private val resolver: NoteTaskInfoResolver, private val eventLogger: NoteTaskEventLogger, private val optionalBubbles: Optional<Bubbles>, - private val optionalUserManager: Optional<UserManager>, - private val optionalKeyguardManager: Optional<KeyguardManager>, + private val userManager: UserManager, + private val keyguardManager: KeyguardManager, @NoteTaskEnabledKey private val isEnabled: Boolean, private val devicePolicyManager: DevicePolicyManager, private val userTracker: UserTracker, @@ -106,8 +109,6 @@ constructor( if (!isEnabled) return val bubbles = optionalBubbles.getOrNull() ?: return - val userManager = optionalUserManager.getOrNull() ?: return - val keyguardManager = optionalKeyguardManager.getOrNull() ?: return // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing. if (!userManager.isUserUnlocked) return @@ -140,12 +141,13 @@ constructor( logDebug { "onShowNoteTask - start: $info" } when (info.launchMode) { is NoteTaskLaunchMode.AppBubble -> { + // TODO(b/267634412, b/268351693): Should use `showOrHideAppBubbleAsUser` bubbles.showOrHideAppBubble(intent) // App bubble logging happens on `onBubbleExpandChanged`. logDebug { "onShowNoteTask - opened as app bubble: $info" } } is NoteTaskLaunchMode.Activity -> { - context.startActivity(intent) + context.startActivityAsUser(intent, userTracker.userHandle) eventLogger.logNoteTaskOpened(info) logDebug { "onShowNoteTask - opened as activity: $info" } } @@ -198,12 +200,21 @@ constructor( } private fun createNoteIntent(info: NoteTaskInfo): Intent = - Intent(NoteTaskController.ACTION_CREATE_NOTE) - .setPackage(info.packageName) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + Intent(NoteTaskController.ACTION_CREATE_NOTE).apply { + setPackage(info.packageName) + // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint // was used to start it. - .putExtra(NoteTaskController.INTENT_EXTRA_USE_STYLUS_MODE, true) + putExtra(NoteTaskController.INTENT_EXTRA_USE_STYLUS_MODE, true) + + addFlags(FLAG_ACTIVITY_NEW_TASK) + // We should ensure the note experience can be open both as a full screen (lock screen) + // and inside the app bubble (contextual). These additional flags will do that. + if (info.launchMode == NoteTaskLaunchMode.Activity) { + addFlags(FLAG_ACTIVITY_MULTIPLE_TASK) + addFlags(FLAG_ACTIVITY_NEW_DOCUMENT) + } + } private inline fun logDebug(message: () -> String) { if (Build.IS_DEBUGGABLE) { diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEntryPoint.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEntryPoint.kt index acc537a8eb36..2fa8f9a1e6fc 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEntryPoint.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEntryPoint.kt @@ -17,7 +17,7 @@ package com.android.systemui.notetask import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceConfig import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity -import com.android.systemui.screenshot.AppClipsTrampolineActivity +import com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity /** * Supported entry points for [NoteTaskController.showNoteTask]. diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt index 0f75f9591074..7be491f0b3a6 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt @@ -17,19 +17,19 @@ package com.android.systemui.notetask import android.app.role.RoleManager -import android.content.Context import android.content.pm.PackageManager import android.content.pm.PackageManager.ApplicationInfoFlags import android.os.UserHandle import android.util.Log +import com.android.systemui.settings.UserTracker import javax.inject.Inject class NoteTaskInfoResolver @Inject constructor( - private val context: Context, private val roleManager: RoleManager, private val packageManager: PackageManager, + private val userTracker: UserTracker, ) { fun resolveInfo( @@ -38,7 +38,7 @@ constructor( isKeyguardLocked: Boolean = false, ): NoteTaskInfo? { // TODO(b/267634412): Select UserHandle depending on where the user initiated note-taking. - val user = context.user + val user = userTracker.userHandle val packageName = roleManager.getRoleHoldersAsUser(RoleManager.ROLE_NOTES, user).firstOrNull() diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt index ba8999c068e3..6278c699498c 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -17,11 +17,7 @@ package com.android.systemui.notetask import android.app.Activity -import android.app.KeyguardManager import android.app.role.RoleManager -import android.content.Context -import android.os.UserManager -import androidx.core.content.getSystemService import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceModule @@ -32,7 +28,6 @@ import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap -import java.util.Optional /** Compose all dependencies required by Note Task feature. */ @Module(includes = [NoteTaskQuickAffordanceModule::class]) @@ -55,15 +50,5 @@ interface NoteTaskModule { val isFeatureEnabled = featureFlags.isEnabled(Flags.NOTE_TASKS) return isRoleAvailable && isFeatureEnabled } - - @Provides - fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> { - return Optional.ofNullable(context.getSystemService()) - } - - @Provides - fun provideOptionalUserManager(context: Context): Optional<UserManager> { - return Optional.ofNullable(context.getSystemService()) - } } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt index 8ced46461dbb..5c59532e0c2e 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt @@ -17,8 +17,10 @@ package com.android.systemui.notetask.shortcut import android.app.Activity +import android.app.role.RoleManager import android.content.Intent import android.os.Bundle +import android.os.PersistableBundle import androidx.activity.ComponentActivity import androidx.annotation.DrawableRes import androidx.core.content.pm.ShortcutInfoCompat @@ -36,7 +38,11 @@ import javax.inject.Inject * href="https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts#custom-pinned">Creating * a custom shortcut activity</a> */ -class CreateNoteTaskShortcutActivity @Inject constructor() : ComponentActivity() { +class CreateNoteTaskShortcutActivity +@Inject +constructor( + private val roleManager: RoleManager, +) : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -59,12 +65,19 @@ class CreateNoteTaskShortcutActivity @Inject constructor() : ComponentActivity() intent: Intent, @DrawableRes iconResource: Int, ): Intent { + val extras = PersistableBundle() + + roleManager.getRoleHoldersAsUser(RoleManager.ROLE_NOTES, user).firstOrNull()?.let { name -> + extras.putString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, name) + } + val shortcutInfo = ShortcutInfoCompat.Builder(this, id) .setIntent(intent) .setShortLabel(shortLabel) .setLongLived(true) .setIcon(IconCompat.createWithResource(this, iconResource)) + .setExtras(extras) .build() return ShortcutManagerCompat.createShortcutResultIntent( @@ -75,5 +88,16 @@ class CreateNoteTaskShortcutActivity @Inject constructor() : ComponentActivity() private companion object { private const val SHORTCUT_ID = "note-task-shortcut-id" + + /** + * Shortcut extra which can point to a package name and can be used to indicate an alternate + * badge info. Launcher only reads this if the shortcut comes from a system app. + * + * Duplicated from [com.android.launcher3.icons.IconCache]. + * + * @see com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE + */ + private const val EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE = + "extra_shortcut_badge_override_package" } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index 7c2536dac56e..d4854e1a7daf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -328,7 +328,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener if (listening) { updateDefaultTileAndIcon(); refreshState(); - if (!mServiceManager.isActiveTile()) { + if (!mServiceManager.isActiveTile() || !isTileReady()) { mServiceManager.setBindRequested(true); mService.onStartListening(); } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 25ff308b46bb..019ca52107dd 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -631,7 +631,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis final NavigationBarView navBarView = mNavBarControllerLazy.get().getNavigationBarView(mContext.getDisplayId()); final NotificationPanelViewController panelController = - mCentralSurfacesOptionalLazy.get().get().getNotificationPanelViewController(); + mCentralSurfacesOptionalLazy.get() + .map(CentralSurfaces::getNotificationPanelViewController) + .orElse(null); if (SysUiState.DEBUG) { Log.d(TAG_OPS, "Updating sysui state flags: navBarFragment=" + navBarFragment + " navBarView=" + navBarView + " panelController=" + panelController); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java index ead3b7b1de53..0b4b7c691cfd 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java @@ -45,6 +45,7 @@ public class DraggableConstraintLayout extends ConstraintLayout implements ViewTreeObserver.OnComputeInternalInsetsListener { private static final float VELOCITY_DP_PER_MS = 1; + private static final int MAXIMUM_DISMISS_DISTANCE_DP = 400; private final SwipeDismissHandler mSwipeDismissHandler; private final GestureDetector mSwipeDetector; @@ -347,14 +348,18 @@ public class DraggableConstraintLayout extends ConstraintLayout } else { finalX = -1 * getBackgroundRight(); } - float distance = Math.abs(finalX - startX); + float distance = Math.min(Math.abs(finalX - startX), + FloatingWindowUtil.dpToPx(mDisplayMetrics, MAXIMUM_DISMISS_DISTANCE_DP)); + // ensure that view dismisses in the right direction (right in LTR, left in RTL) + float distanceVector = Math.copySign(distance, finalX - startX); anim.addUpdateListener(animation -> { - float translation = MathUtils.lerp(startX, finalX, animation.getAnimatedFraction()); + float translation = MathUtils.lerp( + startX, startX + distanceVector, animation.getAnimatedFraction()); mView.setTranslationX(translation); mView.setAlpha(1 - animation.getAnimatedFraction()); }); - anim.setDuration((long) (distance / Math.abs(velocity))); + anim.setDuration((long) (Math.abs(distance / velocity))); return anim; } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index c8c133774766..7cfe2327f992 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -57,7 +57,8 @@ import java.util.concurrent.Executor; import javax.inject.Inject; -class ImageExporter { +/** A class to help with exporting screenshot to storage. */ +public class ImageExporter { private static final String TAG = LogConfig.logTag(ImageExporter.class); static final Duration PENDING_ENTRY_TTL = Duration.ofHours(24); @@ -90,7 +91,7 @@ class ImageExporter { private final FeatureFlags mFlags; @Inject - ImageExporter(ContentResolver resolver, FeatureFlags flags) { + public ImageExporter(ContentResolver resolver, FeatureFlags flags) { mResolver = resolver; mFlags = flags; } @@ -148,7 +149,7 @@ class ImageExporter { * * @return a listenable future result */ - ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, + public ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, UserHandle owner) { return export(executor, requestId, bitmap, ZonedDateTime.now(), owner); } @@ -181,13 +182,14 @@ class ImageExporter { ); } - static class Result { - Uri uri; - UUID requestId; - String fileName; - long timestamp; - CompressFormat format; - boolean published; + /** The result returned by the task exporting screenshots to storage. */ + public static class Result { + public Uri uri; + public UUID requestId; + public String fileName; + public long timestamp; + public CompressFormat format; + public boolean published; @Override public String toString() { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java index fc94aed5336a..7a62bae5b5ae 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java @@ -93,13 +93,7 @@ public enum ScreenshotEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "User has discarded the result of a long screenshot") SCREENSHOT_LONG_SCREENSHOT_EXIT(911), @UiEvent(doc = "A screenshot has been taken and saved to work profile") - SCREENSHOT_SAVED_TO_WORK_PROFILE(1240), - @UiEvent(doc = "Notes application triggered the screenshot for notes") - SCREENSHOT_FOR_NOTE_TRIGGERED(1308), - @UiEvent(doc = "User accepted the screenshot to be sent to the notes app") - SCREENSHOT_FOR_NOTE_ACCEPTED(1309), - @UiEvent(doc = "User cancelled the screenshot for notes app flow") - SCREENSHOT_FOR_NOTE_CANCELLED(1310); + SCREENSHOT_SAVED_TO_WORK_PROFILE(1240); private final int mId; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java index 3133924339f2..4756cc8172e9 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.appclips; -import static com.android.systemui.screenshot.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE; -import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_NAME; -import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_RESULT_RECEIVER; -import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; -import static com.android.systemui.screenshot.AppClipsTrampolineActivity.PERMISSION_SELF; -import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_FOR_NOTE_ACCEPTED; -import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_FOR_NOTE_CANCELLED; +import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_ACCEPTED; +import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_CANCELLED; +import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE; +import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_NAME; +import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_RESULT_RECEIVER; +import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; +import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.PERMISSION_SELF; import android.app.Activity; import android.content.BroadcastReceiver; @@ -52,6 +52,8 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLogger.UiEventEnum; import com.android.settingslib.Utils; import com.android.systemui.R; +import com.android.systemui.screenshot.CropView; +import com.android.systemui.screenshot.MagnifierView; import com.android.systemui.settings.UserTracker; import javax.inject.Inject; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java index 65fb4c9bfb0d..e1619dc9b6ee 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java @@ -32,12 +32,12 @@ import javax.inject.Inject; /** An intermediary singleton object to help communicating with the cross process service. */ @SysUISingleton -public class AppClipsCrossProcessHelper { +class AppClipsCrossProcessHelper { private final ServiceConnector<IAppClipsScreenshotHelperService> mProxyConnector; @Inject - public AppClipsCrossProcessHelper(@Application Context context) { + AppClipsCrossProcessHelper(@Application Context context) { mProxyConnector = new ServiceConnector.Impl<IAppClipsScreenshotHelperService>(context, new Intent(context, AppClipsScreenshotHelperService.class), Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY @@ -52,7 +52,7 @@ public class AppClipsCrossProcessHelper { * pass around but not a {@link Bitmap}. */ @Nullable - public Bitmap takeScreenshot() { + Bitmap takeScreenshot() { try { AndroidFuture<ScreenshotHardwareBufferInternal> future = mProxyConnector.postForResult( diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsEvent.java new file mode 100644 index 000000000000..7a085b9fd7d1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsEvent.java @@ -0,0 +1,40 @@ +/* + * 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.screenshot.appclips; + +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; + +enum AppClipsEvent implements UiEventLogger.UiEventEnum { + + @UiEvent(doc = "Notes application triggered the screenshot for notes") + SCREENSHOT_FOR_NOTE_TRIGGERED(1308), + @UiEvent(doc = "User accepted the screenshot to be sent to the notes app") + SCREENSHOT_FOR_NOTE_ACCEPTED(1309), + @UiEvent(doc = "User cancelled the screenshot for notes app flow") + SCREENSHOT_FOR_NOTE_CANCELLED(1310); + + private final int mId; + + AppClipsEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java index 6f8c36595c74..83ff020362f1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java @@ -24,7 +24,6 @@ import android.window.ScreenCapture.ScreenshotSync; import androidx.annotation.Nullable; -import com.android.systemui.screenshot.AppClipsActivity; import com.android.wm.shell.bubbles.Bubbles; import java.util.Optional; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java index eda38e45c98a..3cb1a34a921c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.appclips; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; @@ -24,7 +24,7 @@ import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; -import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; +import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; import android.app.Activity; import android.app.admin.DevicePolicyManager; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java index b2910fd48854..4cbca28a4032 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java @@ -14,12 +14,10 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.appclips; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; -import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE; - import android.content.Intent; import android.graphics.Bitmap; import android.graphics.HardwareRenderer; @@ -31,7 +29,6 @@ import android.net.Uri; import android.os.Process; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; @@ -39,11 +36,10 @@ import androidx.lifecycle.ViewModelProvider; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.screenshot.appclips.AppClipsCrossProcessHelper; +import com.android.systemui.screenshot.ImageExporter; import com.google.common.util.concurrent.ListenableFuture; -import java.time.ZonedDateTime; import java.util.UUID; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -52,8 +48,7 @@ import java.util.concurrent.Executor; import javax.inject.Inject; /** A {@link ViewModel} to help with the App Clips screenshot flow. */ -@VisibleForTesting(otherwise = PACKAGE_PRIVATE) -public final class AppClipsViewModel extends ViewModel { +final class AppClipsViewModel extends ViewModel { private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; private final ImageExporter mImageExporter; @@ -80,8 +75,7 @@ public final class AppClipsViewModel extends ViewModel { } /** Grabs a screenshot and updates the {@link Bitmap} set in screenshot {@link LiveData}. */ - @VisibleForTesting(otherwise = PACKAGE_PRIVATE) - public void performScreenshot() { + void performScreenshot() { mBgExecutor.execute(() -> { Bitmap screenshot = mAppClipsCrossProcessHelper.takeScreenshot(); mMainExecutor.execute(() -> { @@ -95,14 +89,12 @@ public final class AppClipsViewModel extends ViewModel { } /** Returns a {@link LiveData} that holds the captured screenshot. */ - @VisibleForTesting(otherwise = PACKAGE_PRIVATE) - public LiveData<Bitmap> getScreenshot() { + LiveData<Bitmap> getScreenshot() { return mScreenshotLiveData; } /** Returns a {@link LiveData} that holds the {@link Uri} where screenshot is saved. */ - @VisibleForTesting(otherwise = PACKAGE_PRIVATE) - public LiveData<Uri> getResultLiveData() { + LiveData<Uri> getResultLiveData() { return mResultLiveData; } @@ -110,8 +102,7 @@ public final class AppClipsViewModel extends ViewModel { * Returns a {@link LiveData} that holds the error codes for * {@link Intent#EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE}. */ - @VisibleForTesting(otherwise = PACKAGE_PRIVATE) - public LiveData<Integer> getErrorLiveData() { + LiveData<Integer> getErrorLiveData() { return mErrorLiveData; } @@ -119,8 +110,7 @@ public final class AppClipsViewModel extends ViewModel { * Saves the provided {@link Drawable} to storage then informs the result {@link Uri} to * {@link LiveData}. */ - @VisibleForTesting(otherwise = PACKAGE_PRIVATE) - public void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds) { + void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds) { mBgExecutor.execute(() -> { // Render the screenshot bitmap in background. Bitmap screenshotBitmap = renderBitmap(screenshotDrawable, bounds); @@ -128,7 +118,7 @@ public final class AppClipsViewModel extends ViewModel { // Export and save the screenshot in background. // TODO(b/267310185): Save to work profile UserHandle. ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export( - mBgExecutor, UUID.randomUUID(), screenshotBitmap, ZonedDateTime.now(), + mBgExecutor, UUID.randomUUID(), screenshotBitmap, Process.myUserHandle()); // Get the result and update state on main thread. @@ -160,8 +150,7 @@ public final class AppClipsViewModel extends ViewModel { } /** Helper factory to help with injecting {@link AppClipsViewModel}. */ - @VisibleForTesting(otherwise = PACKAGE_PRIVATE) - public static final class Factory implements ViewModelProvider.Factory { + static final class Factory implements ViewModelProvider.Factory { private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; private final ImageExporter mImageExporter; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.java index 3b107f101088..1e53ebb7935e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.java @@ -28,7 +28,7 @@ import android.window.ScreenCapture.ScreenshotHardwareBuffer; * An internal version of {@link ScreenshotHardwareBuffer} that helps with parceling the information * necessary for creating a {@link Bitmap}. */ -public class ScreenshotHardwareBufferInternal implements Parcelable { +class ScreenshotHardwareBufferInternal implements Parcelable { public static final Creator<ScreenshotHardwareBufferInternal> CREATOR = new Creator<>() { @@ -45,7 +45,7 @@ public class ScreenshotHardwareBufferInternal implements Parcelable { private final HardwareBuffer mHardwareBuffer; private final ParcelableColorSpace mParcelableColorSpace; - public ScreenshotHardwareBufferInternal( + ScreenshotHardwareBufferInternal( ScreenshotHardwareBuffer screenshotHardwareBuffer) { mHardwareBuffer = screenshotHardwareBuffer.getHardwareBuffer(); mParcelableColorSpace = new ParcelableColorSpace( @@ -65,7 +65,7 @@ public class ScreenshotHardwareBufferInternal implements Parcelable { * {@link Bitmap#wrapHardwareBuffer(HardwareBuffer, ColorSpace)} and * {@link HardwareBuffer#close()} for more information. */ - public Bitmap createBitmapThenCloseBuffer() { + Bitmap createBitmapThenCloseBuffer() { Bitmap bitmap = Bitmap.wrapHardwareBuffer(mHardwareBuffer, mParcelableColorSpace.getColorSpace()); mHardwareBuffer.close(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index b1987c151e5f..ee9e54a22ddf 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -4720,8 +4720,13 @@ public final class NotificationPanelViewController implements Dumpable { gesture possible. */ int pointerIndex = event.findPointerIndex(mTrackingPointer); if (pointerIndex < 0) { - pointerIndex = 0; - mTrackingPointer = event.getPointerId(pointerIndex); + if (mTrackingPointer < 0) { + pointerIndex = 0; + mTrackingPointer = event.getPointerId(pointerIndex); + } else { + mShadeLog.logMotionEvent(event, "Skipping intercept of multitouch pointer"); + return false; + } } final float x = event.getX(pointerIndex); final float y = event.getY(pointerIndex); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 87350b465895..c130b3913b64 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -45,6 +45,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.binder.KeyguardBouncerViewBinder; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel; +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; import com.android.systemui.statusbar.DragDownHelper; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationInsetsController; @@ -133,7 +134,8 @@ public class NotificationShadeWindowViewController { KeyguardBouncerViewModel keyguardBouncerViewModel, KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory, AlternateBouncerInteractor alternateBouncerInteractor, - KeyguardTransitionInteractor keyguardTransitionInteractor + KeyguardTransitionInteractor keyguardTransitionInteractor, + PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel ) { mLockscreenShadeTransitionController = transitionController; mFalsingCollector = falsingCollector; @@ -160,6 +162,7 @@ public class NotificationShadeWindowViewController { KeyguardBouncerViewBinder.bind( mView.findViewById(R.id.keyguard_bouncer_container), keyguardBouncerViewModel, + primaryBouncerToGoneTransitionViewModel, keyguardBouncerComponentFactory); collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index c35c5c522798..77550038c94a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -93,6 +93,16 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi @IntDef({STATE_ICON, STATE_DOT, STATE_HIDDEN}) public @interface VisibleState { } + /** Returns a human-readable string of {@link VisibleState}. */ + public static String getVisibleStateString(@VisibleState int state) { + switch(state) { + case STATE_ICON: return "ICON"; + case STATE_DOT: return "DOT"; + case STATE_HIDDEN: return "HIDDEN"; + default: return "UNKNOWN"; + } + } + private static final String TAG = "StatusBarIconView"; private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT = new FloatProperty<StatusBarIconView>("iconAppearAmount") { @@ -561,7 +571,8 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi @Override public String toString() { return "StatusBarIconView(" - + "slot='" + mSlot + " alpha=" + getAlpha() + " icon=" + mIcon + + "slot='" + mSlot + "' alpha=" + getAlpha() + " icon=" + mIcon + + " visibleState=" + getVisibleStateString(getVisibleState()) + " iconColor=#" + Integer.toHexString(mIconColor) + " notification=" + mNotification + ')'; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupWhenCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupWhenCoordinator.kt new file mode 100644 index 000000000000..5ce1db2b6acd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupWhenCoordinator.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.coordinator + +import android.util.ArrayMap +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator +import com.android.systemui.statusbar.notification.collection.render.NotifGroupController +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlin.math.max +import kotlin.math.min + +/** A small coordinator which finds, stores, and applies the closest notification time. */ +@CoordinatorScope +class GroupWhenCoordinator +@Inject +constructor( + @Main private val delayableExecutor: DelayableExecutor, + private val systemClock: SystemClock +) : Coordinator { + + private val invalidator = object : Invalidator("GroupWhenCoordinator") {} + private val notificationGroupTimes = ArrayMap<GroupEntry, Long>() + private var cancelInvalidateListRunnable: Runnable? = null + + private val invalidateListRunnable: Runnable = Runnable { + invalidator.invalidateList("future notification invalidation") + } + + override fun attach(pipeline: NotifPipeline) { + pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilterListener) + pipeline.addOnAfterRenderGroupListener(::onAfterRenderGroupListener) + pipeline.addPreRenderInvalidator(invalidator) + } + + private fun onBeforeFinalizeFilterListener(entries: List<ListEntry>) { + cancelListInvalidation() + notificationGroupTimes.clear() + + val now = systemClock.currentTimeMillis() + var closestFutureTime = Long.MAX_VALUE + entries.asSequence().filterIsInstance<GroupEntry>().forEach { groupEntry -> + val whenMillis = calculateGroupNotificationTime(groupEntry, now) + notificationGroupTimes[groupEntry] = whenMillis + if (whenMillis > now) { + closestFutureTime = min(closestFutureTime, whenMillis) + } + } + + if (closestFutureTime != Long.MAX_VALUE) { + cancelInvalidateListRunnable = + delayableExecutor.executeDelayed(invalidateListRunnable, closestFutureTime - now) + } + } + + private fun cancelListInvalidation() { + cancelInvalidateListRunnable?.run() + cancelInvalidateListRunnable = null + } + + private fun onAfterRenderGroupListener(group: GroupEntry, controller: NotifGroupController) { + notificationGroupTimes[group]?.let(controller::setNotificationGroupWhen) + } + + private fun calculateGroupNotificationTime( + groupEntry: GroupEntry, + currentTimeMillis: Long + ): Long { + var pastTime = Long.MIN_VALUE + var futureTime = Long.MAX_VALUE + groupEntry.children + .asSequence() + .mapNotNull { child -> child.sbn.notification.`when`.takeIf { it > 0 } } + .forEach { time -> + val isInThePast = currentTimeMillis - time > 0 + if (isInThePast) { + pastTime = max(pastTime, time) + } else { + futureTime = min(futureTime, time) + } + } + + if (pastTime == Long.MIN_VALUE && futureTime == Long.MAX_VALUE) { + return checkNotNull(groupEntry.summary).creationTime + } + + return if (futureTime != Long.MAX_VALUE) futureTime else pastTime + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index 8a82bcad44e4..6bb5b9218ed7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -31,31 +31,32 @@ interface NotifCoordinators : Coordinator, PipelineDumpable @CoordinatorScope class NotifCoordinatorsImpl @Inject constructor( - notifPipelineFlags: NotifPipelineFlags, - dataStoreCoordinator: DataStoreCoordinator, - hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, - hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, - keyguardCoordinator: KeyguardCoordinator, - rankingCoordinator: RankingCoordinator, - appOpsCoordinator: AppOpsCoordinator, - deviceProvisionedCoordinator: DeviceProvisionedCoordinator, - bubbleCoordinator: BubbleCoordinator, - headsUpCoordinator: HeadsUpCoordinator, - gutsCoordinator: GutsCoordinator, - conversationCoordinator: ConversationCoordinator, - debugModeCoordinator: DebugModeCoordinator, - groupCountCoordinator: GroupCountCoordinator, - mediaCoordinator: MediaCoordinator, - preparationCoordinator: PreparationCoordinator, - remoteInputCoordinator: RemoteInputCoordinator, - rowAppearanceCoordinator: RowAppearanceCoordinator, - stackCoordinator: StackCoordinator, - shadeEventCoordinator: ShadeEventCoordinator, - smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator, - viewConfigCoordinator: ViewConfigCoordinator, - visualStabilityCoordinator: VisualStabilityCoordinator, - sensitiveContentCoordinator: SensitiveContentCoordinator, - dismissibilityCoordinator: DismissibilityCoordinator + notifPipelineFlags: NotifPipelineFlags, + dataStoreCoordinator: DataStoreCoordinator, + hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, + hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, + keyguardCoordinator: KeyguardCoordinator, + rankingCoordinator: RankingCoordinator, + appOpsCoordinator: AppOpsCoordinator, + deviceProvisionedCoordinator: DeviceProvisionedCoordinator, + bubbleCoordinator: BubbleCoordinator, + headsUpCoordinator: HeadsUpCoordinator, + gutsCoordinator: GutsCoordinator, + conversationCoordinator: ConversationCoordinator, + debugModeCoordinator: DebugModeCoordinator, + groupCountCoordinator: GroupCountCoordinator, + groupWhenCoordinator: GroupWhenCoordinator, + mediaCoordinator: MediaCoordinator, + preparationCoordinator: PreparationCoordinator, + remoteInputCoordinator: RemoteInputCoordinator, + rowAppearanceCoordinator: RowAppearanceCoordinator, + stackCoordinator: StackCoordinator, + shadeEventCoordinator: ShadeEventCoordinator, + smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator, + viewConfigCoordinator: ViewConfigCoordinator, + visualStabilityCoordinator: VisualStabilityCoordinator, + sensitiveContentCoordinator: SensitiveContentCoordinator, + dismissibilityCoordinator: DismissibilityCoordinator ) : NotifCoordinators { private val mCoordinators: MutableList<Coordinator> = ArrayList() @@ -82,6 +83,7 @@ class NotifCoordinatorsImpl @Inject constructor( mCoordinators.add(debugModeCoordinator) mCoordinators.add(conversationCoordinator) mCoordinators.add(groupCountCoordinator) + mCoordinators.add(groupWhenCoordinator) mCoordinators.add(mediaCoordinator) mCoordinators.add(rowAppearanceCoordinator) mCoordinators.add(stackCoordinator) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGroupController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGroupController.kt index e2edc01f0d7c..061ef9e9341c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGroupController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGroupController.kt @@ -20,4 +20,7 @@ package com.android.systemui.statusbar.notification.collection.render interface NotifGroupController { /** Set the number of children that this group would have if not for the 8-child max */ fun setUntruncatedChildCount(untruncatedChildCount: Int) + + /** Set the when value of notification group that reflects most important closest notification time */ + fun setNotificationGroupWhen(whenMillis: Long) }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 2affa77eee04..6deaa23ca20e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -192,7 +192,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private int mMaxSmallHeightBeforeS; private int mMaxSmallHeight; private int mMaxSmallHeightLarge; - private int mMaxSmallHeightMedia; private int mMaxExpandedHeight; private int mIncreasedPaddingBetweenElements; private int mNotificationLaunchHeight; @@ -853,6 +852,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** + * @see NotificationChildrenContainer#setNotificationGroupWhen(long) + */ + public void setNotificationGroupWhen(long whenMillis) { + if (mIsSummaryWithChildren) { + mChildrenContainer.setNotificationGroupWhen(whenMillis); + } else { + Log.w(TAG, "setNotificationGroupWhen( whenMillis: " + whenMillis + ")" + + " mIsSummaryWithChildren: false" + + " mChildrenContainer has not been inflated yet."); + } + } + + /** * Called after children have been attached to set the expansion states */ public void resetChildSystemExpandedStates() { @@ -1774,8 +1786,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView R.dimen.notification_min_height); mMaxSmallHeightLarge = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_min_height_increased); - mMaxSmallHeightMedia = NotificationUtils.getFontScaledHeight(mContext, - R.dimen.notification_min_height_media); mMaxExpandedHeight = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_max_height); mMaxHeadsUpHeightBeforeN = NotificationUtils.getFontScaledHeight(mContext, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index 2dda6fd802e8..dfc80fde3cd2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -349,6 +349,15 @@ public class ExpandableNotificationRowController implements NotifViewController } @Override + public void setNotificationGroupWhen(long whenMillis) { + if (mView.isSummaryWithChildren()) { + mView.setNotificationGroupWhen(whenMillis); + } else { + Log.w(TAG, "Called setNotificationTime(" + whenMillis + ") on a leaf row"); + } + } + + @Override public void setSystemExpanded(boolean systemExpanded) { mView.setSystemExpanded(systemExpanded); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index 1f664cb16179..9a777ea6230b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -27,6 +27,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import android.widget.DateTimeView; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; @@ -344,6 +345,21 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper imple mTransformationHelper.setVisible(visible); } + /*** + * Set Notification when value + * @param whenMillis + */ + public void setNotificationWhen(long whenMillis) { + if (mNotificationHeader == null) { + return; + } + + final View timeView = mNotificationHeader.findViewById(com.android.internal.R.id.time); + + if (timeView instanceof DateTimeView) { + ((DateTimeView) timeView).setTime(whenMillis); + } + } protected void addTransformedViews(View... views) { for (View view : views) { if (view != null) { 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 9b93d7b9e1d0..40f55bd3726c 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 @@ -296,6 +296,19 @@ public class NotificationChildrenContainer extends ViewGroup } /** + * Set the notification time in the group so that the view can show the latest event in the UI + * appropriately. + */ + public void setNotificationGroupWhen(long whenMillis) { + if (mNotificationHeaderWrapper != null) { + mNotificationHeaderWrapper.setNotificationWhen(whenMillis); + } + if (mNotificationHeaderWrapperLowPriority != null) { + mNotificationHeaderWrapperLowPriority.setNotificationWhen(whenMillis); + } + } + + /** * Add a child notification to this view. * * @param row the row to add 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 e09b94bb661d..8d782e1c9fa0 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 @@ -67,6 +67,7 @@ import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AnimationUtils; @@ -199,6 +200,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private final boolean mDebugRemoveAnimation; private final boolean mSimplifiedAppearFraction; private final boolean mUseRoundnessSourceTypes; + private boolean mAnimatedInsets; private int mContentHeight; private float mIntrinsicContentHeight; @@ -207,7 +209,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private int mTopPadding; private boolean mAnimateNextTopPaddingChange; private int mBottomPadding; - private int mBottomInset = 0; + @VisibleForTesting + int mBottomInset = 0; private float mQsExpansionFraction; private final int mSplitShadeMinContentHeight; @@ -388,9 +391,33 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } }; + private boolean mPulsing; private boolean mScrollable; private View mForcedScroll; + private boolean mIsInsetAnimationRunning; + + private final WindowInsetsAnimation.Callback mInsetsCallback = + new WindowInsetsAnimation.Callback( + WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) { + + @Override + public void onPrepare(WindowInsetsAnimation animation) { + mIsInsetAnimationRunning = true; + } + + @Override + public WindowInsets onProgress(WindowInsets windowInsets, + List<WindowInsetsAnimation> list) { + updateBottomInset(windowInsets); + return windowInsets; + } + + @Override + public void onEnd(WindowInsetsAnimation animation) { + mIsInsetAnimationRunning = false; + } + }; /** * @see #setHideAmount(float, float) @@ -584,6 +611,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mDebugRemoveAnimation = featureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION); mSimplifiedAppearFraction = featureFlags.isEnabled(Flags.SIMPLIFIED_APPEAR_FRACTION); mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES); + setAnimatedInsetsEnabled(featureFlags.isEnabled(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS)); mSectionsManager = Dependency.get(NotificationSectionsManager.class); mScreenOffAnimationController = Dependency.get(ScreenOffAnimationController.class); @@ -622,6 +650,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mGroupMembershipManager = Dependency.get(GroupMembershipManager.class); mGroupExpansionManager = Dependency.get(GroupExpansionManager.class); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + if (mAnimatedInsets) { + setWindowInsetsAnimationCallback(mInsetsCallback); + } } /** @@ -690,6 +721,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } @VisibleForTesting + void setAnimatedInsetsEnabled(boolean enabled) { + mAnimatedInsets = enabled; + } + + @VisibleForTesting @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void updateFooter() { if (mFooterView == null) { @@ -1781,7 +1817,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return; } mForcedScroll = v; - scrollTo(v); + if (mAnimatedInsets) { + updateForcedScroll(); + } else { + scrollTo(v); + } } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) @@ -1813,26 +1853,46 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable + ((!isExpanded() && isPinnedHeadsUp(v)) ? mHeadsUpInset : getTopPadding()); } + private void updateBottomInset(WindowInsets windowInsets) { + mBottomInset = windowInsets.getInsets(WindowInsets.Type.ime()).bottom; + + if (mForcedScroll != null) { + updateForcedScroll(); + } + + int range = getScrollRange(); + if (mOwnScrollY > range) { + setOwnScrollY(range); + } + } + @Override @ShadeViewRefactor(RefactorComponent.COORDINATOR) public WindowInsets onApplyWindowInsets(WindowInsets insets) { - mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom; + if (!mAnimatedInsets) { + mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom; + } mWaterfallTopInset = 0; final DisplayCutout cutout = insets.getDisplayCutout(); if (cutout != null) { mWaterfallTopInset = cutout.getWaterfallInsets().top; } - - int range = getScrollRange(); - if (mOwnScrollY > range) { - // HACK: We're repeatedly getting staggered insets here while the IME is - // animating away. To work around that we'll wait until things have settled. - removeCallbacks(mReclamp); - postDelayed(mReclamp, 50); - } else if (mForcedScroll != null) { - // The scroll was requested before we got the actual inset - in case we need - // to scroll up some more do so now. - scrollTo(mForcedScroll); + if (mAnimatedInsets && !mIsInsetAnimationRunning) { + // update bottom inset e.g. after rotation + updateBottomInset(insets); + } + if (!mAnimatedInsets) { + int range = getScrollRange(); + if (mOwnScrollY > range) { + // HACK: We're repeatedly getting staggered insets here while the IME is + // animating away. To work around that we'll wait until things have settled. + removeCallbacks(mReclamp); + postDelayed(mReclamp, 50); + } else if (mForcedScroll != null) { + // The scroll was requested before we got the actual inset - in case we need + // to scroll up some more do so now. + scrollTo(mForcedScroll); + } } return insets; } 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 7855cdfeb4c3..cf5ecdddf854 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -389,6 +389,9 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp boolean isStrongBiometric) { Trace.beginSection("BiometricUnlockController#onBiometricAuthenticated"); if (mUpdateMonitor.isGoingToSleep()) { + mLogger.deferringAuthenticationDueToSleep(userId, + biometricSourceType, + mPendingAuthenticated != null); mPendingAuthenticated = new PendingAuthenticated(userId, biometricSourceType, isStrongBiometric); Trace.endSection(); @@ -795,6 +798,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp public void onFinishedGoingToSleep() { Trace.beginSection("BiometricUnlockController#onFinishedGoingToSleep"); if (mPendingAuthenticated != null) { + mLogger.finishedGoingToSleepWithPendingAuth(); PendingAuthenticated pendingAuthenticated = mPendingAuthenticated; // Post this to make sure it's executed after the device is fully locked. mHandler.post(() -> onBiometricAuthenticated(pendingAuthenticated.userId, 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 d6dc67143892..664d61acf7cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -3747,6 +3747,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void notifyBiometricAuthModeChanged() { mDozeServiceHost.updateDozing(); + if (mBiometricUnlockController.getMode() + == BiometricUnlockController.MODE_DISMISS_BOUNCER) { + // Don't update the scrim controller at this time, in favor of the transition repository + // updating the scrim + return; + } updateScrimController(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java index c72eb054c62c..39b5b5a4cef8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -40,6 +40,7 @@ import com.android.systemui.statusbar.StatusIconDisplayable; import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; @@ -288,10 +289,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da * @param mobileContext possibly mcc/mnc overridden mobile context * @param subId the subscriptionId for this mobile view */ - public void addModernMobileView(Context mobileContext, int subId) { + public void addModernMobileView( + Context mobileContext, + MobileViewLogger mobileViewLogger, + int subId) { Log.d(TAG, "addModernMobileView (subId=" + subId + ")"); ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind( mobileContext, + mobileViewLogger, "mobile", mMobileIconsViewModel.viewModelForSub(subId, mLocation) ); 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 b88531e59568..ae715b3f20c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -429,7 +429,6 @@ public class DozeParameters implements } dispatchAlwaysOnEvent(); - mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn()); } @Override @@ -469,6 +468,7 @@ public class DozeParameters implements for (Callback callback : mCallbacks) { callback.onAlwaysOnChange(); } + mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn()); } private boolean getPostureSpecificBool( 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 753032c2ee01..3268032becf8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -59,7 +59,7 @@ import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import com.android.systemui.statusbar.phone.fragment.StatusBarIconBlocklistKt; -import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventAnimator; +import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventDefaultAnimator; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -75,6 +75,8 @@ import java.util.concurrent.Executor; import javax.inject.Inject; +import kotlin.Unit; + /** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */ public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> { private static final String TAG = "KeyguardStatusBarViewController"; @@ -123,7 +125,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat public void onDensityOrFontScaleChanged() { mView.loadDimens(); // The animator is dependent on resources for offsets - mSystemEventAnimator = new StatusBarSystemEventAnimator(mView, getResources()); + mSystemEventAnimator = + getSystemEventAnimator(mSystemEventAnimator.isAnimationRunning()); } @Override @@ -248,7 +251,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat private int mStatusBarState; private boolean mDozing; private boolean mShowingKeyguardHeadsUp; - private StatusBarSystemEventAnimator mSystemEventAnimator; + private StatusBarSystemEventDefaultAnimator mSystemEventAnimator; + private float mSystemEventAnimatorAlpha = 1; /** * The alpha value to be set on the View. If -1, this value is to be ignored. @@ -324,7 +328,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mView.setKeyguardUserAvatarEnabled( !mStatusBarUserChipViewModel.getChipEnabled()); - mSystemEventAnimator = new StatusBarSystemEventAnimator(mView, r); + mSystemEventAnimator = getSystemEventAnimator(/* isAnimationRunning */ false); mDisableStateTracker = new DisableStateTracker( /* mask1= */ DISABLE_SYSTEM_INFO, @@ -480,6 +484,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat * (1.0f - mKeyguardHeadsUpShowingAmount); } + if (mSystemEventAnimator.isAnimationRunning()) { + newAlpha = Math.min(newAlpha, mSystemEventAnimatorAlpha); + } + boolean hideForBypass = mFirstBypassAttempt && mKeyguardUpdateMonitor.shouldListenForFace() || mDelayShowingKeyguardStatusBar; @@ -488,7 +496,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat && !mDozing && !hideForBypass && !mDisableStateTracker.isDisabled() - ? View.VISIBLE : View.INVISIBLE; + ? View.VISIBLE : View.INVISIBLE; updateViewState(newAlpha, newVisibility); } @@ -614,4 +622,15 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat updateBlockedIcons(); } }; + + private StatusBarSystemEventDefaultAnimator getSystemEventAnimator(boolean isAnimationRunning) { + return new StatusBarSystemEventDefaultAnimator(getResources(), (alpha) -> { + mSystemEventAnimatorAlpha = alpha; + updateViewState(); + return Unit.INSTANCE; + }, (translationX) -> { + mView.setTranslationX(translationX); + return Unit.INSTANCE; + }, isAnimationRunning); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index fb8bf523f625..9fb942c7b740 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; + import static java.lang.Float.isNaN; import android.animation.Animator; @@ -53,9 +55,14 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants; +import com.android.systemui.keyguard.shared.model.TransitionState; +import com.android.systemui.keyguard.shared.model.TransitionStep; +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.NotificationPanelViewController; +import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.stack.ViewState; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -71,6 +78,8 @@ import java.util.function.Consumer; import javax.inject.Inject; +import kotlinx.coroutines.CoroutineDispatcher; + /** * Controls both the scrim behind the notifications and in front of the notifications (when a * security method gets shown). @@ -197,6 +206,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private final ScreenOffAnimationController mScreenOffAnimationController; private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + private final SysuiStatusBarStateController mStatusBarStateController; private GradientColors mColors; private boolean mNeedsDrawableColorUpdate; @@ -251,6 +261,20 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private boolean mWakeLockHeld; private boolean mKeyguardOccluded; + private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private CoroutineDispatcher mMainDispatcher; + private boolean mIsBouncerToGoneTransitionRunning = false; + private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel; + private final Consumer<Float> mScrimAlphaConsumer = + (Float alpha) -> { + mScrimInFront.setViewAlpha(mInFrontAlpha); + mNotificationsScrim.setViewAlpha(mNotificationsAlpha); + mBehindAlpha = alpha; + mScrimBehind.setViewAlpha(alpha); + }; + + Consumer<TransitionStep> mPrimaryBouncerToGoneTransition; + @Inject public ScrimController( LightBarController lightBarController, @@ -265,13 +289,18 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump @Main Executor mainExecutor, ScreenOffAnimationController screenOffAnimationController, KeyguardUnlockAnimationController keyguardUnlockAnimationController, - StatusBarKeyguardViewManager statusBarKeyguardViewManager) { + StatusBarKeyguardViewManager statusBarKeyguardViewManager, + PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel, + KeyguardTransitionInteractor keyguardTransitionInteractor, + SysuiStatusBarStateController sysuiStatusBarStateController, + @Main CoroutineDispatcher mainDispatcher) { mScrimStateListener = lightBarController::setScrimState; mDefaultScrimAlpha = BUSY_SCRIM_ALPHA; mKeyguardStateController = keyguardStateController; mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen(); mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mStatusBarStateController = sysuiStatusBarStateController; mKeyguardVisibilityCallback = new KeyguardVisibilityCallback(); mHandler = handler; mMainExecutor = mainExecutor; @@ -304,6 +333,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } }); mColors = new GradientColors(); + mPrimaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel; + mKeyguardTransitionInteractor = keyguardTransitionInteractor; + mMainDispatcher = mainDispatcher; } /** @@ -343,6 +375,33 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump for (ScrimState state : ScrimState.values()) { state.prepare(state); } + + // Directly control transition to UNLOCKED scrim state from PRIMARY_BOUNCER, and make sure + // to report back that keyguard has faded away. This fixes cases where the scrim state was + // rapidly switching on unlock, due to shifts in state in CentralSurfacesImpl + mPrimaryBouncerToGoneTransition = + (TransitionStep step) -> { + TransitionState state = step.getTransitionState(); + + mIsBouncerToGoneTransitionRunning = state == TransitionState.RUNNING; + + if (state == TransitionState.STARTED) { + setExpansionAffectsAlpha(false); + transitionTo(ScrimState.UNLOCKED); + } + + if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) { + setExpansionAffectsAlpha(true); + if (mKeyguardStateController.isKeyguardFadingAway()) { + mStatusBarKeyguardViewManager.onKeyguardFadedAway(); + } + } + }; + + collectFlow(behindScrim, mKeyguardTransitionInteractor.getPrimaryBouncerToGoneTransition(), + mPrimaryBouncerToGoneTransition, mMainDispatcher); + collectFlow(behindScrim, mPrimaryBouncerToGoneTransitionViewModel.getScrimBehindAlpha(), + mScrimAlphaConsumer, mMainDispatcher); } // TODO(b/270984686) recompute scrim height accurately, based on shade contents. @@ -372,6 +431,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } public void transitionTo(ScrimState state, Callback callback) { + if (mIsBouncerToGoneTransitionRunning) { + Log.i(TAG, "Skipping transition to: " + state + + " while mIsBouncerToGoneTransitionRunning"); + return; + } if (state == mState) { // Call the callback anyway, unless it's already enqueued if (callback != null && mCallback != callback) { @@ -1049,7 +1113,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mBehindAlpha = 1; } // Prevent notification scrim flicker when transitioning away from keyguard. - if (mKeyguardStateController.isKeyguardGoingAway()) { + if (mKeyguardStateController.isKeyguardGoingAway() + && !mStatusBarStateController.leaveOpenOnKeyguardHide()) { mNotificationsAlpha = 0; } @@ -1138,7 +1203,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_tint", Color.alpha(tint)); scrimView.setTint(tint); - scrimView.setViewAlpha(alpha); + if (!mIsBouncerToGoneTransitionRunning) { + scrimView.setViewAlpha(alpha); + } } else { scrim.setAlpha(alpha); } @@ -1486,6 +1553,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } public void setKeyguardOccluded(boolean keyguardOccluded) { + if (mKeyguardOccluded == keyguardOccluded) { + return; + } mKeyguardOccluded = keyguardOccluded; updateScrims(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index 11863627218e..04cc8ce792d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -569,7 +569,10 @@ public interface StatusBarIconController { mGroup.addView(view, index, onCreateLayoutParams()); if (mIsInDemoMode) { - mDemoStatusIcons.addModernMobileView(mContext, subId); + mDemoStatusIcons.addModernMobileView( + mContext, + mMobileIconsViewModel.getLogger(), + subId); } return view; @@ -601,6 +604,7 @@ public interface StatusBarIconController { return ModernStatusBarMobileView .constructAndBind( mobileContext, + mMobileIconsViewModel.getLogger(), slot, mMobileIconsViewModel.viewModelForSub(subId, mLocation) ); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java index f6c0da8da8c0..833cb93f62e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java @@ -79,6 +79,18 @@ public class StatusBarIconHolder { private @IconType int mType = TYPE_ICON; private int mTag = 0; + /** Returns a human-readable string representing the given type. */ + public static String getTypeString(@IconType int type) { + switch(type) { + case TYPE_ICON: return "ICON"; + case TYPE_WIFI: return "WIFI_OLD"; + case TYPE_MOBILE: return "MOBILE_OLD"; + case TYPE_MOBILE_NEW: return "MOBILE_NEW"; + case TYPE_WIFI_NEW: return "WIFI_NEW"; + default: return "UNKNOWN"; + } + } + private StatusBarIconHolder() { } @@ -230,4 +242,11 @@ public class StatusBarIconHolder { public int getTag() { return mTag; } + + @Override + public String toString() { + return "StatusBarIconHolder(type=" + getTypeString(mType) + + " tag=" + getTag() + + " visible=" + isVisible() + ")"; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java index 8800b05fadb7..565481a20d95 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java @@ -27,6 +27,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** A class holding the list of all the system icons that could be shown in the status bar. */ public class StatusBarIconList { @@ -302,7 +303,7 @@ public class StatusBarIconList { @Override public String toString() { - return String.format("(%s) %s", mName, subSlotsString()); + return String.format("(%s) holder=%s %s", mName, mHolder, subSlotsString()); } private String subSlotsString() { @@ -310,7 +311,10 @@ public class StatusBarIconList { return ""; } - return "" + mSubSlots.size() + " subSlots"; + return "| " + mSubSlots.size() + " subSlots: " + + mSubSlots.stream() + .map(StatusBarIconHolder::toString) + .collect(Collectors.joining("|")); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt index 79c0984d9bf7..d30d0e25bd8c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt @@ -22,6 +22,7 @@ import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator.AlphaP import com.android.systemui.statusbar.phone.PhoneStatusBarViewController.StatusBarViewsCenterProvider import com.android.systemui.unfold.SysUIUnfoldScope import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import com.android.systemui.unfold.util.CurrentActivityTypeProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider import javax.inject.Inject import kotlin.math.max @@ -29,9 +30,13 @@ import kotlin.math.max @SysUIUnfoldScope class StatusBarMoveFromCenterAnimationController @Inject constructor( private val progressProvider: ScopedUnfoldTransitionProgressProvider, + private val currentActivityTypeProvider: CurrentActivityTypeProvider, windowManager: WindowManager ) { + // Whether we're on home activity. Updated only when the animation starts. + private var isOnHomeActivity: Boolean? = null + private val transitionListener = TransitionListener() private val moveFromCenterAnimator = UnfoldMoveFromCenterAnimator( windowManager, @@ -60,6 +65,10 @@ class StatusBarMoveFromCenterAnimationController @Inject constructor( } private inner class TransitionListener : TransitionProgressListener { + override fun onTransitionStarted() { + isOnHomeActivity = currentActivityTypeProvider.isHomeActivity + } + override fun onTransitionProgress(progress: Float) { moveFromCenterAnimator.onTransitionProgress(progress) } @@ -68,11 +77,23 @@ class StatusBarMoveFromCenterAnimationController @Inject constructor( // Reset translations when transition is stopped/cancelled // (e.g. the transition could be cancelled mid-way when rotating the screen) moveFromCenterAnimator.onTransitionProgress(1f) + isOnHomeActivity = null } } - private class StatusBarIconsAlphaProvider : AlphaProvider { + + /** + * In certain cases, an alpha is applied based on the progress. + * + * This mainly happens to hide the statusbar during the unfold animation while on apps, as the + * bounds of the app "collapse" to the center, but the statusbar doesn't. + * While on launcher, this alpha is not applied. + */ + private inner class StatusBarIconsAlphaProvider : AlphaProvider { override fun getAlpha(progress: Float): Float { + if (isOnHomeActivity == true) { + return 1.0f + } return max( 0f, (progress - ICONS_START_APPEARING_PROGRESS) / (1 - ICONS_START_APPEARING_PROGRESS) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 726b2344309f..edfc95fcc2e7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -16,11 +16,13 @@ package com.android.systemui.statusbar.phone; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.service.notification.NotificationListenerService.REASON_CLICK; import static com.android.systemui.statusbar.phone.CentralSurfaces.getActivityOptions; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.app.KeyguardManager; import android.app.Notification; import android.app.PendingIntent; @@ -579,8 +581,14 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, entry.getKey()); mCentralSurfaces.wakeUpForFullScreenIntent(); - fullScreenIntent.send(); + + ActivityOptions options = ActivityOptions.makeBasic(); + options.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + fullScreenIntent.sendAndReturnResult(null, 0, null, null, null, null, + options.toBundle()); entry.notifyFullScreenIntentLaunched(); + mMetricsLogger.count("note_fullscreen", 1); String activityName; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt index c04ea36b3d8d..5903fa3d5bd1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt @@ -26,19 +26,39 @@ import com.android.systemui.statusbar.events.STATUS_BAR_X_MOVE_IN import com.android.systemui.statusbar.events.STATUS_BAR_X_MOVE_OUT import com.android.systemui.statusbar.events.SystemStatusAnimationCallback import com.android.systemui.util.animation.AnimationUtil.Companion.frames +import com.android.systemui.util.doOnCancel +import com.android.systemui.util.doOnEnd + +/** + * An implementation of [StatusBarSystemEventDefaultAnimator], applying the onAlphaChanged and + * onTranslationXChanged callbacks directly to the provided animatedView. + */ +class StatusBarSystemEventAnimator @JvmOverloads constructor( + val animatedView: View, + resources: Resources, + isAnimationRunning: Boolean = false +) : StatusBarSystemEventDefaultAnimator( + resources = resources, + onAlphaChanged = animatedView::setAlpha, + onTranslationXChanged = animatedView::setTranslationX, + isAnimationRunning = isAnimationRunning +) /** * Tied directly to [SystemStatusAnimationScheduler]. Any StatusBar-like thing (keyguard, collapsed - * status bar fragment), can just feed this an animatable view to get the default system status - * animation. + * status bar fragment), can use this Animator to get the default system status animation. It simply + * needs to implement the onAlphaChanged and onTranslationXChanged callbacks. * * This animator relies on resources, and should be recreated whenever resources are updated. While * this class could be used directly as the animation callback, it's probably best to forward calls * to it so that it can be recreated at any moment without needing to remove/add callback. */ -class StatusBarSystemEventAnimator( - val animatedView: View, - resources: Resources + +open class StatusBarSystemEventDefaultAnimator @JvmOverloads constructor( + resources: Resources, + private val onAlphaChanged: (Float) -> Unit, + private val onTranslationXChanged: (Float) -> Unit, + var isAnimationRunning: Boolean = false ) : SystemStatusAnimationCallback { private val translationXIn: Int = resources.getDimensionPixelSize( R.dimen.ongoing_appops_chip_animation_in_status_bar_translation_x) @@ -46,18 +66,19 @@ class StatusBarSystemEventAnimator( R.dimen.ongoing_appops_chip_animation_out_status_bar_translation_x) override fun onSystemEventAnimationBegin(): Animator { + isAnimationRunning = true val moveOut = ValueAnimator.ofFloat(0f, 1f).apply { duration = 23.frames interpolator = STATUS_BAR_X_MOVE_OUT addUpdateListener { - animatedView.translationX = -(translationXIn * animatedValue as Float) + onTranslationXChanged(-(translationXIn * animatedValue as Float)) } } val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply { duration = 8.frames interpolator = null addUpdateListener { - animatedView.alpha = animatedValue as Float + onAlphaChanged(animatedValue as Float) } } @@ -67,13 +88,13 @@ class StatusBarSystemEventAnimator( } override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator { - animatedView.translationX = translationXOut.toFloat() + onTranslationXChanged(translationXOut.toFloat()) val moveIn = ValueAnimator.ofFloat(1f, 0f).apply { duration = 23.frames startDelay = 7.frames interpolator = STATUS_BAR_X_MOVE_IN addUpdateListener { - animatedView.translationX = translationXOut * animatedValue as Float + onTranslationXChanged(translationXOut * animatedValue as Float) } } val alphaIn = ValueAnimator.ofFloat(0f, 1f).apply { @@ -81,13 +102,14 @@ class StatusBarSystemEventAnimator( startDelay = 11.frames interpolator = null addUpdateListener { - animatedView.alpha = animatedValue as Float + onAlphaChanged(animatedValue as Float) } } val animatorSet = AnimatorSet() animatorSet.playTogether(moveIn, alphaIn) - + animatorSet.doOnEnd { isAnimationRunning = false } + animatorSet.doOnCancel { isAnimationRunning = false } return animatorSet } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileViewLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileViewLog.kt new file mode 100644 index 000000000000..e594a8a5efd9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileViewLog.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.dagger + +import javax.inject.Qualifier + +/** Logs for changes with the new mobile views. */ +@Qualifier +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class MobileViewLog diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 44647515a6e5..adfea80715a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -148,5 +148,19 @@ abstract class StatusBarPipelineModule { fun provideMobileInputLogBuffer(factory: LogBufferFactory): LogBuffer { return factory.create("MobileInputLog", 100) } + + @Provides + @SysUISingleton + @MobileViewLog + fun provideMobileViewLogBuffer(factory: LogBufferFactory): LogBuffer { + return factory.create("MobileViewLog", 100) + } + + @Provides + @SysUISingleton + @VerboseMobileViewLog + fun provideVerboseMobileViewLogBuffer(factory: LogBufferFactory): LogBuffer { + return factory.create("VerboseMobileViewLog", 100) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/VerboseMobileViewLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/VerboseMobileViewLog.kt new file mode 100644 index 000000000000..b98789807dd3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/VerboseMobileViewLog.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.dagger + +import javax.inject.Qualifier + +/** Logs for **verbose** changes with the new mobile views. */ +@Qualifier +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class VerboseMobileViewLog diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt index 3cbd2b76c248..159f689de370 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.pipeline.mobile.shared +package com.android.systemui.statusbar.pipeline.mobile.data import android.net.Network import android.net.NetworkCapabilities @@ -133,24 +133,6 @@ constructor( ) } - fun logUiAdapterSubIdsUpdated(subs: List<Int>) { - buffer.log( - TAG, - LogLevel.INFO, - { str1 = subs.toString() }, - { "Sub IDs in MobileUiAdapter updated internally: $str1" }, - ) - } - - fun logUiAdapterSubIdsSentToIconController(subs: List<Int>) { - buffer.log( - TAG, - LogLevel.INFO, - { str1 = subs.toString() }, - { "Sub IDs in MobileUiAdapter being sent to icon controller: $str1" }, - ) - } - fun logCarrierConfigChanged(subId: Int) { buffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt index bb3b9b2166c3..efdce062bb37 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt @@ -30,8 +30,8 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope 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 96b96f14d6aa..e182bc66081a 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 @@ -36,6 +36,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType @@ -47,7 +48,6 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameMo import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index 53a208cd171e..f97e41c018f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -45,11 +45,11 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger 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.data.repository.MobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt index da63ab10f733..075e6ec11ae7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -22,7 +22,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import java.io.PrintWriter import javax.inject.Inject @@ -55,17 +54,14 @@ constructor( interactor: MobileIconsInteractor, private val iconController: StatusBarIconController, private val iconsViewModelFactory: MobileIconsViewModel.Factory, - private val logger: MobileInputLogger, + private val logger: MobileViewLogger, @Application private val scope: CoroutineScope, private val statusBarPipelineFlags: StatusBarPipelineFlags, ) : CoreStartable { private val mobileSubIds: Flow<List<Int>> = - interactor.filteredSubscriptions - .mapLatest { subscriptions -> - subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId } - } - .distinctUntilChanged() - .onEach { logger.logUiAdapterSubIdsUpdated(it) } + interactor.filteredSubscriptions.mapLatest { subscriptions -> + subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId } + } /** * We expose the list of tracked subscriptions as a flow of a list of ints, where each int is @@ -75,7 +71,10 @@ constructor( * NOTE: this should go away as the view presenter learns more about this data pipeline */ private val mobileSubIdsState: StateFlow<List<Int>> = - mobileSubIds.stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + mobileSubIds + .distinctUntilChanged() + .onEach { logger.logUiAdapterSubIdsUpdated(it) } + .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) /** In order to keep the logs tame, we will reuse the same top-level mobile icons view model */ val mobileIconsViewModel = iconsViewModelFactory.create(mobileSubIdsState) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt new file mode 100644 index 000000000000..90dff23c637c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui + +import android.view.View +import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.statusbar.pipeline.dagger.MobileViewLog +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel +import java.io.PrintWriter +import javax.inject.Inject + +/** Logs for changes with the new mobile views. */ +@SysUISingleton +class MobileViewLogger +@Inject +constructor( + @MobileViewLog private val buffer: LogBuffer, + dumpManager: DumpManager, +) : Dumpable { + init { + dumpManager.registerNormalDumpable(this) + } + + private val collectionStatuses = mutableMapOf<String, Boolean>() + + fun logUiAdapterSubIdsUpdated(subs: List<Int>) { + buffer.log( + TAG, + LogLevel.INFO, + { str1 = subs.toString() }, + { "Sub IDs in MobileUiAdapter updated internally: $str1" }, + ) + } + + fun logUiAdapterSubIdsSentToIconController(subs: List<Int>) { + buffer.log( + TAG, + LogLevel.INFO, + { str1 = subs.toString() }, + { "Sub IDs in MobileUiAdapter being sent to icon controller: $str1" }, + ) + } + + fun logNewViewBinding(view: View, viewModel: LocationBasedMobileViewModel) { + buffer.log( + TAG, + LogLevel.INFO, + { + str1 = view.getIdForLogging() + str2 = viewModel.getIdForLogging() + str3 = viewModel.locationName + }, + { "New view binding. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, + ) + } + + fun logCollectionStarted(view: View, viewModel: LocationBasedMobileViewModel) { + collectionStatuses[view.getIdForLogging()] = true + buffer.log( + TAG, + LogLevel.INFO, + { + str1 = view.getIdForLogging() + str2 = viewModel.getIdForLogging() + str3 = viewModel.locationName + }, + { "Collection started. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, + ) + } + + fun logCollectionStopped(view: View, viewModel: LocationBasedMobileViewModel) { + collectionStatuses[view.getIdForLogging()] = false + buffer.log( + TAG, + LogLevel.INFO, + { + str1 = view.getIdForLogging() + str2 = viewModel.getIdForLogging() + str3 = viewModel.locationName + }, + { "Collection stopped. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, + ) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("Collection statuses per view:---") + collectionStatuses.forEach { viewId, isCollecting -> + pw.println("viewId=$viewId, isCollecting=$isCollecting") + } + } + + companion object { + fun Any.getIdForLogging(): String { + // The identityHashCode is guaranteed to be constant for the lifetime of the object. + return Integer.toHexString(System.identityHashCode(this)) + } + } +} + +private const val TAG = "MobileViewLogger" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt new file mode 100644 index 000000000000..f67bc8f14447 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui + +import android.view.View +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.statusbar.pipeline.dagger.VerboseMobileViewLog +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger.Companion.getIdForLogging +import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel +import javax.inject.Inject + +/** + * Logs for **verbose** changes with the new mobile views. + * + * This is a hopefully temporary log until we resolve some open bugs (b/267236367, b/269565345, + * b/270300839). + */ +@SysUISingleton +class VerboseMobileViewLogger +@Inject +constructor( + @VerboseMobileViewLog private val buffer: LogBuffer, +) { + fun logBinderReceivedSignalIcon(parentView: View, subId: Int, icon: SignalIconModel) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = parentView.getIdForLogging() + int1 = subId + int2 = icon.level + bool1 = icon.showExclamationMark + }, + { + "Binder[subId=$int1, viewId=$str1] received new signal icon: " + + "level=$int2 showExclamation=$bool1" + }, + ) + } + + fun logBinderReceivedNetworkTypeIcon(parentView: View, subId: Int, icon: Icon.Resource?) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = parentView.getIdForLogging() + int1 = subId + bool1 = icon != null + int2 = icon?.res ?: -1 + }, + { + "Binder[subId=$int1, viewId=$str1] received new network type icon: " + + if (bool1) "resId=$int2" else "null" + }, + ) + } +} + +private const val TAG = "VerboseMobileViewLogger" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index db585e68d185..5b7d45b55c5c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -36,8 +36,10 @@ import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -48,6 +50,7 @@ object MobileIconBinder { fun bind( view: ViewGroup, viewModel: LocationBasedMobileViewModel, + logger: MobileViewLogger, ): ModernStatusBarViewBinding { val mobileGroupView = view.requireViewById<ViewGroup>(R.id.mobile_group) val activityContainer = view.requireViewById<View>(R.id.inout_container) @@ -70,8 +73,13 @@ object MobileIconBinder { val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) + var isCollecting: Boolean = false + view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { + logger.logCollectionStarted(view, viewModel) + isCollecting = true + launch { visibilityState.collect { state -> when (state) { @@ -96,6 +104,11 @@ object MobileIconBinder { // Set the icon for the triangle launch { viewModel.icon.distinctUntilChanged().collect { icon -> + viewModel.verboseLogger?.logBinderReceivedSignalIcon( + view, + viewModel.subscriptionId, + icon, + ) mobileDrawable.level = SignalDrawable.getState( icon.level, @@ -114,6 +127,11 @@ object MobileIconBinder { // Set the network type icon launch { viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId -> + viewModel.verboseLogger?.logBinderReceivedNetworkTypeIcon( + view, + viewModel.subscriptionId, + dataTypeId, + ) dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) } networkTypeView.visibility = if (dataTypeId != null) VISIBLE else GONE } @@ -150,6 +168,13 @@ object MobileIconBinder { } launch { decorTint.collect { tint -> dotView.setDecorColor(tint) } } + + try { + awaitCancellation() + } finally { + isCollecting = false + logger.logCollectionStopped(view, viewModel) + } } } @@ -175,6 +200,10 @@ object MobileIconBinder { } decorTint.value = newTint } + + override fun isCollecting(): Boolean { + return isCollecting + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt index ed9a1884a7b4..4144293d5ccd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -20,6 +20,8 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import com.android.systemui.R +import com.android.systemui.statusbar.StatusBarIconView.getVisibleStateString +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView @@ -31,6 +33,15 @@ class ModernStatusBarMobileView( var subId: Int = -1 + override fun toString(): String { + return "ModernStatusBarMobileView(" + + "slot='$slot', " + + "subId=$subId, " + + "isCollecting=${binding.isCollecting()}, " + + "visibleState=${getVisibleStateString(visibleState)}); " + + "viewString=${super.toString()}" + } + companion object { /** @@ -40,6 +51,7 @@ class ModernStatusBarMobileView( @JvmStatic fun constructAndBind( context: Context, + logger: MobileViewLogger, slot: String, viewModel: LocationBasedMobileViewModel, ): ModernStatusBarMobileView { @@ -48,7 +60,8 @@ class ModernStatusBarMobileView( as ModernStatusBarMobileView) .also { it.subId = viewModel.subscriptionId - it.initView(slot) { MobileIconBinder.bind(it, viewModel) } + it.initView(slot) { MobileIconBinder.bind(it, viewModel, logger) } + logger.logNewViewBinding(it, viewModel) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt index 8e103f7bee2f..f775940140cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.graphics.Color import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger /** * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This @@ -26,11 +27,15 @@ import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags * * @param commonImpl for convenience, this class wraps a base interface that can provides all of the * common implementations between locations. See [MobileIconViewModel] + * @property locationName the name of the location of this VM, used for logging. + * @property verboseLogger an optional logger to log extremely verbose view updates. */ abstract class LocationBasedMobileViewModel( val commonImpl: MobileIconViewModelCommon, statusBarPipelineFlags: StatusBarPipelineFlags, debugTint: Int, + val locationName: String, + val verboseLogger: VerboseMobileViewLogger?, ) : MobileIconViewModelCommon by commonImpl { val useDebugColoring: Boolean = statusBarPipelineFlags.useDebugColoring() @@ -45,11 +50,16 @@ abstract class LocationBasedMobileViewModel( fun viewModelForLocation( commonImpl: MobileIconViewModelCommon, statusBarPipelineFlags: StatusBarPipelineFlags, + verboseMobileViewLogger: VerboseMobileViewLogger, loc: StatusBarLocation, ): LocationBasedMobileViewModel = when (loc) { StatusBarLocation.HOME -> - HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags) + HomeMobileIconViewModel( + commonImpl, + statusBarPipelineFlags, + verboseMobileViewLogger, + ) StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl, statusBarPipelineFlags) StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, statusBarPipelineFlags) @@ -60,20 +70,41 @@ abstract class LocationBasedMobileViewModel( class HomeMobileIconViewModel( commonImpl: MobileIconViewModelCommon, statusBarPipelineFlags: StatusBarPipelineFlags, + verboseMobileViewLogger: VerboseMobileViewLogger, ) : MobileIconViewModelCommon, - LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.CYAN) + LocationBasedMobileViewModel( + commonImpl, + statusBarPipelineFlags, + debugTint = Color.CYAN, + locationName = "Home", + verboseMobileViewLogger, + ) class QsMobileIconViewModel( commonImpl: MobileIconViewModelCommon, statusBarPipelineFlags: StatusBarPipelineFlags, ) : MobileIconViewModelCommon, - LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.GREEN) + LocationBasedMobileViewModel( + commonImpl, + statusBarPipelineFlags, + debugTint = Color.GREEN, + locationName = "QS", + // Only do verbose logging for the Home location. + verboseLogger = null, + ) class KeyguardMobileIconViewModel( commonImpl: MobileIconViewModelCommon, statusBarPipelineFlags: StatusBarPipelineFlags, ) : MobileIconViewModelCommon, - LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.MAGENTA) + LocationBasedMobileViewModel( + commonImpl, + statusBarPipelineFlags, + debugTint = Color.MAGENTA, + locationName = "Keyguard", + // Only do verbose logging for the Home location. + verboseLogger = null, + ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 049627899eff..dbb534b24471 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -49,7 +49,7 @@ interface MobileIconViewModelCommon { val contentDescription: Flow<ContentDescription> val roaming: Flow<Boolean> /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ - val networkTypeIcon: Flow<Icon?> + val networkTypeIcon: Flow<Icon.Resource?> val activityInVisible: Flow<Boolean> val activityOutVisible: Flow<Boolean> val activityContainerVisible: Flow<Boolean> @@ -161,7 +161,7 @@ constructor( ) .stateIn(scope, SharingStarted.WhileSubscribed(), false) - override val networkTypeIcon: Flow<Icon?> = + override val networkTypeIcon: Flow<Icon.Resource?> = combine( iconInteractor.networkTypeIconGroup, showNetworkTypeIcon, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 8cb52af336da..2b90065284d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -23,6 +23,8 @@ import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import javax.inject.Inject @@ -39,6 +41,8 @@ class MobileIconsViewModel @Inject constructor( val subscriptionIdsFlow: StateFlow<List<Int>>, + val logger: MobileViewLogger, + private val verboseLogger: VerboseMobileViewLogger, private val interactor: MobileIconsInteractor, private val airplaneModeInteractor: AirplaneModeInteractor, private val constants: ConnectivityConstants, @@ -66,6 +70,7 @@ constructor( return LocationBasedMobileViewModel.viewModelForLocation( common, statusBarPipelineFlags, + verboseLogger, location, ) } @@ -79,6 +84,8 @@ constructor( class Factory @Inject constructor( + private val logger: MobileViewLogger, + private val verboseLogger: VerboseMobileViewLogger, private val interactor: MobileIconsInteractor, private val airplaneModeInteractor: AirplaneModeInteractor, private val constants: ConnectivityConstants, @@ -88,6 +95,8 @@ constructor( fun create(subscriptionIdsFlow: StateFlow<List<Int>>): MobileIconsViewModel { return MobileIconsViewModel( subscriptionIdsFlow, + logger, + verboseLogger, interactor, airplaneModeInteractor, constants, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt index f67876b50233..81f8683411ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt @@ -37,4 +37,7 @@ interface ModernStatusBarViewBinding { /** Notifies that the decor tint has been updated (used only for the dot). */ fun onDecorTintChanged(newTint: Int) + + /** Returns true if the binding between the view and view-model is currently collecting. */ + fun isCollecting(): Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt index b1e28129a690..1a1340484bfc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt @@ -36,7 +36,7 @@ open class ModernStatusBarView(context: Context, attrs: AttributeSet?) : BaseStatusBarFrameLayout(context, attrs) { private lateinit var slot: String - private lateinit var binding: ModernStatusBarViewBinding + internal lateinit var binding: ModernStatusBarViewBinding @StatusBarIconView.VisibleState private var iconVisibleState: Int = STATE_HIDDEN diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt index 2aff12c8721d..9e8c814ca2a9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt @@ -34,6 +34,7 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarV import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged @@ -74,8 +75,12 @@ object WifiViewBinder { val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) + var isCollecting: Boolean = false + view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { + isCollecting = true + launch { visibilityState.collect { visibilityState -> groupView.isVisible = visibilityState == STATE_ICON @@ -127,6 +132,12 @@ object WifiViewBinder { airplaneSpacer.isVisible = visible } } + + try { + awaitCancellation() + } finally { + isCollecting = false + } } } @@ -152,6 +163,10 @@ object WifiViewBinder { } decorTint.value = newTint } + + override fun isCollecting(): Boolean { + return isCollecting + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt index 7a734862fe1b..f23e10287164 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt @@ -21,6 +21,7 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import com.android.systemui.R +import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel @@ -33,6 +34,15 @@ class ModernStatusBarWifiView( context: Context, attrs: AttributeSet?, ) : ModernStatusBarView(context, attrs) { + + override fun toString(): String { + return "ModernStatusBarWifiView(" + + "slot='$slot', " + + "isCollecting=${binding.isCollecting()}, " + + "visibleState=${StatusBarIconView.getVisibleStateString(visibleState)}); " + + "viewString=${super.toString()}" + } + companion object { /** * Inflates a new instance of [ModernStatusBarWifiView], binds it to a view model, and @@ -45,12 +55,9 @@ class ModernStatusBarWifiView( slot: String, wifiViewModel: LocationBasedWifiViewModel, ): ModernStatusBarWifiView { - return ( - LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null) - as ModernStatusBarWifiView - ).also { - it.initView(slot) { WifiViewBinder.bind(it, wifiViewModel) } - } + return (LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null) + as ModernStatusBarWifiView) + .also { it.initView(slot) { WifiViewBinder.bind(it, wifiViewModel) } } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java index 7acdaffb48c4..01fabcc8bc1e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java @@ -22,13 +22,18 @@ import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_LOCKED import android.annotation.Nullable; import android.hardware.devicestate.DeviceStateManager; import android.os.Trace; -import android.util.Log; +import android.util.IndentingPrintWriter; + +import androidx.annotation.NonNull; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; +import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dump.DumpManager; import com.android.systemui.util.wrapper.RotationPolicyWrapper; +import java.io.PrintWriter; import java.util.concurrent.Executor; import javax.inject.Inject; @@ -39,19 +44,19 @@ import javax.inject.Inject; */ @SysUISingleton public final class DeviceStateRotationLockSettingController - implements Listenable, RotationLockController.RotationLockControllerCallback { - - private static final String TAG = "DSRotateLockSettingCon"; + implements Listenable, RotationLockController.RotationLockControllerCallback, Dumpable { private final RotationPolicyWrapper mRotationPolicyWrapper; private final DeviceStateManager mDeviceStateManager; private final Executor mMainExecutor; private final DeviceStateRotationLockSettingsManager mDeviceStateRotationLockSettingsManager; + private final DeviceStateRotationLockSettingControllerLogger mLogger; // On registration for DeviceStateCallback, we will receive a callback with the current state // and this will be initialized. private int mDeviceState = -1; - @Nullable private DeviceStateManager.DeviceStateCallback mDeviceStateCallback; + @Nullable + private DeviceStateManager.DeviceStateCallback mDeviceStateCallback; private DeviceStateRotationLockSettingsManager.DeviceStateRotationLockSettingsListener mDeviceStateRotationLockSettingsListener; @@ -60,21 +65,27 @@ public final class DeviceStateRotationLockSettingController RotationPolicyWrapper rotationPolicyWrapper, DeviceStateManager deviceStateManager, @Main Executor executor, - DeviceStateRotationLockSettingsManager deviceStateRotationLockSettingsManager) { + DeviceStateRotationLockSettingsManager deviceStateRotationLockSettingsManager, + DeviceStateRotationLockSettingControllerLogger logger, + DumpManager dumpManager) { mRotationPolicyWrapper = rotationPolicyWrapper; mDeviceStateManager = deviceStateManager; mMainExecutor = executor; mDeviceStateRotationLockSettingsManager = deviceStateRotationLockSettingsManager; + mLogger = logger; + dumpManager.registerDumpable(this); } @Override public void setListening(boolean listening) { + mLogger.logListeningChange(listening); if (listening) { // Note that this is called once with the initial state of the device, even if there // is no user action. mDeviceStateCallback = this::updateDeviceState; mDeviceStateManager.registerCallback(mMainExecutor, mDeviceStateCallback); - mDeviceStateRotationLockSettingsListener = () -> readPersistedSetting(mDeviceState); + mDeviceStateRotationLockSettingsListener = () -> + readPersistedSetting("deviceStateRotationLockChange", mDeviceState); mDeviceStateRotationLockSettingsManager.registerListener( mDeviceStateRotationLockSettingsListener); } else { @@ -89,35 +100,28 @@ public final class DeviceStateRotationLockSettingController } @Override - public void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible) { - if (mDeviceState == -1) { - Log.wtf(TAG, "Device state was not initialized."); + public void onRotationLockStateChanged(boolean newRotationLocked, boolean affordanceVisible) { + int deviceState = mDeviceState; + boolean currentRotationLocked = mDeviceStateRotationLockSettingsManager + .isRotationLocked(deviceState); + mLogger.logRotationLockStateChanged(deviceState, newRotationLocked, currentRotationLocked); + if (deviceState == -1) { return; } - - if (rotationLocked - == mDeviceStateRotationLockSettingsManager.isRotationLocked(mDeviceState)) { - Log.v(TAG, "Rotation lock same as the current setting, no need to update."); + if (newRotationLocked == currentRotationLocked) { return; } - - saveNewRotationLockSetting(rotationLocked); + saveNewRotationLockSetting(newRotationLocked); } private void saveNewRotationLockSetting(boolean isRotationLocked) { - Log.v( - TAG, - "saveNewRotationLockSetting [state=" - + mDeviceState - + "] [isRotationLocked=" - + isRotationLocked - + "]"); - - mDeviceStateRotationLockSettingsManager.updateSetting(mDeviceState, isRotationLocked); + int deviceState = mDeviceState; + mLogger.logSaveNewRotationLockSetting(isRotationLocked, deviceState); + mDeviceStateRotationLockSettingsManager.updateSetting(deviceState, isRotationLocked); } private void updateDeviceState(int state) { - Log.v(TAG, "updateDeviceState [state=" + state + "]"); + mLogger.logUpdateDeviceState(mDeviceState, state); if (Trace.isEnabled()) { Trace.traceBegin( Trace.TRACE_TAG_APP, "updateDeviceState [state=" + state + "]"); @@ -127,22 +131,26 @@ public final class DeviceStateRotationLockSettingController return; } - readPersistedSetting(state); + readPersistedSetting("updateDeviceState", state); } finally { Trace.endSection(); } } - private void readPersistedSetting(int state) { + private void readPersistedSetting(String caller, int state) { int rotationLockSetting = mDeviceStateRotationLockSettingsManager.getRotationLockSetting(state); + boolean shouldBeLocked = rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_LOCKED; + boolean isLocked = mRotationPolicyWrapper.isRotationLocked(); + + mLogger.readPersistedSetting(caller, state, rotationLockSetting, shouldBeLocked, isLocked); + if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) { // This should not happen. Device states that have an ignored setting, should also // specify a fallback device state which is not ignored. // We won't handle this device state. The same rotation lock setting as before should // apply and any changes to the rotation lock setting will be written for the previous // valid device state. - Log.w(TAG, "Missing fallback. Ignoring new device state: " + state); return; } @@ -150,9 +158,18 @@ public final class DeviceStateRotationLockSettingController mDeviceState = state; // Update the rotation policy, if needed, for this new device state - boolean newRotationLockSetting = rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_LOCKED; - if (newRotationLockSetting != mRotationPolicyWrapper.isRotationLocked()) { - mRotationPolicyWrapper.setRotationLock(newRotationLockSetting); + if (shouldBeLocked != isLocked) { + mRotationPolicyWrapper.setRotationLock(shouldBeLocked); } } + + @Override + public void dump(@NonNull PrintWriter printWriter, @NonNull String[] args) { + IndentingPrintWriter pw = new IndentingPrintWriter(printWriter); + mDeviceStateRotationLockSettingsManager.dump(pw); + pw.println("DeviceStateRotationLockSettingController"); + pw.increaseIndent(); + pw.println("mDeviceState: " + mDeviceState); + pw.decreaseIndent(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerLogger.kt new file mode 100644 index 000000000000..aa502bc48149 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerLogger.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import android.content.Context +import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_IGNORED +import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_LOCKED +import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_UNLOCKED +import com.android.internal.R +import com.android.systemui.log.dagger.DeviceStateAutoRotationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import javax.inject.Inject + +class DeviceStateRotationLockSettingControllerLogger +@Inject +constructor(@DeviceStateAutoRotationLog private val logBuffer: LogBuffer, context: Context) { + + private val foldedStates = context.resources.getIntArray(R.array.config_foldedDeviceStates) + private val halfFoldedStates = + context.resources.getIntArray(R.array.config_halfFoldedDeviceStates) + private val unfoldedStates = context.resources.getIntArray(R.array.config_openDeviceStates) + + fun logListeningChange(listening: Boolean) { + logBuffer.log(TAG, VERBOSE, { bool1 = listening }, { "setListening: $bool1" }) + } + + fun logRotationLockStateChanged( + state: Int, + newRotationLocked: Boolean, + currentRotationLocked: Boolean + ) { + logBuffer.log( + TAG, + VERBOSE, + { + int1 = state + bool1 = newRotationLocked + bool2 = currentRotationLocked + }, + { + "onRotationLockStateChanged: " + + "state=$int1 [${int1.toDevicePostureString()}], " + + "newRotationLocked=$bool1, " + + "currentRotationLocked=$bool2" + } + ) + } + + fun logSaveNewRotationLockSetting(isRotationLocked: Boolean, state: Int) { + logBuffer.log( + TAG, + VERBOSE, + { + bool1 = isRotationLocked + int1 = state + }, + { "saveNewRotationLockSetting: isRotationLocked=$bool1, state=$int1" } + ) + } + + fun logUpdateDeviceState(currentState: Int, newState: Int) { + logBuffer.log( + TAG, + VERBOSE, + { + int1 = currentState + int2 = newState + }, + { + "updateDeviceState: " + + "current=$int1 [${int1.toDevicePostureString()}], " + + "new=$int2 [${int2.toDevicePostureString()}]" + } + ) + } + + fun readPersistedSetting( + caller: String, + state: Int, + rotationLockSetting: Int, + shouldBeLocked: Boolean, + isLocked: Boolean + ) { + logBuffer.log( + TAG, + VERBOSE, + { + str1 = caller + int1 = state + int2 = rotationLockSetting + bool1 = shouldBeLocked + bool2 = isLocked + }, + { + "readPersistedSetting: " + + "caller=$str1, " + + "state=$int1 [${int1.toDevicePostureString()}], " + + "rotationLockSettingForState: ${int2.toRotationLockSettingString()}, " + + "shouldBeLocked=$bool1, " + + "isLocked=$bool2" + } + ) + } + + private fun Int.toDevicePostureString(): String { + return when (this) { + in foldedStates -> "Folded" + in unfoldedStates -> "Unfolded" + in halfFoldedStates -> "Half-Folded" + -1 -> "Uninitialized" + else -> "Unknown" + } + } +} + +private fun Int.toRotationLockSettingString(): String { + return when (this) { + DEVICE_STATE_ROTATION_LOCK_IGNORED -> "IGNORED" + DEVICE_STATE_ROTATION_LOCK_LOCKED -> "LOCKED" + DEVICE_STATE_ROTATION_LOCK_UNLOCKED -> "UNLOCKED" + else -> "Unknown" + } +} + +private const val TAG = "DSRotateLockSettingCon" diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index 030c54fbd87d..9952cfd4e85b 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -62,8 +62,6 @@ constructor( 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() @@ -106,14 +104,6 @@ constructor( stylusCallbacks.remove(callback) } - fun registerBatteryCallback(callback: StylusBatteryCallback) { - stylusBatteryCallbacks.add(callback) - } - - fun unregisterBatteryCallback(callback: StylusBatteryCallback) { - stylusBatteryCallbacks.remove(callback) - } - override fun onInputDeviceAdded(deviceId: Int) { if (!hasStarted) return @@ -195,7 +185,7 @@ constructor( "${device.address}: $isCharging" } - executeStylusBatteryCallbacks { cb -> + executeStylusCallbacks { cb -> cb.onStylusBluetoothChargingStateChanged(inputDeviceId, device, isCharging) } } @@ -221,7 +211,7 @@ constructor( onStylusUsed() } - executeStylusBatteryCallbacks { cb -> + executeStylusCallbacks { cb -> cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState) } } @@ -329,10 +319,6 @@ constructor( stylusCallbacks.forEach(run) } - private fun executeStylusBatteryCallbacks(run: (cb: StylusBatteryCallback) -> Unit) { - stylusBatteryCallbacks.forEach(run) - } - private fun registerBatteryListener(deviceId: Int) { try { inputManager.addInputDeviceBatteryListener(deviceId, executor, this) @@ -378,13 +364,6 @@ constructor( fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {} fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {} fun onStylusFirstUsed() {} - } - - /** - * 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, diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt index 27cafb10c07d..3667392b515e 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt @@ -37,7 +37,7 @@ constructor( private val inputManager: InputManager, private val stylusUsiPowerUi: StylusUsiPowerUI, private val featureFlags: FeatureFlags, -) : CoreStartable, StylusManager.StylusCallback, StylusManager.StylusBatteryCallback { +) : CoreStartable, StylusManager.StylusCallback { override fun onStylusAdded(deviceId: Int) { // On some devices, the addition of a new internal stylus indicates the use of a @@ -74,7 +74,6 @@ constructor( stylusUsiPowerUi.init() stylusManager.registerCallback(this) - stylusManager.registerBatteryCallback(this) stylusManager.startListener() } diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbAccessoryUriActivity.java b/packages/SystemUI/src/com/android/systemui/usb/UsbAccessoryUriActivity.java index d5d3efd78d13..3a7ac9c8a8bd 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/UsbAccessoryUriActivity.java +++ b/packages/SystemUI/src/com/android/systemui/usb/UsbAccessoryUriActivity.java @@ -31,6 +31,9 @@ import android.view.WindowManager; import com.android.internal.app.AlertActivity; import com.android.internal.app.AlertController; import com.android.systemui.R; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; + +import javax.inject.Inject; /** * If the attached USB accessory has a URL associated with it, and that URL is valid, @@ -46,13 +49,27 @@ public class UsbAccessoryUriActivity extends AlertActivity private UsbAccessory mAccessory; private Uri mUri; + private final DeviceProvisionedController mDeviceProvisionedController; + + @Inject + UsbAccessoryUriActivity(DeviceProvisionedController deviceProvisionedController) { + mDeviceProvisionedController = deviceProvisionedController; + } + @Override public void onCreate(Bundle icicle) { - getWindow().addSystemFlags( + getWindow().addSystemFlags( WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); - super.onCreate(icicle); + super.onCreate(icicle); + + // Don't show this dialog during Setup Wizard + if (!mDeviceProvisionedController.isDeviceProvisioned()) { + Log.e(TAG, "device not provisioned"); + finish(); + return; + } - Intent intent = getIntent(); + Intent intent = getIntent(); mAccessory = (UsbAccessory)intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY); String uriString = intent.getStringExtra("uri"); mUri = (uriString == null ? null : Uri.parse(uriString)); diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java index 81ae6e851fb9..c72853ef37be 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java @@ -115,6 +115,17 @@ public abstract class SysUIConcurrencyModule { } /** + * Provide a Long running Executor. + */ + @Provides + @SysUISingleton + @LongRunning + public static DelayableExecutor provideLongRunningDelayableExecutor( + @LongRunning Looper looper) { + return new ExecutorImpl(looper); + } + + /** * Provide a Background-Thread Executor. */ @Provides diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 76a01b918952..54b30300ba49 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -41,7 +41,7 @@ import android.view.WindowManager; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -71,17 +71,16 @@ public class ImageWallpaper extends WallpaperService { private HandlerThread mWorker; // used for most tasks (call canvas.drawBitmap, load/unload the bitmap) - @Background - private final DelayableExecutor mBackgroundExecutor; + @LongRunning + private final DelayableExecutor mLongExecutor; // wait at least this duration before unloading the bitmap private static final int DELAY_UNLOAD_BITMAP = 2000; @Inject - public ImageWallpaper(@Background DelayableExecutor backgroundExecutor, - UserTracker userTracker) { + public ImageWallpaper(@LongRunning DelayableExecutor longExecutor, UserTracker userTracker) { super(); - mBackgroundExecutor = backgroundExecutor; + mLongExecutor = longExecutor; mUserTracker = userTracker; } @@ -131,7 +130,7 @@ public class ImageWallpaper extends WallpaperService { setFixedSizeAllowed(true); setShowForAllUsers(true); mWallpaperLocalColorExtractor = new WallpaperLocalColorExtractor( - mBackgroundExecutor, + mLongExecutor, new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() { @Override public void onColorsProcessed(List<RectF> regions, @@ -230,7 +229,7 @@ public class ImageWallpaper extends WallpaperService { } private void drawFrame() { - mBackgroundExecutor.execute(this::drawFrameSynchronized); + mLongExecutor.execute(this::drawFrameSynchronized); } private void drawFrameSynchronized() { @@ -285,7 +284,7 @@ public class ImageWallpaper extends WallpaperService { } private void unloadBitmapIfNotUsed() { - mBackgroundExecutor.execute(this::unloadBitmapIfNotUsedSynchronized); + mLongExecutor.execute(this::unloadBitmapIfNotUsedSynchronized); } private void unloadBitmapIfNotUsedSynchronized() { @@ -381,7 +380,7 @@ public class ImageWallpaper extends WallpaperService { * - the mini bitmap from color extractor is recomputed * - the DELAY_UNLOAD_BITMAP has passed */ - mBackgroundExecutor.executeDelayed( + mLongExecutor.executeDelayed( this::unloadBitmapIfNotUsedSynchronized, DELAY_UNLOAD_BITMAP); } // even if the bitmap cannot be loaded, call reportEngineShown diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java index 988fd710d2dc..1e8446f8df1d 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java @@ -29,7 +29,7 @@ import android.util.MathUtils; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.util.Assert; import java.io.FileDescriptor; @@ -66,8 +66,8 @@ public class WallpaperLocalColorExtractor { private final List<RectF> mPendingRegions = new ArrayList<>(); private final Set<RectF> mProcessedRegions = new ArraySet<>(); - @Background - private final Executor mBackgroundExecutor; + @LongRunning + private final Executor mLongExecutor; private final WallpaperLocalColorExtractorCallback mWallpaperLocalColorExtractorCallback; @@ -101,13 +101,13 @@ public class WallpaperLocalColorExtractor { /** * Creates a new color extractor. - * @param backgroundExecutor the executor on which the color extraction will be performed + * @param longExecutor the executor on which the color extraction will be performed * @param wallpaperLocalColorExtractorCallback an interface to handle the callbacks from * the color extractor. */ - public WallpaperLocalColorExtractor(@Background Executor backgroundExecutor, + public WallpaperLocalColorExtractor(@LongRunning Executor longExecutor, WallpaperLocalColorExtractorCallback wallpaperLocalColorExtractorCallback) { - mBackgroundExecutor = backgroundExecutor; + mLongExecutor = longExecutor; mWallpaperLocalColorExtractorCallback = wallpaperLocalColorExtractorCallback; } @@ -117,7 +117,7 @@ public class WallpaperLocalColorExtractor { * not recomputed. */ public void setDisplayDimensions(int displayWidth, int displayHeight) { - mBackgroundExecutor.execute(() -> + mLongExecutor.execute(() -> setDisplayDimensionsSynchronized(displayWidth, displayHeight)); } @@ -144,7 +144,7 @@ public class WallpaperLocalColorExtractor { * @param bitmap the new wallpaper */ public void onBitmapChanged(@NonNull Bitmap bitmap) { - mBackgroundExecutor.execute(() -> onBitmapChangedSynchronized(bitmap)); + mLongExecutor.execute(() -> onBitmapChangedSynchronized(bitmap)); } private void onBitmapChangedSynchronized(@NonNull Bitmap bitmap) { @@ -167,7 +167,7 @@ public class WallpaperLocalColorExtractor { * @param pages the total number of pages of the launcher */ public void onPageChanged(int pages) { - mBackgroundExecutor.execute(() -> onPageChangedSynchronized(pages)); + mLongExecutor.execute(() -> onPageChangedSynchronized(pages)); } private void onPageChangedSynchronized(int pages) { @@ -194,7 +194,7 @@ public class WallpaperLocalColorExtractor { */ public void addLocalColorsAreas(@NonNull List<RectF> regions) { if (regions.size() > 0) { - mBackgroundExecutor.execute(() -> addLocalColorsAreasSynchronized(regions)); + mLongExecutor.execute(() -> addLocalColorsAreasSynchronized(regions)); } else { Log.w(TAG, "Attempt to add colors with an empty list"); } @@ -218,7 +218,7 @@ public class WallpaperLocalColorExtractor { * @param regions The areas of interest in our wallpaper (in screen pixel coordinates) */ public void removeLocalColorAreas(@NonNull List<RectF> regions) { - mBackgroundExecutor.execute(() -> removeLocalColorAreasSynchronized(regions)); + mLongExecutor.execute(() -> removeLocalColorAreasSynchronized(regions)); } private void removeLocalColorAreasSynchronized(@NonNull List<RectF> regions) { @@ -236,7 +236,7 @@ public class WallpaperLocalColorExtractor { * Clean up the memory (in particular, the mini bitmap) used by this class. */ public void cleanUp() { - mBackgroundExecutor.execute(this::cleanUpSynchronized); + mLongExecutor.execute(this::cleanUpSynchronized); } private void cleanUpSynchronized() { diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java index 9600fd88a2a3..08f7eaee3011 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java @@ -25,7 +25,6 @@ import static android.service.notification.NotificationListenerService.REASON_GR import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE; import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; -import static com.android.systemui.flags.Flags.WM_BUBBLE_BAR; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -364,7 +363,6 @@ public class BubblesManager { }); } }; - mBubbles.setBubbleBarEnabled(featureFlags.isEnabled(WM_BUBBLE_BAR)); mBubbles.setSysuiProxy(mSysuiProxy); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index bd77c327e765..86ba30cee7a3 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -2210,6 +2210,32 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testPostureChangeToUnsupported_stopsFaceListeningState() { + // GIVEN device is listening for face + mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_CLOSED; + deviceInPostureStateClosed(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); + mTestableLooper.processAllMessages(); + keyguardIsVisible(); + + verifyFaceAuthenticateCall(); + + final CancellationSignal faceCancel = spy(mKeyguardUpdateMonitor.mFaceCancelSignal); + mKeyguardUpdateMonitor.mFaceCancelSignal = faceCancel; + KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class); + mKeyguardUpdateMonitor.registerCallback(callback); + + // WHEN device is opened + deviceInPostureStateOpened(); + mTestableLooper.processAllMessages(); + + // THEN face listening is stopped. + verify(faceCancel).cancel(); + verify(callback).onBiometricRunningStateChanged( + eq(false), eq(BiometricSourceType.FACE)); + } + + @Test public void testShouldListenForFace_withLockedDown_returnsFalse() throws RemoteException { keyguardNotGoingAway(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java index 47c91911e52a..52a70ee9cce2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java @@ -201,6 +201,13 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { assertThat(magnifierMediumButton.isSelected()).isTrue(); } + @Test + public void showSettingPanel_focusOnThePanel() { + mWindowMagnificationSettings.showSettingPanel(); + + assertThat(mSettingView.isFocused()).isTrue(); + } + private <T extends View> T getInternalView(@IdRes int idRes) { T view = mSettingView.findViewById(idRes); assertNotNull(view); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index b765ab3c5eac..a245c01d74de 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -25,7 +25,9 @@ import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.keyguard.logging.KeyguardLogger import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -109,6 +111,7 @@ class AuthRippleControllerTest : SysuiTestCase() { udfpsControllerProvider, statusBarStateController, featureFlags, + KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)), rippleView ) controller.init() diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java index 35039026fe9e..9d16185e75c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogTest.java @@ -45,8 +45,8 @@ import org.mockito.MockitoAnnotations; public class BroadcastDialogTest extends SysuiTestCase { private static final String CURRENT_BROADCAST_APP = "Music"; - private static final String SWITCH_APP = "Files by Google"; - private static final String TEST_PACKAGE = "com.google.android.apps.nbu.files"; + private static final String SWITCH_APP = "System UI"; + private static final String TEST_PACKAGE = "com.android.systemui"; private BroadcastDialog mBroadcastDialog; private View mDialogView; private TextView mTitle; @@ -59,6 +59,7 @@ public class BroadcastDialogTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mBroadcastDialog = new BroadcastDialog(mContext, mock(MediaOutputDialogFactory.class), CURRENT_BROADCAST_APP, TEST_PACKAGE, mock(UiEventLogger.class)); + mBroadcastDialog.show(); mDialogView = mBroadcastDialog.mDialogView; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java index faa5db4327a6..ab6d5b771d5a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java @@ -94,7 +94,9 @@ public class DistanceClassifierTest extends ClassifierTest { mClassifier.onTouchEvent(appendMoveEvent(1, 16, 3)); mClassifier.onTouchEvent(appendMoveEvent(1, 17, 300)); mClassifier.onTouchEvent(appendMoveEvent(1, 18, 301)); - mClassifier.onTouchEvent(appendUpEvent(1, 19, 501)); + mClassifier.onTouchEvent(appendMoveEvent(1, 19, 501)); + mClassifier.onTouchEvent(appendUpEvent(1, 19, 501)); //event will be dropped + assertThat(mClassifier.classifyGesture(0, 0.5, 1).isFalse()).isTrue(); } 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 2edc3d361316..8eadadff1ca5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java @@ -75,16 +75,17 @@ public class FalsingDataProviderTest extends ClassifierTest { } @Test - public void test_trackMotionEvents() { + public void test_trackMotionEvents_dropUpEvent() { mDataProvider.onMotionEvent(appendDownEvent(2, 9)); mDataProvider.onMotionEvent(appendMoveEvent(4, 7)); - mDataProvider.onMotionEvent(appendUpEvent(6, 5)); + mDataProvider.onMotionEvent(appendMoveEvent(6, 5)); + mDataProvider.onMotionEvent(appendUpEvent(0, 0)); // event will be dropped List<MotionEvent> motionEventList = mDataProvider.getRecentMotionEvents(); assertThat(motionEventList.size()).isEqualTo(3); assertThat(motionEventList.get(0).getActionMasked()).isEqualTo(MotionEvent.ACTION_DOWN); assertThat(motionEventList.get(1).getActionMasked()).isEqualTo(MotionEvent.ACTION_MOVE); - assertThat(motionEventList.get(2).getActionMasked()).isEqualTo(MotionEvent.ACTION_UP); + assertThat(motionEventList.get(2).getActionMasked()).isEqualTo(MotionEvent.ACTION_MOVE); assertThat(motionEventList.get(0).getEventTime()).isEqualTo(1L); assertThat(motionEventList.get(1).getEventTime()).isEqualTo(2L); assertThat(motionEventList.get(2).getEventTime()).isEqualTo(3L); @@ -97,6 +98,28 @@ public class FalsingDataProviderTest extends ClassifierTest { } @Test + public void test_trackMotionEvents_keepUpEvent() { + mDataProvider.onMotionEvent(appendDownEvent(2, 9)); + mDataProvider.onMotionEvent(appendMoveEvent(4, 7)); + mDataProvider.onMotionEvent(appendUpEvent(0, 0, 100)); + List<MotionEvent> motionEventList = mDataProvider.getRecentMotionEvents(); + + assertThat(motionEventList.size()).isEqualTo(3); + assertThat(motionEventList.get(0).getActionMasked()).isEqualTo(MotionEvent.ACTION_DOWN); + assertThat(motionEventList.get(1).getActionMasked()).isEqualTo(MotionEvent.ACTION_MOVE); + assertThat(motionEventList.get(2).getActionMasked()).isEqualTo(MotionEvent.ACTION_UP); + assertThat(motionEventList.get(0).getEventTime()).isEqualTo(1L); + assertThat(motionEventList.get(1).getEventTime()).isEqualTo(2L); + assertThat(motionEventList.get(2).getEventTime()).isEqualTo(100); + assertThat(motionEventList.get(0).getX()).isEqualTo(2f); + assertThat(motionEventList.get(1).getX()).isEqualTo(4f); + assertThat(motionEventList.get(2).getX()).isEqualTo(0f); + assertThat(motionEventList.get(0).getY()).isEqualTo(9f); + assertThat(motionEventList.get(1).getY()).isEqualTo(7f); + assertThat(motionEventList.get(2).getY()).isEqualTo(0f); + } + + @Test public void test_trackRecentMotionEvents() { mDataProvider.onMotionEvent(appendDownEvent(2, 9, 1)); mDataProvider.onMotionEvent(appendMoveEvent(4, 7, 800)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ZigZagClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/ZigZagClassifierTest.java index c343c20398e9..ae2b8bbb4ce6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ZigZagClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/ZigZagClassifierTest.java @@ -68,6 +68,15 @@ public class ZigZagClassifierTest extends ClassifierTest { } @Test + public void testPass_dropClosingUpEvent() { + appendMoveEvent(0, 0); + appendMoveEvent(0, 100); + appendMoveEvent(0, 200); + appendUpEvent(0, 180); // this event would push us over the maxDevianceY + assertThat(mClassifier.classifyGesture(0, 0.5, 1).isFalse()).isFalse(); + } + + @Test public void testPass_fewTouchesHorizontal() { assertThat(mClassifier.classifyGesture(0, 0.5, 1).isFalse()).isFalse(); appendMoveEvent(0, 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java index 2099281d694a..ffd75fb9bbc6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.app.RemoteAction; import android.content.ClipData; import android.content.ClipDescription; @@ -101,6 +102,9 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { private ArgumentCaptor<ClipboardOverlayView.ClipboardOverlayCallbacks> mOverlayCallbacksCaptor; private ClipboardOverlayView.ClipboardOverlayCallbacks mCallbacks; + @Captor + private ArgumentCaptor<AnimatorListenerAdapter> mAnimatorArgumentCaptor; + private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); @Before @@ -446,7 +450,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true); when(mClipboardUtils.isRemoteCopy(any(Context.class), any(ClipData.class), anyString())) .thenReturn(true); - when(mClipboardUtils.getAction(any(CharSequence.class), any(TextLinks.class), anyString())) + when(mClipboardUtils.getAction(any(TextLinks.class), anyString())) .thenReturn(Optional.of(Mockito.mock(RemoteAction.class))); when(mClipboardOverlayView.post(any(Runnable.class))).thenAnswer(new Answer<Object>() { @Override @@ -478,12 +482,16 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { when(mClipboardOverlayWindow.getWindowInsets()).thenReturn( getImeInsets(new Rect(0, 0, 0, 1))); mOverlayController.setClipData(mSampleClipData, ""); + Animator mockFadeoutAnimator = Mockito.mock(Animator.class); + when(mClipboardOverlayView.getMinimizedFadeoutAnimation()).thenReturn(mockFadeoutAnimator); verify(mClipboardOverlayView).setMinimized(true); verify(mClipboardOverlayView, never()).setMinimized(false); verify(mClipboardOverlayView, never()).showTextPreview(any(), anyBoolean()); mCallbacks.onMinimizedViewTapped(); + verify(mockFadeoutAnimator).addListener(mAnimatorArgumentCaptor.capture()); + mAnimatorArgumentCaptor.getValue().onAnimationEnd(mockFadeoutAnimator); verify(mClipboardOverlayView).setMinimized(false); verify(mClipboardOverlayView).showTextPreview("Test Item", false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java index aea6be3d468b..3d8f04e08825 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.when; @@ -77,6 +78,74 @@ public class ClipboardOverlayUtilsTest extends SysuiTestCase { @Test public void test_getAction_noLinks_returnsEmptyOptional() { + Optional<RemoteAction> action = + mClipboardUtils.getAction(Mockito.mock(TextLinks.class), "abc"); + + assertTrue(action.isEmpty()); + } + + @Test + public void test_getAction_returnsFirstLink() { + TextLinks links = getFakeTextLinksBuilder().build(); + RemoteAction actionA = constructRemoteAction("abc"); + RemoteAction actionB = constructRemoteAction("def"); + TextClassification classificationA = Mockito.mock(TextClassification.class); + when(classificationA.getActions()).thenReturn(Lists.newArrayList(actionA)); + TextClassification classificationB = Mockito.mock(TextClassification.class); + when(classificationB.getActions()).thenReturn(Lists.newArrayList(actionB)); + when(mTextClassifier.classifyText(anyString(), anyInt(), anyInt(), isNull())).thenReturn( + classificationA, classificationB); + + RemoteAction result = mClipboardUtils.getAction(links, "test").orElse(null); + + assertEquals(actionA, result); + } + + @Test + public void test_getAction_skipsMatchingComponent() { + TextLinks links = getFakeTextLinksBuilder().build(); + RemoteAction actionA = constructRemoteAction("abc"); + RemoteAction actionB = constructRemoteAction("def"); + TextClassification classificationA = Mockito.mock(TextClassification.class); + when(classificationA.getActions()).thenReturn(Lists.newArrayList(actionA)); + TextClassification classificationB = Mockito.mock(TextClassification.class); + when(classificationB.getActions()).thenReturn(Lists.newArrayList(actionB)); + when(mTextClassifier.classifyText(anyString(), anyInt(), anyInt(), isNull())).thenReturn( + classificationA, classificationB); + + RemoteAction result = mClipboardUtils.getAction(links, "abc").orElse(null); + + assertEquals(actionB, result); + } + + @Test + public void test_getAction_skipsShortEntity() { + TextLinks.Builder textLinks = new TextLinks.Builder("test text of length 22"); + final Map<String, Float> scores = new ArrayMap<>(); + scores.put(TextClassifier.TYPE_EMAIL, 1f); + textLinks.addLink(20, 22, scores); + textLinks.addLink(0, 22, scores); + + RemoteAction actionA = constructRemoteAction("abc"); + RemoteAction actionB = constructRemoteAction("def"); + TextClassification classificationA = Mockito.mock(TextClassification.class); + when(classificationA.getActions()).thenReturn(Lists.newArrayList(actionA)); + TextClassification classificationB = Mockito.mock(TextClassification.class); + when(classificationB.getActions()).thenReturn(Lists.newArrayList(actionB)); + when(mTextClassifier.classifyText(anyString(), eq(20), eq(22), isNull())).thenReturn( + classificationA); + when(mTextClassifier.classifyText(anyString(), eq(0), eq(22), isNull())).thenReturn( + classificationB); + + RemoteAction result = mClipboardUtils.getAction(textLinks.build(), "test").orElse(null); + + assertEquals(actionB, result); + } + + // TODO(b/267162944): Next four tests (marked "legacy") are obsolete once + // CLIPBOARD_MINIMIZED_LAYOUT flag is released and removed + @Test + public void test_getAction_noLinks_returnsEmptyOptional_legacy() { ClipData.Item item = new ClipData.Item("no text links"); item.setTextLinks(Mockito.mock(TextLinks.class)); @@ -86,8 +155,8 @@ public class ClipboardOverlayUtilsTest extends SysuiTestCase { } @Test - public void test_getAction_returnsFirstLink() { - when(mClipDataItem.getTextLinks()).thenReturn(getFakeTextLinks()); + public void test_getAction_returnsFirstLink_legacy() { + when(mClipDataItem.getTextLinks()).thenReturn(getFakeTextLinksBuilder().build()); when(mClipDataItem.getText()).thenReturn(""); RemoteAction actionA = constructRemoteAction("abc"); RemoteAction actionB = constructRemoteAction("def"); @@ -98,14 +167,14 @@ public class ClipboardOverlayUtilsTest extends SysuiTestCase { when(mTextClassifier.classifyText(anyString(), anyInt(), anyInt(), isNull())).thenReturn( classificationA, classificationB); - RemoteAction result = mClipboardUtils.getAction(mClipDataItem, "def").orElse(null); + RemoteAction result = mClipboardUtils.getAction(mClipDataItem, "test").orElse(null); assertEquals(actionA, result); } @Test - public void test_getAction_skipsMatchingComponent() { - when(mClipDataItem.getTextLinks()).thenReturn(getFakeTextLinks()); + public void test_getAction_skipsMatchingComponent_legacy() { + when(mClipDataItem.getTextLinks()).thenReturn(getFakeTextLinksBuilder().build()); when(mClipDataItem.getText()).thenReturn(""); RemoteAction actionA = constructRemoteAction("abc"); RemoteAction actionB = constructRemoteAction("def"); @@ -122,6 +191,33 @@ public class ClipboardOverlayUtilsTest extends SysuiTestCase { } @Test + public void test_getAction_skipsShortEntity_legacy() { + TextLinks.Builder textLinks = new TextLinks.Builder("test text of length 22"); + final Map<String, Float> scores = new ArrayMap<>(); + scores.put(TextClassifier.TYPE_EMAIL, 1f); + textLinks.addLink(20, 22, scores); + textLinks.addLink(0, 22, scores); + + when(mClipDataItem.getTextLinks()).thenReturn(textLinks.build()); + when(mClipDataItem.getText()).thenReturn(textLinks.build().getText()); + + RemoteAction actionA = constructRemoteAction("abc"); + RemoteAction actionB = constructRemoteAction("def"); + TextClassification classificationA = Mockito.mock(TextClassification.class); + when(classificationA.getActions()).thenReturn(Lists.newArrayList(actionA)); + TextClassification classificationB = Mockito.mock(TextClassification.class); + when(classificationB.getActions()).thenReturn(Lists.newArrayList(actionB)); + when(mTextClassifier.classifyText(anyString(), eq(20), eq(22), isNull())).thenReturn( + classificationA); + when(mTextClassifier.classifyText(anyString(), eq(0), eq(22), isNull())).thenReturn( + classificationB); + + RemoteAction result = mClipboardUtils.getAction(mClipDataItem, "test").orElse(null); + + assertEquals(actionB, result); + } + + @Test public void test_extra_withPackage_returnsTrue() { PersistableBundle b = new PersistableBundle(); b.putBoolean(ClipDescription.EXTRA_IS_REMOTE_DEVICE, true); @@ -184,12 +280,12 @@ public class ClipboardOverlayUtilsTest extends SysuiTestCase { return action; } - private static TextLinks getFakeTextLinks() { - TextLinks.Builder textLinks = new TextLinks.Builder("test"); + private static TextLinks.Builder getFakeTextLinksBuilder() { + TextLinks.Builder textLinks = new TextLinks.Builder("test text of length 22"); final Map<String, Float> scores = new ArrayMap<>(); scores.put(TextClassifier.TYPE_EMAIL, 1f); - textLinks.addLink(0, 0, scores); - textLinks.addLink(0, 0, scores); - return textLinks.build(); + textLinks.addLink(0, 22, scores); + textLinks.addLink(0, 22, scores); + return textLinks; } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java index eafe727ee7dc..afd9be5787c9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java @@ -107,4 +107,32 @@ public class SeekBarWithIconButtonsViewTest extends SysuiTestCase { assertThat(mSeekbar.getProgress()).isEqualTo(0); } + + @Test + public void setProgressStateLabels_getExpectedStateDescriptionOnInitialization() { + String[] stateLabels = new String[]{"1", "2", "3", "4", "5"}; + mIconDiscreteSliderLinearLayout.setMax(stateLabels.length); + mIconDiscreteSliderLinearLayout.setProgress(1); + mIconDiscreteSliderLinearLayout.setProgressStateLabels(stateLabels); + + final int currentProgress = mSeekbar.getProgress(); + final CharSequence stateDescription = mSeekbar.getStateDescription(); + + assertThat(currentProgress).isEqualTo(1); + assertThat(stateDescription).isEqualTo(stateLabels[currentProgress]); + } + + @Test + public void setProgressStateLabels_progressChanged_getExpectedStateDescription() { + String[] stateLabels = new String[]{"1", "2", "3", "4", "5"}; + mIconDiscreteSliderLinearLayout.setMax(stateLabels.length); + mIconDiscreteSliderLinearLayout.setProgressStateLabels(stateLabels); + mIconDiscreteSliderLinearLayout.setProgress(1); + + final int currentProgress = mSeekbar.getProgress(); + final CharSequence stateDescription = mSeekbar.getStateDescription(); + + assertThat(currentProgress).isEqualTo(1); + assertThat(stateDescription).isEqualTo(stateLabels[currentProgress]); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt index 6c23254941a8..0a9470617a5f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt @@ -1,6 +1,8 @@ package com.android.systemui.dreams +import android.animation.Animator import android.animation.AnimatorSet +import android.animation.ValueAnimator import android.testing.AndroidTestingRunner import android.view.View import androidx.test.filters.SmallTest @@ -10,13 +12,16 @@ import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransition import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.anyLong import org.mockito.Mockito.eq @@ -71,6 +76,19 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { } @Test + fun testExitAnimationUpdatesState() { + controller.startExitAnimations(animatorBuilder = { mockAnimator }) + + verify(stateController).setExitAnimationsRunning(true) + + val captor = argumentCaptor<Animator.AnimatorListener>() + verify(mockAnimator).addListener(captor.capture()) + + captor.value.onAnimationEnd(mockAnimator) + verify(stateController).setExitAnimationsRunning(false) + } + + @Test fun testWakeUpCallsExecutor() { val mockExecutor: DelayableExecutor = mock() val mockCallback: Runnable = mock() @@ -87,7 +105,7 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { fun testWakeUpAfterStartWillCancel() { val mockStartAnimator: AnimatorSet = mock() - controller.startEntryAnimations(animatorBuilder = { mockStartAnimator }) + controller.startEntryAnimations(false, animatorBuilder = { mockStartAnimator }) verify(mockStartAnimator, never()).cancel() @@ -100,4 +118,50 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { // animator. verify(mockStartAnimator, times(1)).cancel() } + + @Test + fun testEntryAnimations_translatesUpwards() { + val mockStartAnimator: AnimatorSet = mock() + + controller.startEntryAnimations( + /* downwards= */ false, + animatorBuilder = { mockStartAnimator } + ) + + val animatorCaptor = ArgumentCaptor.forClass(Animator::class.java) + verify(mockStartAnimator).playTogether(animatorCaptor.capture()) + + // Check if there's a ValueAnimator starting at the expected Y distance. + val animators: List<ValueAnimator> = animatorCaptor.allValues as List<ValueAnimator> + assertTrue( + animators.any { + // Call setCurrentFraction so the animated value jumps to the initial value. + it.setCurrentFraction(0f) + it.animatedValue == DREAM_IN_TRANSLATION_Y_DISTANCE.toFloat() + } + ) + } + + @Test + fun testEntryAnimations_translatesDownwards() { + val mockStartAnimator: AnimatorSet = mock() + + controller.startEntryAnimations( + /* downwards= */ true, + animatorBuilder = { mockStartAnimator } + ) + + val animatorCaptor = ArgumentCaptor.forClass(Animator::class.java) + verify(mockStartAnimator).playTogether(animatorCaptor.capture()) + + // Check if there's a ValueAnimator starting at the expected Y distance. + val animators: List<ValueAnimator> = animatorCaptor.allValues as List<ValueAnimator> + assertTrue( + animators.any { + // Call setCurrentFraction so the animated value jumps to the initial value. + it.setCurrentFraction(0f) + it.animatedValue == -DREAM_IN_TRANSLATION_Y_DISTANCE.toFloat() + } + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java index 6b095ffd3977..2a72e7d85d3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -17,6 +17,7 @@ package com.android.systemui.dreams; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -33,6 +34,7 @@ import android.view.ViewTreeObserver; import androidx.test.filters.SmallTest; +import com.android.dream.lowlight.LowLightTransitionCoordinator; import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.complication.ComplicationHostViewController; @@ -65,6 +67,9 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { DreamOverlayStatusBarViewController mDreamOverlayStatusBarViewController; @Mock + LowLightTransitionCoordinator mLowLightTransitionCoordinator; + + @Mock DreamOverlayContainerView mDreamOverlayContainerView; @Mock @@ -109,6 +114,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mComplicationHostViewController, mDreamOverlayContentView, mDreamOverlayStatusBarViewController, + mLowLightTransitionCoordinator, mBlurUtils, mHandler, mResources, @@ -200,7 +206,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mController.onViewAttached(); - verify(mAnimationsController).startEntryAnimations(); + verify(mAnimationsController).startEntryAnimations(false); verify(mAnimationsController, never()).cancelAnimations(); } @@ -210,11 +216,11 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mController.onViewAttached(); - verify(mAnimationsController, never()).startEntryAnimations(); + verify(mAnimationsController, never()).startEntryAnimations(anyBoolean()); } @Test - public void testSkipEntryAnimationsWhenExitingLowLight() { + public void testDownwardEntryAnimationsWhenExitingLowLight() { ArgumentCaptor<DreamOverlayStateController.Callback> callbackCaptor = ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); when(mStateController.isLowLightActive()).thenReturn(false); @@ -230,8 +236,14 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mController.onViewAttached(); // Entry animations should be started then immediately ended to skip to the end. - verify(mAnimationsController).startEntryAnimations(); - verify(mAnimationsController).endAnimations(); + verify(mAnimationsController).startEntryAnimations(true); + } + + @Test + public void testStartsExitAnimationsBeforeEnteringLowLight() { + mController.onBeforeEnterLowLight(); + + verify(mAnimationsController).startExitAnimations(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java index dcd8736711f6..068852de7a43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java @@ -22,6 +22,8 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.os.UserHandle; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.view.View; @@ -33,6 +35,8 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.util.settings.FakeSettings; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Test; @@ -96,6 +100,10 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { private ComplicationHostViewController mController; + private SecureSettings mSecureSettings; + + private static final int CURRENT_USER_ID = UserHandle.USER_SYSTEM; + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -108,12 +116,17 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { when(mViewHolder.getLayoutParams()).thenReturn(mComplicationLayoutParams); when(mComplicationView.getParent()).thenReturn(mComplicationHostView); + mSecureSettings = new FakeSettings(); + mSecureSettings.putFloatForUser( + Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f, CURRENT_USER_ID); + mController = new ComplicationHostViewController( mComplicationHostView, mLayoutEngine, mDreamOverlayStateController, mLifecycleOwner, - mViewModel); + mViewModel, + mSecureSettings); mController.init(); } @@ -188,6 +201,23 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { verify(mComplicationView, never()).setVisibility(View.INVISIBLE); } + @Test + public void testAnimationsDisabled_ComplicationsNeverSetToInvisible() { + //Disable animations + mController.mIsAnimationEnabled = false; + + final Observer<Collection<ComplicationViewModel>> observer = + captureComplicationViewModelsObserver(); + + // Add a complication before entry animations are finished. + final HashSet<ComplicationViewModel> complications = new HashSet<>( + Collections.singletonList(mComplicationViewModel)); + observer.onChanged(complications); + + // The complication view should not be set to invisible. + verify(mComplicationView, never()).setVisibility(View.INVISIBLE); + } + private Observer<Collection<ComplicationViewModel>> captureComplicationViewModelsObserver() { verify(mComplicationViewModelLiveData).observe(eq(mLifecycleOwner), mObserverCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java index 3a168d4e234b..d6dbd730368e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java @@ -450,6 +450,15 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { swipeToPosition(0f, Direction.DOWN, 0); } + @Test + public void testTouchSessionOnRemovedCalledTwice() { + mTouchHandler.onSessionStart(mTouchSession); + ArgumentCaptor<DreamTouchHandler.TouchSession.Callback> onRemovedCallbackCaptor = + ArgumentCaptor.forClass(DreamTouchHandler.TouchSession.Callback.class); + verify(mTouchSession).registerCallback(onRemovedCallbackCaptor.capture()); + onRemovedCallbackCaptor.getValue().onRemoved(); + onRemovedCallbackCaptor.getValue().onRemoved(); + } private void swipeToPosition(float percent, Direction direction, float velocityY) { Mockito.clearInvocations(mTouchSession); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt new file mode 100644 index 000000000000..ec94cdec78f0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt @@ -0,0 +1,73 @@ +/* + * 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.keyboard.backlight.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository +import com.android.systemui.keyboard.shared.model.BacklightModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class KeyboardBacklightInteractorTest : SysuiTestCase() { + + private val keyboardRepository = FakeKeyboardRepository() + private lateinit var underTest: KeyboardBacklightInteractor + + @Before + fun setUp() { + underTest = KeyboardBacklightInteractor(keyboardRepository) + } + + @Test + fun emitsNull_whenKeyboardJustConnected() = runTest { + val latest by collectLastValue(underTest.backlight) + keyboardRepository.setKeyboardConnected(true) + + assertThat(latest).isNull() + } + + @Test + fun emitsBacklight_whenKeyboardConnectedAndBacklightChanged() = runTest { + keyboardRepository.setKeyboardConnected(true) + keyboardRepository.setBacklight(BacklightModel(1, 5)) + + assertThat(underTest.backlight.first()).isEqualTo(BacklightModel(1, 5)) + } + + @Test + fun emitsNull_afterKeyboardDisconnecting() = runTest { + val latest by collectLastValue(underTest.backlight) + keyboardRepository.setKeyboardConnected(true) + keyboardRepository.setBacklight(BacklightModel(1, 5)) + + keyboardRepository.setKeyboardConnected(false) + + assertThat(latest).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt new file mode 100644 index 000000000000..ec05d10b793c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt @@ -0,0 +1,102 @@ +/* + * 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.keyboard.backlight.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyboard.backlight.domain.interactor.KeyboardBacklightInteractor +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository +import com.android.systemui.keyboard.shared.model.BacklightModel +import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class BacklightDialogViewModelTest : SysuiTestCase() { + + private val keyboardRepository = FakeKeyboardRepository() + private lateinit var underTest: BacklightDialogViewModel + @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper + private val timeoutMillis = 3000L + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(accessibilityManagerWrapper.getRecommendedTimeoutMillis(any(), any())) + .thenReturn(timeoutMillis.toInt()) + underTest = + BacklightDialogViewModel( + KeyboardBacklightInteractor(keyboardRepository), + accessibilityManagerWrapper + ) + keyboardRepository.setKeyboardConnected(true) + } + + @Test + fun emitsViewModel_whenBacklightChanged() = runTest { + keyboardRepository.setBacklight(BacklightModel(1, 5)) + + assertThat(underTest.dialogContent.first()).isEqualTo(BacklightDialogContentViewModel(1, 5)) + } + + @Test + fun emitsNull_afterTimeout() = runTest { + val latest by collectLastValue(underTest.dialogContent) + keyboardRepository.setBacklight(BacklightModel(1, 5)) + + assertThat(latest).isEqualTo(BacklightDialogContentViewModel(1, 5)) + advanceTimeBy(timeoutMillis + 1) + assertThat(latest).isNull() + } + + @Test + fun emitsNull_after5secDelay_fromLastBacklightChange() = runTest { + val latest by collectLastValue(underTest.dialogContent) + keyboardRepository.setKeyboardConnected(true) + + keyboardRepository.setBacklight(BacklightModel(1, 5)) + assertThat(latest).isEqualTo(BacklightDialogContentViewModel(1, 5)) + + advanceTimeBy(timeoutMillis * 2 / 3) + // timeout yet to pass, no new emission + keyboardRepository.setBacklight(BacklightModel(2, 5)) + assertThat(latest).isEqualTo(BacklightDialogContentViewModel(2, 5)) + + advanceTimeBy(timeoutMillis * 2 / 3) + // timeout refreshed because of last `setBacklight`, still content present + assertThat(latest).isEqualTo(BacklightDialogContentViewModel(2, 5)) + + advanceTimeBy(timeoutMillis * 2 / 3) + // finally timeout reached and null emitted + assertThat(latest).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt index a4e5bcaecde4..984f4be0ae97 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt @@ -45,6 +45,7 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRendererFactory import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager @@ -91,6 +92,7 @@ class CustomizationProviderTest : SysuiTestCase() { @Mock private lateinit var launchAnimator: DialogLaunchAnimator @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger private lateinit var underTest: CustomizationProvider private lateinit var testScope: TestScope @@ -184,6 +186,7 @@ class CustomizationProviderTest : SysuiTestCase() { featureFlags = featureFlags, repository = { quickAffordanceRepository }, launchAnimator = launchAnimator, + logger = logger, devicePolicyManager = devicePolicyManager, backgroundDispatcher = testDispatcher, ) 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 0469e77ca991..0e6f8d4e0720 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 @@ -219,6 +219,29 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun isKeyguardUnlocked() = + runTest(UnconfinedTestDispatcher()) { + whenever(keyguardStateController.isUnlocked).thenReturn(false) + var latest: Boolean? = null + val job = underTest.isKeyguardUnlocked.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + val captor = argumentCaptor<KeyguardStateController.Callback>() + verify(keyguardStateController).addCallback(captor.capture()) + + whenever(keyguardStateController.isUnlocked).thenReturn(true) + captor.value.onUnlockedChanged() + assertThat(latest).isTrue() + + whenever(keyguardStateController.isUnlocked).thenReturn(false) + captor.value.onUnlockedChanged() + assertThat(latest).isFalse() + + job.cancel() + } + + @Test fun isDozing() = runTest(UnconfinedTestDispatcher()) { var latest: Boolean? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 84ec125bfa55..46c623a7d3c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -42,6 +42,7 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserFileManager @@ -225,6 +226,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @Mock private lateinit var launchAnimator: DialogLaunchAnimator @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger private lateinit var underTest: KeyguardQuickAffordanceInteractor private lateinit var testScope: TestScope @@ -331,6 +333,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { featureFlags = featureFlags, repository = { quickAffordanceRepository }, launchAnimator = launchAnimator, + logger = logger, devicePolicyManager = devicePolicyManager, backgroundDispatcher = testDispatcher, ) @@ -360,10 +363,11 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled } - underTest.onQuickAffordanceTriggered( - configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS, - expandable = expandable, - ) + underTest.onQuickAffordanceTriggered( + configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS, + expandable = expandable, + slotId = "", + ) if (startActivity) { if (needsToUnlockFirst) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 62c9e5ffbb51..cd579dbc132e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -45,6 +45,7 @@ import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAff import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker @@ -80,6 +81,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @Mock private lateinit var launchAnimator: DialogLaunchAnimator @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger private lateinit var underTest: KeyguardQuickAffordanceInteractor @@ -186,6 +188,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { featureFlags = featureFlags, repository = { quickAffordanceRepository }, launchAnimator = launchAnimator, + logger = logger, devicePolicyManager = devicePolicyManager, backgroundDispatcher = testDispatcher, ) 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 ae7a928cdb2c..fe9098fa5c25 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 @@ -19,6 +19,8 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import androidx.test.filters.FlakyTest import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardSecurityModel +import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Interpolators import com.android.systemui.flags.FakeFeatureFlags @@ -40,6 +42,7 @@ import com.android.systemui.keyguard.util.KeyguardTransitionRunner import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.cancelChildren @@ -51,6 +54,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.reset import org.mockito.Mockito.verify @@ -77,6 +82,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { // Used to verify transition requests for test output @Mock private lateinit var mockTransitionRepository: KeyguardTransitionRepository @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor @@ -102,6 +108,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { transitionRepository = KeyguardTransitionRepositoryImpl() runner = KeyguardTransitionRunner(transitionRepository) + whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN) + val featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, true) } fromLockscreenTransitionInteractor = FromLockscreenTransitionInteractor( @@ -173,16 +181,17 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardInteractor = createKeyguardInteractor(featureFlags), keyguardTransitionRepository = mockTransitionRepository, keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + keyguardSecurityModel = keyguardSecurityModel, ) fromPrimaryBouncerTransitionInteractor.start() } @Test - fun `DREAMING to LOCKSCREEN - dreaming state changes first`() = + fun `DREAMING to LOCKSCREEN`() = testScope.runTest { - // GIVEN a device is dreaming and occluded + // GIVEN a device is dreaming keyguardRepository.setDreamingWithOverlay(true) - keyguardRepository.setKeyguardOccluded(true) + keyguardRepository.setWakefulnessModel(startingToWake()) runCurrent() // GIVEN a prior transition has run to DREAMING @@ -215,56 +224,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) - } - // THEN a transition to BOUNCER should occur - assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor") - assertThat(info.from).isEqualTo(KeyguardState.DREAMING) - assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN) - assertThat(info.animator).isNotNull() - - coroutineContext.cancelChildren() - } - - @Test - fun `DREAMING to LOCKSCREEN - occluded state changes first`() = - testScope.runTest { - // GIVEN a device is dreaming and occluded - keyguardRepository.setDreamingWithOverlay(true) - keyguardRepository.setKeyguardOccluded(true) - runCurrent() - - // GIVEN a prior transition has run to DREAMING - runner.startTransition( - testScope, - TransitionInfo( - ownerName = "", - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.DREAMING, - animator = - ValueAnimator().apply { - duration = 10 - interpolator = Interpolators.LINEAR - }, - ) - ) - runCurrent() - reset(mockTransitionRepository) - - // WHEN doze is complete - keyguardRepository.setDozeTransitionModel( - DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) - ) - // AND occluded has stopped - keyguardRepository.setKeyguardOccluded(false) - advanceUntilIdle() - // AND then dreaming has stopped - keyguardRepository.setDreamingWithOverlay(false) - advanceUntilIdle() - - val info = - withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to BOUNCER should occur assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor") @@ -304,7 +264,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to PRIMARY_BOUNCER should occur assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") @@ -345,7 +305,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to DOZING should occur assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") @@ -386,7 +346,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to DOZING should occur assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") @@ -427,7 +387,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to DOZING should occur assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") @@ -468,7 +428,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to DOZING should occur assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") @@ -505,7 +465,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to DOZING should occur assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor") @@ -542,7 +502,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to DOZING should occur assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor") @@ -583,7 +543,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to DOZING should occur assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") @@ -624,7 +584,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to AOD should occur assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") @@ -661,7 +621,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to AOD should occur assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") @@ -677,6 +637,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { testScope.runTest { // GIVEN a device that is not dreaming or dozing keyguardRepository.setDreamingWithOverlay(false) + keyguardRepository.setWakefulnessModel(startingToWake()) keyguardRepository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) @@ -704,7 +665,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to DREAMING should occur assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") @@ -741,7 +702,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to PRIMARY_BOUNCER should occur assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor") @@ -784,7 +745,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to AOD should occur assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor") @@ -828,7 +789,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to DOZING should occur assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor") @@ -870,7 +831,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to LOCKSCREEN should occur assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor") @@ -912,7 +873,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to AOD should occur assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor") @@ -954,7 +915,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to DOZING should occur assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor") @@ -995,7 +956,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val info = withArgCaptor<TransitionInfo> { - verify(mockTransitionRepository).startTransition(capture()) + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) } // THEN a transition to LOCKSCREEN should occur assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor") diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index c727b3a6cd10..bfc09d7c0379 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -45,6 +45,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceIn import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker @@ -89,6 +90,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { @Mock private lateinit var launchAnimator: DialogLaunchAnimator @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger private lateinit var underTest: KeyguardBottomAreaViewModel @@ -208,6 +210,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { featureFlags = featureFlags, repository = { quickAffordanceRepository }, launchAnimator = launchAnimator, + logger = logger, devicePolicyManager = devicePolicyManager, backgroundDispatcher = testDispatcher, ), @@ -230,6 +233,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { icon = mock(), canShowWhileLocked = false, intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ) val configKey = setUpQuickAffordanceModel( @@ -260,6 +264,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { icon = mock(), canShowWhileLocked = false, intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ) val configKey = setUpQuickAffordanceModel( @@ -272,6 +277,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { testConfig = TestConfig( isVisible = false, + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ), configKey = configKey, ) @@ -299,6 +305,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { icon = icon, canShowWhileLocked = false, intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ), ) @@ -313,6 +320,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { canShowWhileLocked = false, intent = Intent("action"), isSelected = true, + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ), configKey = configKey, ) @@ -341,6 +349,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { icon = icon, canShowWhileLocked = false, intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ), ) val configKey = @@ -354,6 +363,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { icon = icon, canShowWhileLocked = false, intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(), ), ) @@ -368,6 +378,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { canShowWhileLocked = false, intent = Intent("action"), isDimmed = true, + slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(), ), configKey = configKey, ) @@ -387,6 +398,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { canShowWhileLocked = false, intent = null, // This will cause it to tell the system that the click was handled. + slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(), ) val configKey = setUpQuickAffordanceModel( @@ -409,6 +421,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { val config = TestConfig( isVisible = false, + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ) val configKey = setUpQuickAffordanceModel( @@ -434,6 +447,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { icon = mock(), canShowWhileLocked = false, intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ) setUpQuickAffordanceModel( @@ -513,6 +527,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { isClickable = true, icon = mock(), canShowWhileLocked = true, + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ) ) assertThat(value()).isTrue() @@ -524,6 +539,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { isClickable = true, icon = mock(), canShowWhileLocked = false, + slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(), ) ) assertThat(value()).isTrue() @@ -532,6 +548,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { testConfig = TestConfig( isVisible = false, + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ) ) assertThat(value()).isTrue() @@ -540,6 +557,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { testConfig = TestConfig( isVisible = false, + slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(), ) ) assertThat(value()).isFalse() @@ -594,6 +612,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { icon = mock(), canShowWhileLocked = false, intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ) val configKey = setUpQuickAffordanceModel( @@ -626,6 +645,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { icon = mock(), canShowWhileLocked = false, intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ) val configKey = setUpQuickAffordanceModel( @@ -656,6 +676,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { icon = mock(), canShowWhileLocked = false, intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ) val configKey = setUpQuickAffordanceModel( @@ -684,6 +705,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { icon = mock(), canShowWhileLocked = false, intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), ) val configKey = setUpQuickAffordanceModel( @@ -748,12 +770,14 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { assertThat(viewModel.isActivated).isEqualTo(testConfig.isActivated) assertThat(viewModel.isSelected).isEqualTo(testConfig.isSelected) assertThat(viewModel.isDimmed).isEqualTo(testConfig.isDimmed) + assertThat(viewModel.slotId).isEqualTo(testConfig.slotId) if (testConfig.isVisible) { assertThat(viewModel.icon).isEqualTo(testConfig.icon) viewModel.onClicked.invoke( KeyguardQuickAffordanceViewModel.OnClickedParameters( configKey = configKey, expandable = expandable, + slotId = viewModel.slotId, ) ) if (testConfig.intent != null) { @@ -775,6 +799,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { val intent: Intent? = null, val isSelected: Boolean = false, val isDimmed: Boolean = false, + val slotId: String = "" ) { init { check(!isVisible || icon != null) { "Must supply non-null icon if visible!" } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt new file mode 100644 index 000000000000..2a91799741b7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -0,0 +1,109 @@ +/* + * 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.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.util.mockito.whenever +import com.google.common.collect.Range +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { + private lateinit var underTest: PrimaryBouncerToGoneTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + repository = FakeKeyguardTransitionRepository() + val interactor = KeyguardTransitionInteractor(repository) + underTest = PrimaryBouncerToGoneTransitionViewModel(interactor, statusBarStateController) + } + + @Test + fun scrimBehindAlpha_leaveShadeOpen() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val job = underTest.scrimBehindAlpha.onEach { values.add(it) }.launchIn(this) + + whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(true) + + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.6f)) + repository.sendTransitionStep(step(1f)) + + assertThat(values.size).isEqualTo(4) + values.forEach { assertThat(it).isEqualTo(1f) } + + job.cancel() + } + + @Test + fun scrimBehindAlpha_doNotLeaveShadeOpen() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val job = underTest.scrimBehindAlpha.onEach { values.add(it) }.launchIn(this) + + whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(false) + + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.6f)) + repository.sendTransitionStep(step(1f)) + + assertThat(values.size).isEqualTo(4) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } + assertThat(values[3]).isEqualTo(0f) + + job.cancel() + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return TransitionStep( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + value = value, + transitionState = state, + ownerName = "PrimaryBouncerToGoneTransitionViewModelTest" + ) + } +} 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 7f5707722b9c..e0ca90ec2c58 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 @@ -18,7 +18,6 @@ package com.android.systemui.media.controls.ui import android.app.PendingIntent import android.content.res.ColorStateList -import android.content.res.Configuration import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.util.MathUtils.abs @@ -685,46 +684,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { } @Test - fun testOnConfigChanged_playersAreAddedBack() { - mediaCarouselController.pageIndicator = pageIndicator - - listener.value.onMediaDataLoaded( - "playing local", - null, - DATA.copy( - active = true, - isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) - ) - listener.value.onMediaDataLoaded( - "paused local", - null, - DATA.copy( - active = true, - isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) - ) - runAllReady() - - val playersSize = MediaPlayerData.players().size - - configListener.value.onConfigChanged(Configuration()) - runAllReady() - - verify(pageIndicator).tintList = - ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) - assertEquals(playersSize, MediaPlayerData.players().size) - assertEquals( - MediaPlayerData.getMediaPlayerIndex("playing local"), - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex - ) - } - - @Test fun testOnUiModeChanged_playersAreAddedBack() { mediaCarouselController.pageIndicator = pageIndicator 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 55a33b6636e0..fd353afff7c0 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 @@ -27,6 +27,7 @@ import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color +import android.graphics.Matrix import android.graphics.drawable.Animatable2 import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.Drawable @@ -78,6 +79,8 @@ 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.util.MediaUiEventLogger import com.android.systemui.media.dialog.MediaOutputDialogFactory +import com.android.systemui.monet.ColorScheme +import com.android.systemui.monet.Style import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.NotificationLockscreenUserManager @@ -214,6 +217,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var recSubtitleMock2: TextView @Mock private lateinit var recSubtitleMock3: TextView @Mock private lateinit var coverItem: ImageView + @Mock private lateinit var matrix: Matrix private lateinit var coverItem1: ImageView private lateinit var coverItem2: ImageView private lateinit var coverItem3: ImageView @@ -700,6 +704,46 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + fun addTwoPlayerGradients_differentStates() { + // Setup redArtwork and its color scheme. + val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val redCanvas = Canvas(redBmp) + redCanvas.drawColor(Color.RED) + val redArt = Icon.createWithBitmap(redBmp) + val redWallpaperColor = player.getWallpaperColor(redArt) + val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT) + + // Setup greenArt and its color scheme. + val greenBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val greenCanvas = Canvas(greenBmp) + greenCanvas.drawColor(Color.GREEN) + val greenArt = Icon.createWithBitmap(greenBmp) + val greenWallpaperColor = player.getWallpaperColor(greenArt) + val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT) + + // Add gradient to both icons. + val redArtwork = player.addGradientToPlayerAlbum(redArt, redColorScheme, 10, 10) + val greenArtwork = player.addGradientToPlayerAlbum(greenArt, greenColorScheme, 10, 10) + + // They should have different constant states as they have different gradient color. + assertThat(redArtwork.getDrawable(1).constantState) + .isNotEqualTo(greenArtwork.getDrawable(1).constantState) + } + + @Test + fun getWallpaperColor_recycledBitmap_notCrashing() { + // Setup redArt icon. + val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val redArt = Icon.createWithBitmap(redBmp) + + // Recycle bitmap of redArt icon. + redArt.bitmap.recycle() + + // get wallpaperColor without illegal exception. + player.getWallpaperColor(redArt) + } + + @Test fun bind_seekBarDisabled_hasActions_seekBarVisibilityIsSetToInvisible() { useRealConstraintSets() @@ -2092,6 +2136,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3)) whenever(recommendationViewHolder.mediaSubtitles) .thenReturn(listOf(recSubtitleMock1, recSubtitleMock2, recSubtitleMock3)) + whenever(coverItem.imageMatrix).thenReturn(matrix) val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) val canvas = Canvas(bmp) @@ -2127,6 +2172,7 @@ public class MediaControlPanelTest : SysuiTestCase() { verify(recCardTitle).setTextColor(any<Int>()) verify(recAppIconItem, times(3)).setImageDrawable(any(Drawable::class.java)) verify(coverItem, times(3)).setImageDrawable(any(Drawable::class.java)) + verify(coverItem, times(3)).imageMatrix = any() } @Test @@ -2189,6 +2235,34 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + fun addTwoRecommendationGradients_differentStates() { + // Setup redArtwork and its color scheme. + val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val redCanvas = Canvas(redBmp) + redCanvas.drawColor(Color.RED) + val redArt = Icon.createWithBitmap(redBmp) + val redWallpaperColor = player.getWallpaperColor(redArt) + val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT) + + // Setup greenArt and its color scheme. + val greenBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val greenCanvas = Canvas(greenBmp) + greenCanvas.drawColor(Color.GREEN) + val greenArt = Icon.createWithBitmap(greenBmp) + val greenWallpaperColor = player.getWallpaperColor(greenArt) + val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT) + + // Add gradient to both icons. + val redArtwork = player.addGradientToRecommendationAlbum(redArt, redColorScheme, 10, 10) + val greenArtwork = + player.addGradientToRecommendationAlbum(greenArt, greenColorScheme, 10, 10) + + // They should have different constant states as they have different gradient color. + assertThat(redArtwork.getDrawable(1).constantState) + .isNotEqualTo(greenArtwork.getDrawable(1).constantState) + } + + @Test fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() { fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true) val semanticActions = 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 af91cdb1522c..0fac3db2dc1f 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 @@ -16,9 +16,12 @@ package com.android.systemui.media.controls.ui +import android.content.res.Configuration +import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View +import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase @@ -58,6 +61,8 @@ class MediaViewControllerTest : SysuiTestCase() { @Mock private lateinit var mediaSubTitleWidgetState: WidgetState @Mock private lateinit var mediaContainerWidgetState: WidgetState @Mock private lateinit var mediaFlags: MediaFlags + @Mock private lateinit var expandedLayout: ConstraintSet + @Mock private lateinit var collapsedLayout: ConstraintSet val delta = 0.1F @@ -77,6 +82,19 @@ class MediaViewControllerTest : SysuiTestCase() { } @Test + fun testOrientationChanged_layoutsAreLoaded() { + mediaViewController.expandedLayout = expandedLayout + mediaViewController.collapsedLayout = collapsedLayout + + val newConfig = Configuration() + newConfig.orientation = ORIENTATION_LANDSCAPE + configurationController.onConfigurationChanged(newConfig) + + verify(expandedLayout).load(context, R.xml.media_session_expanded) + verify(collapsedLayout).load(context, R.xml.media_session_collapsed) + } + + @Test fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() { mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) player.measureState = 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 376b7cc70150..4efc30f0aee2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -20,12 +20,17 @@ import android.app.admin.DevicePolicyManager import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.PackageManager +import android.os.UserHandle import android.os.UserManager import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity +import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserTracker import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -57,8 +62,8 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Mock lateinit var keyguardManager: KeyguardManager @Mock lateinit var userManager: UserManager @Mock lateinit var eventLogger: NoteTaskEventLogger - @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var devicePolicyManager: DevicePolicyManager + private val userTracker: UserTracker = FakeUserTracker() private val noteTaskInfo = NoteTaskInfo(packageName = NOTES_PACKAGE_NAME, uid = NOTES_UID) @@ -81,16 +86,14 @@ internal class NoteTaskControllerTest : SysuiTestCase() { private fun createNoteTaskController( isEnabled: Boolean = true, bubbles: Bubbles? = this.bubbles, - keyguardManager: KeyguardManager? = this.keyguardManager, - userManager: UserManager? = this.userManager, ): NoteTaskController = NoteTaskController( context = context, resolver = resolver, eventLogger = eventLogger, optionalBubbles = Optional.ofNullable(bubbles), - optionalUserManager = Optional.ofNullable(userManager), - optionalKeyguardManager = Optional.ofNullable(keyguardManager), + userManager = userManager, + keyguardManager = keyguardManager, isEnabled = isEnabled, devicePolicyManager = devicePolicyManager, userTracker = userTracker, @@ -225,13 +228,19 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) val intentCaptor = argumentCaptor<Intent>() - verify(context).startActivity(capture(intentCaptor)) + val userCaptor = argumentCaptor<UserHandle>() + verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) intentCaptor.value.let { intent -> assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME) - assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK) + assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) + assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK) + .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK) + assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT) + .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT) assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() } + assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) verify(eventLogger).logNoteTaskOpened(expectedInfo) verifyZeroInteractions(bubbles) } @@ -259,7 +268,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { intentCaptor.value.let { intent -> assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME) - assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK) + assertThat(intent.flags).isEqualTo(FLAG_ACTIVITY_NEW_TASK) assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() } verifyZeroInteractions(eventLogger) @@ -283,13 +292,22 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) val intentCaptor = argumentCaptor<Intent>() - verify(context).startActivity(capture(intentCaptor)) + val userCaptor = argumentCaptor<UserHandle>() + verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) + + (intentCaptor.value.flags and FLAG_ACTIVITY_NEW_TASK) == FLAG_ACTIVITY_NEW_TASK + intentCaptor.value.let { intent -> assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME) - assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK) + assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) + assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK) + .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK) + assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT) + .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT) assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() } + assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) verify(eventLogger).logNoteTaskOpened(expectedInfo) verifyZeroInteractions(bubbles) } @@ -306,28 +324,6 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() { - createNoteTaskController(keyguardManager = null) - .showNoteTask( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - isInMultiWindowMode = false, - ) - - verifyZeroInteractions(context, bubbles, eventLogger) - } - - @Test - fun showNoteTask_userManagerIsNull_shouldDoNothing() { - createNoteTaskController(userManager = null) - .showNoteTask( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - isInMultiWindowMode = false, - ) - - verifyZeroInteractions(context, bubbles, eventLogger) - } - - @Test fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() { whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(null) @@ -460,7 +456,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { intentCaptor.value.let { intent -> assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE) assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME) - assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK) + assertThat(intent.flags).isEqualTo(FLAG_ACTIVITY_NEW_TASK) assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() } } @@ -487,7 +483,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { intentCaptor.value.let { intent -> assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE) assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME) - assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK) + assertThat(intent.flags).isEqualTo(FLAG_ACTIVITY_NEW_TASK) assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt index 7f64f8a123e0..0c945dfa4b4c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt @@ -22,6 +22,8 @@ import android.content.pm.PackageManager import android.test.suitebuilder.annotation.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.UserTracker import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -44,13 +46,14 @@ internal class NoteTaskInfoResolverTest : SysuiTestCase() { @Mock lateinit var packageManager: PackageManager @Mock lateinit var roleManager: RoleManager + private val userTracker: UserTracker = FakeUserTracker() private lateinit var underTest: NoteTaskInfoResolver @Before fun setUp() { MockitoAnnotations.initMocks(this) - underTest = NoteTaskInfoResolver(context, roleManager, packageManager) + underTest = NoteTaskInfoResolver(roleManager, packageManager, userTracker) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt index d6dfc8509165..ac106ef9bf51 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt @@ -57,6 +57,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.`when` +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset @@ -351,4 +352,44 @@ class CustomTileTest : SysuiTestCase() { .startPendingIntentDismissingKeyguard( eq(pi), nullable(), nullable<ActivityLaunchAnimator.Controller>()) } + + @Test + fun testActiveTileListensOnceAfterCreated() { + `when`(tileServiceManager.isActiveTile).thenReturn(true) + + val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) + tile.initialize() + tile.postStale() + testableLooper.processAllMessages() + + verify(tileServiceManager).setBindRequested(true) + verify(tileService).onStartListening() + } + + @Test + fun testActiveTileDoesntListenAfterFirstTime() { + `when`(tileServiceManager.isActiveTile).thenReturn(true) + + val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) + tile.initialize() + // Make sure we have an icon in the tile because we don't have a default icon + // This should not be overridden by the retrieved tile that has null icon. + tile.qsTile.icon = mock(Icon::class.java) + `when`(tile.qsTile.icon.loadDrawable(any(Context::class.java))) + .thenReturn(mock(Drawable::class.java)) + + tile.postStale() + testableLooper.processAllMessages() + + // postStale will set it to not listening after it's done + verify(tileService).onStopListening() + + clearInvocations(tileServiceManager, tileService) + + tile.setListening(Any(), true) + testableLooper.processAllMessages() + + verify(tileServiceManager, never()).setBindRequested(true) + verify(tileService, never()).onStartListening() + } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java index 515e1ee172ed..3c08d58cbb67 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java @@ -18,14 +18,14 @@ package com.android.systemui.screenshot.appclips; import static android.app.Activity.RESULT_OK; -import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_FOR_NOTE_ACCEPTED; -import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_FOR_NOTE_CANCELLED; +import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_ACCEPTED; +import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_CANCELLED; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,36 +34,35 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.graphics.Bitmap; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.ResultReceiver; +import android.os.UserHandle; import android.testing.AndroidTestingRunner; import android.widget.ImageView; -import androidx.lifecycle.MutableLiveData; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.intercepting.SingleActivityFactory; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; -import com.android.systemui.screenshot.AppClipsActivity; -import com.android.systemui.screenshot.AppClipsTrampolineActivity; -import com.android.systemui.screenshot.AppClipsViewModel; +import com.android.systemui.screenshot.ImageExporter; import com.android.systemui.settings.UserTracker; +import com.google.common.util.concurrent.Futures; + import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.UUID; +import java.util.concurrent.Executor; import java.util.function.BiConsumer; @RunWith(AndroidTestingRunner.class) @@ -78,18 +77,16 @@ public final class AppClipsActivityTest extends SysuiTestCase { private static final String TEST_CALLING_PACKAGE = "test-calling-package"; @Mock - private AppClipsViewModel.Factory mViewModelFactory; + private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; + @Mock + private ImageExporter mImageExporter; @Mock private PackageManager mPackageManager; @Mock private UserTracker mUserTracker; @Mock private UiEventLogger mUiEventLogger; - @Mock - private AppClipsViewModel mViewModel; - private MutableLiveData<Bitmap> mScreenshotLiveData; - private MutableLiveData<Uri> mResultLiveData; private AppClipsActivity mActivity; // Using the deprecated ActivityTestRule and SingleActivityFactory to help with injecting mocks. @@ -97,8 +94,11 @@ public final class AppClipsActivityTest extends SysuiTestCase { new SingleActivityFactory<>(AppClipsActivityTestable.class) { @Override protected AppClipsActivityTestable create(Intent unUsed) { - return new AppClipsActivityTestable(mViewModelFactory, mPackageManager, - mUserTracker, mUiEventLogger); + return new AppClipsActivityTestable( + new AppClipsViewModel.Factory(mAppClipsCrossProcessHelper, + mImageExporter, getContext().getMainExecutor(), + directExecutor()), mPackageManager, mUserTracker, + mUiEventLogger); } }; @@ -110,29 +110,17 @@ public final class AppClipsActivityTest extends SysuiTestCase { public void setUp() throws PackageManager.NameNotFoundException { MockitoAnnotations.initMocks(this); - mScreenshotLiveData = new MutableLiveData<>(); - mResultLiveData = new MutableLiveData<>(); - MutableLiveData<Integer> errorLiveData = new MutableLiveData<>(); - - when(mViewModelFactory.create(any(Class.class))).thenReturn(mViewModel); - when(mViewModel.getScreenshot()).thenReturn(mScreenshotLiveData); - when(mViewModel.getResultLiveData()).thenReturn(mResultLiveData); - when(mViewModel.getErrorLiveData()).thenReturn(errorLiveData); when(mUserTracker.getUserId()).thenReturn(TEST_USER_ID); - ApplicationInfo applicationInfo = new ApplicationInfo(); applicationInfo.uid = TEST_UID; when(mPackageManager.getApplicationInfoAsUser(eq(TEST_CALLING_PACKAGE), any(ApplicationInfoFlags.class), eq(TEST_USER_ID))).thenReturn(applicationInfo); - doAnswer(invocation -> { - runOnMainThread(() -> mScreenshotLiveData.setValue(TEST_BITMAP)); - return null; - }).when(mViewModel).performScreenshot(); - doAnswer(invocation -> { - runOnMainThread(() -> mResultLiveData.setValue(TEST_URI)); - return null; - }).when(mViewModel).saveScreenshotThenFinish(any(Drawable.class), any(Rect.class)); + when(mAppClipsCrossProcessHelper.takeScreenshot()).thenReturn(TEST_BITMAP); + ImageExporter.Result result = new ImageExporter.Result(); + result.uri = TEST_URI; + when(mImageExporter.export(any(Executor.class), any(UUID.class), any(Bitmap.class), + any(UserHandle.class))).thenReturn(Futures.immediateFuture(result)); } @After @@ -140,7 +128,6 @@ public final class AppClipsActivityTest extends SysuiTestCase { mActivityRule.finishActivity(); } - @Ignore("b/269403503") @Test public void appClipsLaunched_screenshotDisplayed() { launchActivity(); @@ -148,7 +135,6 @@ public final class AppClipsActivityTest extends SysuiTestCase { assertThat(((ImageView) mActivity.findViewById(R.id.preview)).getDrawable()).isNotNull(); } - @Ignore("b/269403503") @Test public void screenshotDisplayed_userConsented_screenshotExportedSuccessfully() { ResultReceiver resultReceiver = createResultReceiver((resultCode, data) -> { @@ -168,7 +154,6 @@ public final class AppClipsActivityTest extends SysuiTestCase { verify(mUiEventLogger).log(SCREENSHOT_FOR_NOTE_ACCEPTED, TEST_UID, TEST_CALLING_PACKAGE); } - @Ignore("b/269403503") @Test public void screenshotDisplayed_userDeclined() { ResultReceiver resultReceiver = createResultReceiver((resultCode, data) -> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java index e40c49b56ac5..ad06dcc6f327 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java @@ -25,7 +25,8 @@ import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPP import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; -import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; +import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; +import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; import static com.google.common.truth.Truth.assertThat; @@ -59,8 +60,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.notetask.NoteTaskController; -import com.android.systemui.screenshot.AppClipsTrampolineActivity; -import com.android.systemui.screenshot.ScreenshotEvent; import com.android.systemui.settings.UserTracker; import com.android.wm.shell.bubbles.Bubbles; @@ -262,8 +261,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { mActivityRule.launchActivity(mActivityIntent); waitForIdleSync(); - verify(mUiEventLogger).log(ScreenshotEvent.SCREENSHOT_FOR_NOTE_TRIGGERED, TEST_UID, - TEST_CALLING_PACKAGE); + verify(mUiEventLogger).log(SCREENSHOT_FOR_NOTE_TRIGGERED, TEST_UID, TEST_CALLING_PACKAGE); } private void mockToSatisfyAllPrerequisites() throws NameNotFoundException { diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java index d5af7ce1d346..e7c3c0578627 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.appclips; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; @@ -36,7 +36,7 @@ import android.os.UserHandle; import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; -import com.android.systemui.screenshot.appclips.AppClipsCrossProcessHelper; +import com.android.systemui.screenshot.ImageExporter; import com.google.common.util.concurrent.Futures; @@ -46,7 +46,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.time.ZonedDateTime; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; @@ -62,7 +61,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Mock private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; @Mock private ImageExporter mImageExporter; - private com.android.systemui.screenshot.AppClipsViewModel mViewModel; + private AppClipsViewModel mViewModel; @Before public void setUp() { @@ -99,8 +98,8 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void saveScreenshot_throwsError_shouldUpdateErrorWithFailed() { - when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), any( - ZonedDateTime.class), any(UserHandle.class))).thenReturn( + when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), + any(UserHandle.class))).thenReturn( Futures.immediateFailedFuture(new ExecutionException(new Throwable()))); mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); @@ -113,9 +112,9 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void saveScreenshot_failsSilently_shouldUpdateErrorWithFailed() { - when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), any( - ZonedDateTime.class), any(UserHandle.class))).thenReturn( - Futures.immediateFuture(new ImageExporter.Result())); + when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), + any(UserHandle.class))).thenReturn( + Futures.immediateFuture(new ImageExporter.Result())); mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); waitForIdleSync(); @@ -129,9 +128,8 @@ public final class AppClipsViewModelTest extends SysuiTestCase { public void saveScreenshot_succeeds_shouldUpdateResultWithUri() { ImageExporter.Result result = new ImageExporter.Result(); result.uri = FAKE_URI; - when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), any( - ZonedDateTime.class), any(UserHandle.class))).thenReturn( - Futures.immediateFuture(result)); + when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), + any(UserHandle.class))).thenReturn(Futures.immediateFuture(result)); mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); waitForIdleSync(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 0a401b09b6cf..82a57438052f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteracto import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.NotificationInsetsController @@ -65,48 +66,32 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @RunWithLooper(setAsMainLooper = true) class NotificationShadeWindowViewControllerTest : SysuiTestCase() { - @Mock - private lateinit var view: NotificationShadeWindowView - @Mock - private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController - @Mock - private lateinit var centralSurfaces: CentralSurfaces - @Mock - private lateinit var dockManager: DockManager - @Mock - private lateinit var notificationPanelViewController: NotificationPanelViewController - @Mock - private lateinit var notificationShadeDepthController: NotificationShadeDepthController - @Mock - private lateinit var notificationShadeWindowController: NotificationShadeWindowController - @Mock - private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController - @Mock - private lateinit var ambientState: AmbientState - @Mock - private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel - @Mock - private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController - @Mock - private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager - @Mock - private lateinit var statusBarWindowStateController: StatusBarWindowStateController + @Mock private lateinit var view: NotificationShadeWindowView + @Mock private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController + @Mock private lateinit var centralSurfaces: CentralSurfaces + @Mock private lateinit var dockManager: DockManager + @Mock private lateinit var notificationPanelViewController: NotificationPanelViewController + @Mock private lateinit var notificationShadeDepthController: NotificationShadeDepthController + @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController + @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController + @Mock private lateinit var ambientState: AmbientState + @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel + @Mock private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController + @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager + @Mock private lateinit var statusBarWindowStateController: StatusBarWindowStateController @Mock private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController - @Mock - private lateinit var lockIconViewController: LockIconViewController - @Mock - private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController - @Mock - private lateinit var pulsingGestureListener: PulsingGestureListener - @Mock - private lateinit var notificationInsetsController: NotificationInsetsController - @Mock - private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor + @Mock private lateinit var lockIconViewController: LockIconViewController + @Mock private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController + @Mock private lateinit var pulsingGestureListener: PulsingGestureListener + @Mock private lateinit var notificationInsetsController: NotificationInsetsController + @Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent @Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor + @Mock + lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler> private lateinit var interactionEventHandler: InteractionEventHandler @@ -118,43 +103,44 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(view.bottom).thenReturn(VIEW_BOTTOM) whenever(view.findViewById<ViewGroup>(R.id.keyguard_bouncer_container)) - .thenReturn(mock(ViewGroup::class.java)) + .thenReturn(mock(ViewGroup::class.java)) whenever(keyguardBouncerComponentFactory.create(any(ViewGroup::class.java))) - .thenReturn(keyguardBouncerComponent) + .thenReturn(keyguardBouncerComponent) whenever(keyguardBouncerComponent.securityContainerController) - .thenReturn(keyguardSecurityContainerController) + .thenReturn(keyguardSecurityContainerController) whenever(keyguardTransitionInteractor.lockscreenToDreamingTransition) - .thenReturn(emptyFlow<TransitionStep>()) - underTest = NotificationShadeWindowViewController( - lockscreenShadeTransitionController, - FalsingCollectorFake(), - sysuiStatusBarStateController, - dockManager, - notificationShadeDepthController, - view, - notificationPanelViewController, - ShadeExpansionStateManager(), - stackScrollLayoutController, - statusBarKeyguardViewManager, - statusBarWindowStateController, - lockIconViewController, - centralSurfaces, - notificationShadeWindowController, - keyguardUnlockAnimationController, - notificationInsetsController, - ambientState, - pulsingGestureListener, - keyguardBouncerViewModel, - keyguardBouncerComponentFactory, - alternateBouncerInteractor, - keyguardTransitionInteractor, - ) + .thenReturn(emptyFlow<TransitionStep>()) + underTest = + NotificationShadeWindowViewController( + lockscreenShadeTransitionController, + FalsingCollectorFake(), + sysuiStatusBarStateController, + dockManager, + notificationShadeDepthController, + view, + notificationPanelViewController, + ShadeExpansionStateManager(), + stackScrollLayoutController, + statusBarKeyguardViewManager, + statusBarWindowStateController, + lockIconViewController, + centralSurfaces, + notificationShadeWindowController, + keyguardUnlockAnimationController, + notificationInsetsController, + ambientState, + pulsingGestureListener, + keyguardBouncerViewModel, + keyguardBouncerComponentFactory, + alternateBouncerInteractor, + keyguardTransitionInteractor, + primaryBouncerToGoneTransitionViewModel, + ) underTest.setupExpandedStatusBar() - interactionEventHandlerCaptor = - ArgumentCaptor.forClass(InteractionEventHandler::class.java) + interactionEventHandlerCaptor = ArgumentCaptor.forClass(InteractionEventHandler::class.java) verify(view).setInteractionEventHandler(interactionEventHandlerCaptor.capture()) - interactionEventHandler = interactionEventHandlerCaptor.value + interactionEventHandler = interactionEventHandlerCaptor.value } // Note: So far, these tests only cover interactions with the status bar view controller. More @@ -184,14 +170,11 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Test fun handleDispatchTouchEvent_downTouchBelowViewThenAnotherTouch_sendsTouchToSb() { underTest.setStatusBarViewController(phoneStatusBarViewController) - val downEvBelow = MotionEvent.obtain( - 0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0 - ) + val downEvBelow = + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0) interactionEventHandler.handleDispatchTouchEvent(downEvBelow) - val nextEvent = MotionEvent.obtain( - 0L, 0L, MotionEvent.ACTION_MOVE, 0f, VIEW_BOTTOM + 5f, 0 - ) + val nextEvent = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, VIEW_BOTTOM + 5f, 0) whenever(phoneStatusBarViewController.sendTouchToView(nextEvent)).thenReturn(true) val returnVal = interactionEventHandler.handleDispatchTouchEvent(nextEvent) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java index 5d719790386a..faa6221b675c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java @@ -46,6 +46,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel; +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; import com.android.systemui.statusbar.DragDownHelper; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationInsetsController; @@ -101,6 +102,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { @Mock private NotificationInsetsController mNotificationInsetsController; @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel; @Captor private ArgumentCaptor<NotificationShadeWindowView.InteractionEventHandler> mInteractionEventHandlerCaptor; @@ -150,7 +152,8 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { mKeyguardBouncerViewModel, mKeyguardBouncerComponentFactory, mAlternateBouncerInteractor, - mKeyguardTransitionInteractor + mKeyguardTransitionInteractor, + mPrimaryBouncerToGoneTransitionViewModel ); mController.setupExpandedStatusBar(); mController.setDragDownHelper(mDragDownHelper); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupWhenCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupWhenCoordinatorTest.kt new file mode 100644 index 000000000000..eac0e296c51f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupWhenCoordinatorTest.kt @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.coordinator + +import android.app.Notification +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.SbnBuilder +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderGroupListener +import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener +import com.android.systemui.statusbar.notification.collection.render.NotifGroupController +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.mockito.any +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.mockito.withArgCaptor +import com.android.systemui.util.time.SystemClock +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations.initMocks + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class GroupWhenCoordinatorTest : SysuiTestCase() { + + private lateinit var beforeFinalizeFilterListener: OnBeforeFinalizeFilterListener + private lateinit var afterRenderGroupListener: OnAfterRenderGroupListener + + @Mock private lateinit var pipeline: NotifPipeline + + @Mock private lateinit var delayableExecutor: DelayableExecutor + + @Mock private lateinit var groupController: NotifGroupController + + @Mock private lateinit var systemClock: SystemClock + + @InjectMocks private lateinit var coordinator: GroupWhenCoordinator + + @Before + fun setUp() { + initMocks(this) + whenever(systemClock.currentTimeMillis()).thenReturn(NOW) + coordinator.attach(pipeline) + + beforeFinalizeFilterListener = withArgCaptor { + verify(pipeline).addOnBeforeFinalizeFilterListener(capture()) + } + afterRenderGroupListener = withArgCaptor { + verify(pipeline).addOnAfterRenderGroupListener(capture()) + } + } + + @Test + fun setNotificationGroupWhen_setClosestTimeByNow_whenAllNotificationsAreBeforeNow() { + // GIVEN + val summaryEntry = buildNotificationEntry(0, NOW) + val childEntry1 = buildNotificationEntry(1, NOW - 10L) + val childEntry2 = buildNotificationEntry(2, NOW - 100L) + val groupEntry = + GroupEntryBuilder() + .setSummary(summaryEntry) + .setChildren(listOf(childEntry1, childEntry2)) + .build() + // WHEN + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController) + + // THEN + verify(groupController).setNotificationGroupWhen(eq(NOW - 10L)) + } + + @Test + fun setNotificationGroupWhen_setClosestTimeByNow_whenAllNotificationsAreAfterNow() { + // GIVEN + val summaryEntry = buildNotificationEntry(0, NOW) + val childEntry1 = buildNotificationEntry(1, NOW + 10L) + val childEntry2 = buildNotificationEntry(2, NOW + 100L) + + val groupEntry = + GroupEntryBuilder() + .setSummary(summaryEntry) + .setChildren(listOf(childEntry1, childEntry2)) + .build() + + // WHEN + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController) + + // THEN + verify(groupController).setNotificationGroupWhen(eq(NOW + 10L)) + } + + @Test + fun setNotificationGroupWhen_setClosestFutureTimeByNow_whenThereAreBothBeforeAndAfterNow() { + // GIVEN + val summaryEntry = buildNotificationEntry(0, NOW) + val childEntry1 = buildNotificationEntry(1, NOW + 100L) + val childEntry2 = buildNotificationEntry(2, NOW + 10L) + val childEntry3 = buildNotificationEntry(3, NOW - 100L) + val childEntry4 = buildNotificationEntry(4, NOW - 9L) + + val groupEntry = + GroupEntryBuilder() + .setSummary(summaryEntry) + .setChildren(listOf(childEntry1, childEntry2, childEntry3, childEntry4)) + .build() + + // WHEN + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController) + + // THEN + verify(groupController).setNotificationGroupWhen(eq(NOW + 10L)) + } + + @Test + fun setNotificationGroupWhen_filterInvalidNotificationTimes() { + // GIVEN + val summaryEntry = buildNotificationEntry(0, NOW) + val childEntry1 = buildNotificationEntry(1, NOW + 100L) + val childEntry2 = buildNotificationEntry(2, -20000L) + val childEntry3 = buildNotificationEntry(4, 0) + + val groupEntry = + GroupEntryBuilder() + .setSummary(summaryEntry) + .setChildren(listOf(childEntry1, childEntry2, childEntry3)) + .build() + + // WHEN + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController) + + // THEN + verify(groupController).setNotificationGroupWhen(eq(NOW + 100)) + } + + @Test + fun setNotificationGroupWhen_setSummaryTimeWhenAllNotificationTimesAreInvalid() { + // GIVEN + val summaryEntry = buildNotificationEntry(0, NOW) + val childEntry1 = buildNotificationEntry(1, 0) + val childEntry2 = buildNotificationEntry(2, -1) + + val groupEntry = + GroupEntryBuilder() + .setSummary(summaryEntry) + .setChildren(listOf(childEntry1, childEntry2)) + .build() + + // WHEN + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController) + + // THEN + verify(groupController, never()).setNotificationGroupWhen(NOW) + } + + @Test + fun setNotificationGroupWhen_schedulePipelineInvalidationWhenAnyNotificationIsInTheFuture() { + // GIVEN + val summaryEntry = buildNotificationEntry(0, NOW) + val childEntry1 = buildNotificationEntry(1, NOW + 1000L) + val childEntry2 = buildNotificationEntry(2, NOW + 2000L) + val childEntry3 = buildNotificationEntry(3, NOW - 100L) + + val groupEntry = + GroupEntryBuilder() + .setSummary(summaryEntry) + .setChildren(listOf(childEntry1, childEntry2, childEntry3)) + .build() + + // WHEN + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController) + + // THEN + verify(delayableExecutor).executeDelayed(any(), eq(1000)) + } + + @Test + fun setNotificationGroupWhen_cancelPrevPipelineInvalidation() { + // GIVEN + val summaryEntry = buildNotificationEntry(0, NOW) + val childEntry1 = buildNotificationEntry(1, NOW + 1L) + val prevInvalidation = mock<Runnable>() + whenever(delayableExecutor.executeDelayed(any(), any())).thenReturn(prevInvalidation) + + val groupEntry = + GroupEntryBuilder().setSummary(summaryEntry).setChildren(listOf(childEntry1)).build() + + // WHEN + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + afterRenderGroupListener.onAfterRenderGroup(groupEntry, groupController) + + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry)) + + // THEN + verify(prevInvalidation).run() + } + + private fun buildNotificationEntry(id: Int, timeMillis: Long): NotificationEntry { + val notification = Notification.Builder(mContext).setWhen(timeMillis).build() + val sbn = SbnBuilder().setNotification(notification).build() + return NotificationEntryBuilder().setId(id).setSbn(sbn).build() + } + + private companion object { + private const val NOW = 1000L + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index dd7143ae7e16..cbf841b5a1f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.stack; import static android.view.View.GONE; +import static android.view.WindowInsets.Type.ime; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE; @@ -46,6 +47,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.graphics.Insets; import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -54,6 +56,8 @@ import android.util.MathUtils; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.widget.TextView; import androidx.test.annotation.UiThreadTest; @@ -91,6 +95,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.ArrayList; + /** * Tests for {@link NotificationStackScrollLayout}. */ @@ -843,6 +849,19 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { verify(mEmptyShadeView).setFooterText(not(0)); } + @Test + public void testWindowInsetAnimationProgress_updatesBottomInset() { + int bottomImeInset = 100; + mStackScrollerInternal.setAnimatedInsetsEnabled(true); + WindowInsets windowInsets = new WindowInsets.Builder() + .setInsets(ime(), Insets.of(0, 0, 0, bottomImeInset)).build(); + ArrayList<WindowInsetsAnimation> windowInsetsAnimations = new ArrayList<>(); + mStackScrollerInternal + .dispatchWindowInsetsAnimationProgress(windowInsets, windowInsetsAnimations); + + assertEquals(bottomImeInset, mStackScrollerInternal.mBottomInset); + } + private void setBarStateForTest(int state) { // Can't inject this through the listener or we end up on the actual implementation // rather than the mock because the spy just coppied the anonymous inner /shruggie. 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 eb5edbc21d89..f5b7ca804fbc 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 @@ -180,6 +180,7 @@ public class DozeParametersTest extends SysuiTestCase { when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true); mDozeParameters.onTuningChanged(Settings.Secure.DOZE_ALWAYS_ON, "1"); + verify(mScreenOffAnimationController).onAlwaysOnChanged(false); assertThat(mDozeParameters.getAlwaysOn()).isFalse(); } @@ -196,13 +197,16 @@ public class DozeParametersTest extends SysuiTestCase { mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); verify(callback, times(2)).onAlwaysOnChange(); + verify(mScreenOffAnimationController, times(2)).onAlwaysOnChanged(false); assertThat(mDozeParameters.getAlwaysOn()).isFalse(); + reset(mScreenOffAnimationController); reset(callback); when(mBatteryController.isAodPowerSave()).thenReturn(false); mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); verify(callback).onAlwaysOnChange(); + verify(mScreenOffAnimationController).onAlwaysOnChanged(true); assertThat(mDozeParameters.getAlwaysOn()).isTrue(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index dc5a0472f49e..e1fba816382c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -24,6 +24,8 @@ import static com.android.systemui.statusbar.phone.ScrimState.SHADE_LOCKED; import static com.google.common.truth.Truth.assertThat; +import static kotlinx.coroutines.flow.FlowKt.emptyFlow; + import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; @@ -58,8 +60,14 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants; +import com.android.systemui.keyguard.shared.model.KeyguardState; +import com.android.systemui.keyguard.shared.model.TransitionState; +import com.android.systemui.keyguard.shared.model.TransitionStep; +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; import com.android.systemui.scrim.ScrimView; +import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.policy.FakeConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; @@ -85,8 +93,10 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import kotlinx.coroutines.CoroutineDispatcher; + @RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper +@TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest public class ScrimControllerTest extends SysuiTestCase { @@ -115,6 +125,11 @@ public class ScrimControllerTest extends SysuiTestCase { @Mock private DockManager mDockManager; @Mock private ScreenOffAnimationController mScreenOffAnimationController; @Mock private KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; + @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel; + @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + @Mock private CoroutineDispatcher mMainDispatcher; + @Mock private SysuiStatusBarStateController mSysuiStatusBarStateController; + // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The // event-dispatch-on-registration pattern caused some of these unit tests to fail.) @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @@ -225,13 +240,22 @@ public class ScrimControllerTest extends SysuiTestCase { when(mDelayedWakeLockBuilder.build()).thenReturn(mWakeLock); when(mDockManager.isDocked()).thenReturn(false); + when(mKeyguardTransitionInteractor.getPrimaryBouncerToGoneTransition()) + .thenReturn(emptyFlow()); + when(mPrimaryBouncerToGoneTransitionViewModel.getScrimBehindAlpha()) + .thenReturn(emptyFlow()); + mScrimController = new ScrimController(mLightBarController, mDozeParameters, mAlarmManager, mKeyguardStateController, mDelayedWakeLockBuilder, new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor, mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, - mStatusBarKeyguardViewManager); + mStatusBarKeyguardViewManager, + mPrimaryBouncerToGoneTransitionViewModel, + mKeyguardTransitionInteractor, + mSysuiStatusBarStateController, + mMainDispatcher); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -861,7 +885,11 @@ public class ScrimControllerTest extends SysuiTestCase { mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, - mStatusBarKeyguardViewManager); + mStatusBarKeyguardViewManager, + mPrimaryBouncerToGoneTransitionViewModel, + mKeyguardTransitionInteractor, + mSysuiStatusBarStateController, + mMainDispatcher); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -1629,6 +1657,28 @@ public class ScrimControllerTest extends SysuiTestCase { assertScrimAlpha(mScrimBehind, 0); } + @Test + public void ignoreTransitionRequestWhileKeyguardTransitionRunning() { + mScrimController.transitionTo(ScrimState.UNLOCKED); + mScrimController.mPrimaryBouncerToGoneTransition.accept( + new TransitionStep(KeyguardState.PRIMARY_BOUNCER, KeyguardState.GONE, 0f, + TransitionState.RUNNING, "ScrimControllerTest")); + + // This request should not happen + mScrimController.transitionTo(ScrimState.BOUNCER); + assertThat(mScrimController.getState()).isEqualTo(ScrimState.UNLOCKED); + } + + @Test + public void primaryBouncerToGoneOnFinishCallsKeyguardFadedAway() { + when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true); + mScrimController.mPrimaryBouncerToGoneTransition.accept( + new TransitionStep(KeyguardState.PRIMARY_BOUNCER, KeyguardState.GONE, 0f, + TransitionState.FINISHED, "ScrimControllerTest")); + + verify(mStatusBarKeyguardViewManager).onKeyguardFadedAway(); + } + private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) { mScrimController.setRawPanelExpansionFraction(expansion); finishAnimationsImmediately(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt index 1779de729e5b..7594c90daa8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt @@ -8,13 +8,14 @@ import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.TestUnfoldTransitionProvider +import com.android.systemui.unfold.util.CurrentActivityTypeProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.any import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -26,6 +27,9 @@ class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() { @Mock private lateinit var display: Display + @Mock + private lateinit var currentActivityTypeProvider: CurrentActivityTypeProvider + private val view: View = View(context) private val progressProvider = TestUnfoldTransitionProvider() private val scopedProvider = ScopedUnfoldTransitionProgressProvider(progressProvider) @@ -36,9 +40,9 @@ class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - `when`(windowManager.defaultDisplay).thenReturn(display) - `when`(display.rotation).thenReturn(Surface.ROTATION_0) - `when`(display.getSize(any())).thenAnswer { + whenever(windowManager.defaultDisplay).thenReturn(display) + whenever(display.rotation).thenReturn(Surface.ROTATION_0) + whenever(display.getSize(any())).thenAnswer { val point = it.arguments[0] as Point point.x = 100 point.y = 100 @@ -47,7 +51,12 @@ class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() { scopedProvider.setReadyToHandleTransition(true) - controller = StatusBarMoveFromCenterAnimationController(scopedProvider, windowManager) + controller = + StatusBarMoveFromCenterAnimationController( + scopedProvider, + currentActivityTypeProvider, + windowManager + ) } @Test @@ -99,6 +108,31 @@ class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() { } @Test + fun alpha_onLauncher_alphaDoesNotChange() { + whenever(currentActivityTypeProvider.isHomeActivity).thenReturn(true) + controller.onViewsReady(arrayOf(view)) + progressProvider.onTransitionStarted() + progressProvider.onTransitionProgress(0.0f) + assertThat(view.alpha).isEqualTo(1.0f) + + progressProvider.onTransitionProgress(1.0f) + + assertThat(view.alpha).isEqualTo(1.0f) + } + + @Test + fun alpha_NotOnLauncher_alphaChanges() { + whenever(currentActivityTypeProvider.isHomeActivity).thenReturn(false) + controller.onViewsReady(arrayOf(view)) + progressProvider.onTransitionStarted() + assertThat(view.alpha).isEqualTo(1.0f) + + progressProvider.onTransitionProgress(0.5f) + + assertThat(view.alpha).isNotEqualTo(1.0f) + } + + @Test fun transitionFinished_viewReAttached_noChangesToTranslation() { controller.onViewsReady(arrayOf(view)) progressProvider.onTransitionProgress(0.5f) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt index 86529dce948a..35dea60b1a1f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/shared/MobileInputLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.pipeline.mobile.shared +package com.android.systemui.statusbar.pipeline.mobile.data import android.net.Network import android.net.NetworkCapabilities diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryTest.kt index 0145103d55e1..dfef62e95eda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryTest.kt @@ -24,8 +24,8 @@ import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.createTestConfig -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt index 17502f28a479..07c8cee9a3d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -27,13 +27,13 @@ import com.android.systemui.demomode.DemoModeController import com.android.systemui.dump.DumpManager import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.validMobileEvent import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt index b2577e349da7..bd5a4d7f7385 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt @@ -53,6 +53,7 @@ import android.telephony.TelephonyManager.NETWORK_TYPE_LTE import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -65,7 +66,6 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrier import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel 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 09b7a66c925d..68b1cda62f4c 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,12 +37,12 @@ import com.android.settingslib.mobile.MobileMappings 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.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName -import com.android.systemui.statusbar.pipeline.mobile.shared.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt new file mode 100644 index 000000000000..4aa48d6f25f1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui + +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.log.LogBufferFactory +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger.Companion.getIdForLogging +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.KeyguardMobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.QsMobileIconViewModel +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import java.io.PrintWriter +import java.io.StringWriter +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +class MobileViewLoggerTest : SysuiTestCase() { + private val buffer = LogBufferFactory(DumpManager(), mock()).create("buffer", 10) + private val stringWriter = StringWriter() + private val printWriter = PrintWriter(stringWriter) + + private val underTest = MobileViewLogger(buffer, mock()) + + @Mock private lateinit var flags: StatusBarPipelineFlags + @Mock private lateinit var commonViewModel: MobileIconViewModel + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun collectionStarted_dumpHasInfo() { + val view = TextView(context) + val viewModel = QsMobileIconViewModel(commonViewModel, flags) + + underTest.logCollectionStarted(view, viewModel) + + val dumpString = getDumpString() + assertThat(dumpString).contains("${view.getIdForLogging()}, isCollecting=true") + } + + @Test + fun collectionStarted_multipleViews_dumpHasInfo() { + val view = TextView(context) + val view2 = TextView(context) + val viewModel = QsMobileIconViewModel(commonViewModel, flags) + val viewModel2 = KeyguardMobileIconViewModel(commonViewModel, flags) + + underTest.logCollectionStarted(view, viewModel) + underTest.logCollectionStarted(view2, viewModel2) + + val dumpString = getDumpString() + assertThat(dumpString).contains("${view.getIdForLogging()}, isCollecting=true") + assertThat(dumpString).contains("${view2.getIdForLogging()}, isCollecting=true") + } + + @Test + fun collectionStopped_dumpHasInfo() { + val view = TextView(context) + val view2 = TextView(context) + val viewModel = QsMobileIconViewModel(commonViewModel, flags) + val viewModel2 = KeyguardMobileIconViewModel(commonViewModel, flags) + + underTest.logCollectionStarted(view, viewModel) + underTest.logCollectionStarted(view2, viewModel2) + underTest.logCollectionStopped(view, viewModel) + + val dumpString = getDumpString() + assertThat(dumpString).contains("${view.getIdForLogging()}, isCollecting=false") + assertThat(dumpString).contains("${view2.getIdForLogging()}, isCollecting=true") + } + + private fun getDumpString(): String { + underTest.dump(printWriter, args = arrayOf()) + return stringWriter.toString() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt index e68a3970ae93..7420db2e895e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.QsMobileIconViewModel @@ -60,6 +61,7 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var tableLogBuffer: TableLogBuffer + @Mock private lateinit var viewLogger: MobileViewLogger @Mock private lateinit var constants: ConnectivityConstants private lateinit var interactor: FakeMobileIconInteractor private lateinit var airplaneModeRepository: FakeAirplaneModeRepository @@ -94,7 +96,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Test fun setVisibleState_icon_iconShownDotHidden() { - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false) @@ -109,8 +117,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Test fun setVisibleState_dot_iconHiddenDotShown() { - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false) ViewUtils.attachView(view) @@ -124,8 +137,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Test fun setVisibleState_hidden_iconAndDotHidden() { - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false) ViewUtils.attachView(view) @@ -142,8 +160,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { whenever(constants.hasDataCapabilities).thenReturn(false) createViewModel() - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -157,8 +180,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { whenever(constants.hasDataCapabilities).thenReturn(true) createViewModel() - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -171,8 +199,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { fun isIconVisible_notAirplaneMode_outputsTrue() { airplaneModeRepository.setIsAirplaneMode(false) - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -185,8 +218,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { fun isIconVisible_airplaneMode_outputsTrue() { airplaneModeRepository.setIsAirplaneMode(true) - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) - + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -198,7 +236,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Test fun onDarkChanged_iconHasNewColor() { whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false) - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -214,7 +258,13 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() { @Test fun setStaticDrawableColor_iconHasNewColor() { whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false) - val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + val view = + ModernStatusBarMobileView.constructAndBind( + context, + viewLogger, + SLOT_NAME, + viewModel, + ) ViewUtils.attachView(view) testableLooper.processAllMessages() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt index f9830309252d..a6d915243f60 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn @@ -84,7 +85,7 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() { testScope.backgroundScope, ) - homeIcon = HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags) + homeIcon = HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags, mock()) qsIcon = QsMobileIconViewModel(commonImpl, statusBarPipelineFlags) keyguardIcon = KeyguardMobileIconViewModel(commonImpl, statusBarPipelineFlags) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt index 4628f8410245..ddb7f4d88d30 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt @@ -24,6 +24,8 @@ import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirp import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository @@ -51,6 +53,8 @@ class MobileIconsViewModelTest : SysuiTestCase() { private lateinit var airplaneModeInteractor: AirplaneModeInteractor @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var constants: ConnectivityConstants + @Mock private lateinit var logger: MobileViewLogger + @Mock private lateinit var verboseLogger: VerboseMobileViewLogger private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -73,6 +77,8 @@ class MobileIconsViewModelTest : SysuiTestCase() { underTest = MobileIconsViewModel( subscriptionIdsFlow, + logger, + verboseLogger, interactor, airplaneModeInteractor, constants, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt index e4c8fd0cd8a1..b4039d906810 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt @@ -164,6 +164,10 @@ class ModernStatusBarViewTest : SysuiTestCase() { override fun getShouldIconBeVisible(): Boolean { return shouldIconBeVisibleInternal } + + override fun isCollecting(): Boolean { + return true + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java index 48b17322da4d..481d453fa0b1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java @@ -38,6 +38,7 @@ import com.android.internal.R; import com.android.internal.view.RotationPolicy; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; import com.android.systemui.SysuiTestCase; +import com.android.systemui.dump.DumpManager; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.wrapper.RotationPolicyWrapper; @@ -55,10 +56,12 @@ public class DeviceStateRotationLockSettingControllerTest extends SysuiTestCase private static final String[] DEFAULT_SETTINGS = new String[]{"0:1", "2:0:1", "1:2"}; + @Mock private DeviceStateManager mDeviceStateManager; + @Mock private DeviceStateRotationLockSettingControllerLogger mLogger; + @Mock private DumpManager mDumpManager; + private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private final FakeExecutor mFakeExecutor = new FakeExecutor(mFakeSystemClock); - @Mock - private DeviceStateManager mDeviceStateManager; private final RotationPolicyWrapper mFakeRotationPolicy = new FakeRotationPolicy(); private DeviceStateRotationLockSettingController mDeviceStateRotationLockSettingController; private DeviceStateManager.DeviceStateCallback mDeviceStateCallback; @@ -78,7 +81,13 @@ public class DeviceStateRotationLockSettingControllerTest extends SysuiTestCase mSettingsManager = DeviceStateRotationLockSettingsManager.getInstance(mContext); mDeviceStateRotationLockSettingController = new DeviceStateRotationLockSettingController( - mFakeRotationPolicy, mDeviceStateManager, mFakeExecutor, mSettingsManager); + mFakeRotationPolicy, + mDeviceStateManager, + mFakeExecutor, + mSettingsManager, + mLogger, + mDumpManager + ); mDeviceStateRotationLockSettingController.setListening(true); verify(mDeviceStateManager) @@ -173,15 +182,11 @@ public class DeviceStateRotationLockSettingControllerTest extends SysuiTestCase } @Test - public void whenDeviceStateSwitchedToIgnoredState_usePreviousSetting() { - initializeSettingsWith( - 0, DEVICE_STATE_ROTATION_LOCK_IGNORED, 1, DEVICE_STATE_ROTATION_LOCK_UNLOCKED); - mFakeRotationPolicy.setRotationLock(true); - - mDeviceStateCallback.onStateChanged(1); - assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse(); - + public void whenDeviceStateSwitchedToIgnoredState_useFallbackSetting() { mDeviceStateCallback.onStateChanged(0); + assertThat(mFakeRotationPolicy.isRotationLocked()).isTrue(); + + mDeviceStateCallback.onStateChanged(2); assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse(); } 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 f8bf4b91e11a..4525ad27b749 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt @@ -65,8 +65,6 @@ class StylusManagerTest : SysuiTestCase() { @Mock lateinit var uiEventLogger: UiEventLogger @Mock lateinit var stylusCallback: StylusManager.StylusCallback @Mock lateinit var otherStylusCallback: StylusManager.StylusCallback - @Mock lateinit var stylusBatteryCallback: StylusManager.StylusBatteryCallback - @Mock lateinit var otherStylusBatteryCallback: StylusManager.StylusBatteryCallback private lateinit var mockitoSession: StaticMockitoSession private lateinit var stylusManager: StylusManager @@ -123,7 +121,6 @@ class StylusManagerTest : SysuiTestCase() { stylusManager.startListener() stylusManager.registerCallback(stylusCallback) - stylusManager.registerBatteryCallback(stylusBatteryCallback) clearInvocations(inputManager) } @@ -434,23 +431,6 @@ class StylusManagerTest : SysuiTestCase() { } @Test - fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() { - stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) - stylusManager.registerBatteryCallback(otherStylusBatteryCallback) - - stylusManager.onMetadataChanged( - bluetoothDevice, - BluetoothDevice.METADATA_MAIN_CHARGING, - "true".toByteArray() - ) - - verify(stylusBatteryCallback, times(1)) - .onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, true) - verify(otherStylusBatteryCallback, times(1)) - .onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, true) - } - - @Test fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -460,7 +440,7 @@ class StylusManagerTest : SysuiTestCase() { "true".toByteArray() ) - verify(stylusBatteryCallback, times(1)) + verify(stylusCallback, times(1)) .onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, true) } @@ -474,7 +454,7 @@ class StylusManagerTest : SysuiTestCase() { "false".toByteArray() ) - verify(stylusBatteryCallback, times(1)) + verify(stylusCallback, times(1)) .onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, false) } @@ -486,7 +466,7 @@ class StylusManagerTest : SysuiTestCase() { "true".toByteArray() ) - verifyNoMoreInteractions(stylusBatteryCallback) + verifyNoMoreInteractions(stylusCallback) } @Test @@ -499,8 +479,7 @@ class StylusManagerTest : SysuiTestCase() { "true".toByteArray() ) - verify(stylusBatteryCallback, never()) - .onStylusBluetoothChargingStateChanged(any(), any(), any()) + verify(stylusCallback, never()).onStylusBluetoothChargingStateChanged(any(), any(), any()) } @Test @@ -614,7 +593,7 @@ class StylusManagerTest : SysuiTestCase() { fun onBatteryStateChanged_executesBatteryCallbacks() { stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) - verify(stylusBatteryCallback, times(1)) + verify(stylusCallback, times(1)) .onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) } 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 82b80f53d6b0..3db0ecc4e8df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt @@ -96,7 +96,6 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { startable.start() verify(stylusManager, times(1)).registerCallback(startable) - verify(stylusManager, times(1)).registerBatteryCallback(startable) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt index 85cfef727954..fd368eb07b5b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt @@ -16,22 +16,24 @@ package com.android.systemui.unfold.updates +import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Looper import android.testing.AndroidTestingRunner -import android.view.IRotationWatcher -import android.view.IWindowManager +import android.view.Display import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.utils.os.FakeHandler import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.anyInt import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations @@ -42,19 +44,23 @@ class RotationChangeProviderTest : SysuiTestCase() { private lateinit var rotationChangeProvider: RotationChangeProvider - @Mock lateinit var windowManagerInterface: IWindowManager + @Mock lateinit var displayManager: DisplayManager @Mock lateinit var listener: RotationListener - @Captor lateinit var rotationWatcher: ArgumentCaptor<IRotationWatcher> - private val fakeExecutor = FakeExecutor(FakeSystemClock()) + @Mock lateinit var display: Display + @Captor lateinit var displayListener: ArgumentCaptor<DisplayManager.DisplayListener> + private val fakeHandler = FakeHandler(Looper.getMainLooper()) + + private lateinit var spyContext: Context @Before fun setup() { MockitoAnnotations.initMocks(this) - rotationChangeProvider = - RotationChangeProvider(windowManagerInterface, context, fakeExecutor) + spyContext = spy(context) + whenever(spyContext.display).thenReturn(display) + rotationChangeProvider = RotationChangeProvider(displayManager, spyContext, fakeHandler) rotationChangeProvider.addCallback(listener) - fakeExecutor.runAllReady() - verify(windowManagerInterface).watchRotation(rotationWatcher.capture(), anyInt()) + fakeHandler.dispatchQueuedMessages() + verify(displayManager).registerDisplayListener(displayListener.capture(), any()) } @Test @@ -70,15 +76,16 @@ class RotationChangeProviderTest : SysuiTestCase() { verify(listener).onRotationChanged(42) rotationChangeProvider.removeCallback(listener) - fakeExecutor.runAllReady() + fakeHandler.dispatchQueuedMessages() sendRotationUpdate(43) - verify(windowManagerInterface).removeRotationWatcher(any()) + verify(displayManager).unregisterDisplayListener(any()) verifyNoMoreInteractions(listener) } private fun sendRotationUpdate(newRotation: Int) { - rotationWatcher.value.onRotationChanged(newRotation) - fakeExecutor.runAllReady() + whenever(display.rotation).thenReturn(newRotation) + displayListener.allValues.forEach { it.onDisplayChanged(display.displayId) } + fakeHandler.dispatchQueuedMessages() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java index 31cce4f3168b..468c5a73645b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java @@ -88,7 +88,7 @@ public class ImageWallpaperTest extends SysuiTestCase { @Mock private Bitmap mWallpaperBitmap; FakeSystemClock mFakeSystemClock = new FakeSystemClock(); - FakeExecutor mFakeBackgroundExecutor = new FakeExecutor(mFakeSystemClock); + FakeExecutor mFakeExecutor = new FakeExecutor(mFakeSystemClock); @Before public void setUp() throws Exception { @@ -125,7 +125,7 @@ public class ImageWallpaperTest extends SysuiTestCase { @Test public void testBitmapWallpaper_normal() { - // Will use a image wallpaper with dimensions DISPLAY_WIDTH x DISPLAY_WIDTH. + // Will use an image wallpaper with dimensions DISPLAY_WIDTH x DISPLAY_WIDTH. // Then we expect the surface size will be also DISPLAY_WIDTH x DISPLAY_WIDTH. int bitmapSide = DISPLAY_WIDTH; testSurfaceHelper( @@ -137,7 +137,7 @@ public class ImageWallpaperTest extends SysuiTestCase { @Test public void testBitmapWallpaper_low_resolution() { - // Will use a image wallpaper with dimensions BMP_WIDTH x BMP_HEIGHT. + // Will use an image wallpaper with dimensions BMP_WIDTH x BMP_HEIGHT. // Then we expect the surface size will be also BMP_WIDTH x BMP_HEIGHT. testSurfaceHelper(LOW_BMP_WIDTH /* bitmapWidth */, LOW_BMP_HEIGHT /* bitmapHeight */, @@ -161,13 +161,13 @@ public class ImageWallpaperTest extends SysuiTestCase { ImageWallpaper.CanvasEngine spyEngine = getSpyEngine(); spyEngine.onCreate(mSurfaceHolder); spyEngine.onSurfaceRedrawNeeded(mSurfaceHolder); - assertThat(mFakeBackgroundExecutor.numPending()).isAtLeast(1); + assertThat(mFakeExecutor.numPending()).isAtLeast(1); int n = 0; - while (mFakeBackgroundExecutor.numPending() >= 1) { + while (mFakeExecutor.numPending() >= 1) { n++; assertThat(n).isAtMost(10); - mFakeBackgroundExecutor.runNextReady(); + mFakeExecutor.runNextReady(); mFakeSystemClock.advanceTime(1000); } @@ -176,7 +176,7 @@ public class ImageWallpaperTest extends SysuiTestCase { } private ImageWallpaper createImageWallpaper() { - return new ImageWallpaper(mFakeBackgroundExecutor, mUserTracker) { + return new ImageWallpaper(mFakeExecutor, mUserTracker) { @Override public Engine onCreateEngine() { return new CanvasEngine() { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt new file mode 100644 index 000000000000..4e435462be50 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt @@ -0,0 +1,41 @@ +/* + * 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.keyboard.data.repository + +import com.android.systemui.keyboard.shared.model.BacklightModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull + +class FakeKeyboardRepository : KeyboardRepository { + + private val _keyboardConnected = MutableStateFlow(false) + override val keyboardConnected: Flow<Boolean> = _keyboardConnected + + private val _backlightState: MutableStateFlow<BacklightModel?> = MutableStateFlow(null) + // filtering to make sure backlight doesn't have default initial value + override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull() + + fun setBacklight(state: BacklightModel) { + _backlightState.value = state + } + + fun setKeyboardConnected(connected: Boolean) { + _keyboardConnected.value = connected + } +} 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 1a371c73550c..194ed02712b2 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 @@ -47,6 +47,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _isKeyguardShowing = MutableStateFlow(false) override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing + private val _isKeyguardUnlocked = MutableStateFlow(false) + override val isKeyguardUnlocked: Flow<Boolean> = _isKeyguardUnlocked + private val _isKeyguardOccluded = MutableStateFlow(false) override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index eac1bd145033..16442bb525b6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -37,7 +37,7 @@ class FakeKeyguardTransitionRepository : KeyguardTransitionRepository { _transitions.emit(step) } - override fun startTransition(info: TransitionInfo): UUID? { + override fun startTransition(info: TransitionInfo, resetIfCanceled: Boolean): UUID? { return null } diff --git a/packages/SystemUI/unfold/Android.bp b/packages/SystemUI/unfold/Android.bp index 180b611aa13b..2e0a9462ffbe 100644 --- a/packages/SystemUI/unfold/Android.bp +++ b/packages/SystemUI/unfold/Android.bp @@ -35,6 +35,7 @@ android_library { ], kotlincflags: ["-Xjvm-default=enable"], java_version: "1.8", + sdk_version: "current", min_sdk_version: "current", plugins: ["dagger2-compiler"], } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt index 068347cfe9d8..c3a6cf035d09 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt @@ -19,14 +19,15 @@ package com.android.systemui.unfold import android.content.ContentResolver import android.content.Context import android.hardware.SensorManager +import android.hardware.display.DisplayManager import android.os.Handler -import android.view.IWindowManager import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.dagger.UnfoldMain import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver import com.android.systemui.unfold.updates.FoldProvider import com.android.systemui.unfold.updates.RotationChangeProvider +import com.android.systemui.unfold.updates.hinge.HingeAngleProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider import com.android.systemui.unfold.util.CurrentActivityTypeProvider import com.android.systemui.unfold.util.UnfoldTransitionATracePrefix @@ -61,12 +62,13 @@ interface UnfoldSharedComponent { @BindsInstance @UnfoldMain executor: Executor, @BindsInstance @UnfoldSingleThreadBg singleThreadBgExecutor: Executor, @BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String, - @BindsInstance windowManager: IWindowManager, + @BindsInstance displayManager: DisplayManager, @BindsInstance contentResolver: ContentResolver = context.contentResolver ): UnfoldSharedComponent } val unfoldTransitionProvider: Optional<UnfoldTransitionProgressProvider> + val hingeAngleProvider: HingeAngleProvider val rotationChangeProvider: RotationChangeProvider } @@ -84,8 +86,9 @@ interface RemoteUnfoldSharedComponent { @BindsInstance context: Context, @BindsInstance config: UnfoldTransitionConfig, @BindsInstance @UnfoldMain executor: Executor, + @BindsInstance @UnfoldMain handler: Handler, @BindsInstance @UnfoldSingleThreadBg singleThreadBgExecutor: Executor, - @BindsInstance windowManager: IWindowManager, + @BindsInstance displayManager: DisplayManager, @BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String, ): RemoteUnfoldSharedComponent } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt index 8eb79df55496..18399194434a 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt @@ -19,8 +19,8 @@ package com.android.systemui.unfold import android.content.Context import android.hardware.SensorManager +import android.hardware.display.DisplayManager import android.os.Handler -import android.view.IWindowManager import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.updates.FoldProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider @@ -47,7 +47,7 @@ fun createUnfoldSharedComponent( mainExecutor: Executor, singleThreadBgExecutor: Executor, tracingTagPrefix: String, - windowManager: IWindowManager, + displayManager: DisplayManager, ): UnfoldSharedComponent = DaggerUnfoldSharedComponent.factory() .create( @@ -61,7 +61,7 @@ fun createUnfoldSharedComponent( mainExecutor, singleThreadBgExecutor, tracingTagPrefix, - windowManager, + displayManager, ) /** @@ -73,16 +73,18 @@ fun createRemoteUnfoldSharedComponent( context: Context, config: UnfoldTransitionConfig, mainExecutor: Executor, + mainHandler: Handler, singleThreadBgExecutor: Executor, tracingTagPrefix: String, - windowManager: IWindowManager, + displayManager: DisplayManager, ): RemoteUnfoldSharedComponent = DaggerRemoteUnfoldSharedComponent.factory() .create( context, config, mainExecutor, + mainHandler, singleThreadBgExecutor, - windowManager, + displayManager, tracingTagPrefix, ) diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt index d19b414cb963..28e493651137 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt @@ -16,7 +16,6 @@ package com.android.systemui.unfold.progress import android.os.Trace -import android.os.Trace.TRACE_TAG_APP import android.util.Log import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FloatPropertyCompat @@ -110,7 +109,7 @@ class PhysicsBasedUnfoldTransitionProgressProvider @Inject constructor( if (DEBUG) { Log.d(TAG, "onFoldUpdate = ${update.name()}") - Trace.traceCounter(Trace.TRACE_TAG_APP, "fold_update", update) + Trace.setCounter("fold_update", update.toLong()) } } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt index 82fd2258120a..d653fc7beff2 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt @@ -119,7 +119,7 @@ constructor( "lastHingeAngle: $lastHingeAngle, " + "lastHingeAngleBeforeTransition: $lastHingeAngleBeforeTransition" ) - Trace.traceCounter(Trace.TRACE_TAG_APP, "hinge_angle", angle.toInt()) + Trace.setCounter( "hinge_angle", angle.toLong()) } val currentDirection = diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt index 0cf8224d3a3f..ce8f1a178d05 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt @@ -17,36 +17,32 @@ package com.android.systemui.unfold.updates import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Handler import android.os.RemoteException -import android.view.IRotationWatcher -import android.view.IWindowManager -import android.view.Surface.Rotation import com.android.systemui.unfold.dagger.UnfoldMain import com.android.systemui.unfold.util.CallbackController -import java.util.concurrent.Executor import javax.inject.Inject /** - * Allows to subscribe to rotation changes. - * - * This is needed as rotation updates from [IWindowManager] are received in a binder thread, while - * most of the times we want them in the main one. Updates are provided for the display associated + * Allows to subscribe to rotation changes. Updates are provided for the display associated * to [context]. */ class RotationChangeProvider @Inject constructor( - private val windowManagerInterface: IWindowManager, + private val displayManager: DisplayManager, private val context: Context, - @UnfoldMain private val mainExecutor: Executor, + @UnfoldMain private val mainHandler: Handler, ) : CallbackController<RotationChangeProvider.RotationListener> { private val listeners = mutableListOf<RotationListener>() - private val rotationWatcher = RotationWatcher() + private val displayListener = RotationDisplayListener() + private var lastRotation: Int? = null override fun addCallback(listener: RotationListener) { - mainExecutor.execute { + mainHandler.post { if (listeners.isEmpty()) { subscribeToRotation() } @@ -55,17 +51,18 @@ constructor( } override fun removeCallback(listener: RotationListener) { - mainExecutor.execute { + mainHandler.post { listeners -= listener if (listeners.isEmpty()) { unsubscribeToRotation() + lastRotation = null } } } private fun subscribeToRotation() { try { - windowManagerInterface.watchRotation(rotationWatcher, context.displayId) + displayManager.registerDisplayListener(displayListener, mainHandler) } catch (e: RemoteException) { throw e.rethrowFromSystemServer() } @@ -73,7 +70,7 @@ constructor( private fun unsubscribeToRotation() { try { - windowManagerInterface.removeRotationWatcher(rotationWatcher) + displayManager.unregisterDisplayListener(displayListener) } catch (e: RemoteException) { throw e.rethrowFromSystemServer() } @@ -82,12 +79,25 @@ constructor( /** Gets notified of rotation changes. */ fun interface RotationListener { /** Called once rotation changes. */ - fun onRotationChanged(@Rotation newRotation: Int) + fun onRotationChanged(newRotation: Int) } - private inner class RotationWatcher : IRotationWatcher.Stub() { - override fun onRotationChanged(rotation: Int) { - mainExecutor.execute { listeners.forEach { it.onRotationChanged(rotation) } } + private inner class RotationDisplayListener : DisplayManager.DisplayListener { + + override fun onDisplayChanged(displayId: Int) { + val display = context.display ?: return + + if (displayId == display.displayId) { + val currentRotation = display.rotation + if (lastRotation == null || lastRotation != currentRotation) { + listeners.forEach { it.onRotationChanged(currentRotation) } + lastRotation = currentRotation + } + } } + + override fun onDisplayAdded(displayId: Int) {} + + override fun onDisplayRemoved(displayId: Int) {} } } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt index 06ca153b694b..ce5c5f91914b 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt @@ -79,10 +79,9 @@ constructor( companion object { fun ContentResolver.areAnimationsEnabled(): Boolean { val animationScale = - Settings.Global.getStringForUser( + Settings.Global.getString( this, Settings.Global.ANIMATOR_DURATION_SCALE, - this.userId ) ?.toFloatOrNull() ?: 1f diff --git a/services/accessibility/java/com/android/server/accessibility/FlashNotificationsController.java b/services/accessibility/java/com/android/server/accessibility/FlashNotificationsController.java index a94ab34bc8b3..fa30a6f419f9 100644 --- a/services/accessibility/java/com/android/server/accessibility/FlashNotificationsController.java +++ b/services/accessibility/java/com/android/server/accessibility/FlashNotificationsController.java @@ -255,7 +255,7 @@ class FlashNotificationsController { broadcastFilter.addAction(ACTION_FLASH_NOTIFICATION_STOP_PREVIEW); mFlashBroadcastReceiver = new FlashBroadcastReceiver(); mContext.registerReceiver( - mFlashBroadcastReceiver, broadcastFilter, Context.RECEIVER_EXPORTED); + mFlashBroadcastReceiver, broadcastFilter, Context.RECEIVER_NOT_EXPORTED); final PowerManager powerManager = mContext.getSystemService(PowerManager.class); mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index a35cae9dffda..542cc2f0a0a6 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -59,6 +59,8 @@ import android.companion.DeviceNotAssociatedException; import android.companion.IAssociationRequestCallback; import android.companion.ICompanionDeviceManager; import android.companion.IOnAssociationsChangedListener; +import android.companion.IOnMessageReceivedListener; +import android.companion.IOnTransportsChangedListener; import android.companion.ISystemDataTransferCallback; import android.content.ComponentName; import android.content.Context; @@ -232,7 +234,7 @@ public class CompanionDeviceManagerService extends SystemService { /* cdmService */this, mAssociationStore); mCompanionAppController = new CompanionApplicationController( context, mAssociationStore, mDevicePresenceMonitor); - mTransportManager = new CompanionTransportManager(context); + mTransportManager = new CompanionTransportManager(context, mAssociationStore); mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); @@ -601,6 +603,37 @@ public class CompanionDeviceManagerService extends SystemService { } @Override + @GuardedBy("CompanionDeviceManagerService.this.mTransportManager.mTransports") + public void addOnTransportsChangedListener(IOnTransportsChangedListener listener) { + mTransportManager.addListener(listener); + } + + @Override + @GuardedBy("CompanionDeviceManagerService.this.mTransportManager.mTransports") + public void removeOnTransportsChangedListener(IOnTransportsChangedListener listener) { + mTransportManager.removeListener(listener); + } + + @Override + @GuardedBy("CompanionDeviceManagerService.this.mTransportManager.mTransports") + public void sendMessage(int messageType, byte[] data, int[] associationIds) { + mTransportManager.sendMessage(messageType, data, associationIds); + } + + @Override + @GuardedBy("CompanionDeviceManagerService.this.mTransportManager.mTransports") + public void addOnMessageReceivedListener(int messageType, + IOnMessageReceivedListener listener) { + mTransportManager.addListener(messageType, listener); + } + + @Override + public void removeOnMessageReceivedListener(int messageType, + IOnMessageReceivedListener listener) { + mTransportManager.removeListener(messageType, listener); + } + + @Override public void legacyDisassociate(String deviceMacAddress, String packageName, int userId) { Log.i(TAG, "legacyDisassociate() pkg=u" + userId + "/" + packageName + ", macAddress=" + deviceMacAddress); diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index 3fffdbecd0de..f3a949d29ff8 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -31,6 +31,7 @@ import android.annotation.UserIdInt; import android.app.PendingIntent; import android.companion.AssociationInfo; import android.companion.DeviceNotAssociatedException; +import android.companion.IOnMessageReceivedListener; import android.companion.ISystemDataTransferCallback; import android.companion.datatransfer.PermissionSyncRequest; import android.companion.datatransfer.SystemDataTransferRequest; @@ -40,6 +41,7 @@ import android.content.Intent; import android.os.Binder; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; @@ -92,8 +94,18 @@ public class SystemDataTransferProcessor { mAssociationStore = associationStore; mSystemDataTransferRequestStore = systemDataTransferRequestStore; mTransportManager = transportManager; - mTransportManager.addListener(MESSAGE_REQUEST_PERMISSION_RESTORE, - this::onReceivePermissionRestore); + IOnMessageReceivedListener messageListener = new IOnMessageReceivedListener() { + @Override + public void onMessageReceived(int associationId, byte[] data) throws RemoteException { + onReceivePermissionRestore(data); + } + + @Override + public IBinder asBinder() { + return null; + } + }; + mTransportManager.addListener(MESSAGE_REQUEST_PERMISSION_RESTORE, messageListener); mPermissionControllerManager = mContext.getSystemService(PermissionControllerManager.class); mExecutor = Executors.newSingleThreadExecutor(); } diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java index 8dab231d1819..539020519f84 100644 --- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -22,25 +22,32 @@ import static com.android.server.companion.transport.Transport.MESSAGE_REQUEST_P import static com.android.server.companion.transport.Transport.MESSAGE_REQUEST_PLATFORM_INFO; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManagerInternal; +import android.companion.AssociationInfo; +import android.companion.IOnMessageReceivedListener; +import android.companion.IOnTransportsChangedListener; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Binder; import android.os.Build; +import android.os.IBinder; import android.os.ParcelFileDescriptor; +import android.os.RemoteCallbackList; +import android.os.RemoteException; import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.server.LocalServices; -import com.android.server.companion.transport.Transport.Listener; +import com.android.server.companion.AssociationStore; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -63,31 +70,100 @@ public class CompanionTransportManager { } private final Context mContext; + private final AssociationStore mAssociationStore; + /** Association id -> Transport */ @GuardedBy("mTransports") private final SparseArray<Transport> mTransports = new SparseArray<>(); - @NonNull - private final Map<Integer, Listener> mListeners = new HashMap<>(); + private final RemoteCallbackList<IOnTransportsChangedListener> mTransportsListeners = + new RemoteCallbackList<>(); + /** Message type -> IOnMessageReceivedListener */ + @NonNull + private final SparseArray<IOnMessageReceivedListener> mMessageListeners = new SparseArray<>(); + + @Nullable private Transport mTempTransport; - public CompanionTransportManager(Context context) { + public CompanionTransportManager(Context context, AssociationStore associationStore) { mContext = context; + mAssociationStore = associationStore; } /** - * Add a message listener when a message is received for the message type + * Add a listener to receive callbacks when a message is received for the message type */ @GuardedBy("mTransports") - public void addListener(int message, @NonNull Listener listener) { - mListeners.put(message, listener); + public void addListener(int message, @NonNull IOnMessageReceivedListener listener) { + mMessageListeners.put(message, listener); for (int i = 0; i < mTransports.size(); i++) { mTransports.valueAt(i).addListener(message, listener); } } /** + * Add a listener to receive callbacks when any of the transports is changed + */ + @GuardedBy("mTransports") + public void addListener(IOnTransportsChangedListener listener) { + Slog.i(TAG, "Registering OnTransportsChangedListener"); + mTransportsListeners.register(listener); + List<AssociationInfo> associations = new ArrayList<>(); + for (int i = 0; i < mTransports.size(); i++) { + AssociationInfo association = mAssociationStore.getAssociationById( + mTransports.keyAt(i)); + if (association != null) { + associations.add(association); + } + } + mTransportsListeners.broadcast(listener1 -> { + // callback to the current listener with all the associations of the transports + // immediately + if (listener1 == listener) { + try { + listener.onTransportsChanged(associations); + } catch (RemoteException ignored) { + } + } + }); + } + + /** + * Remove the listener for receiving callbacks when any of the transports is changed + */ + public void removeListener(IOnTransportsChangedListener listener) { + mTransportsListeners.unregister(listener); + } + + /** + * Remove the listener to stop receiving calbacks when a message is received for the given type + */ + public void removeListener(int messageType, IOnMessageReceivedListener listener) { + mMessageListeners.remove(messageType); + } + + /** + * Send a message to remote devices through the transports + */ + @GuardedBy("mTransports") + public void sendMessage(int message, byte[] data, int[] associationIds) { + Slog.i(TAG, "Sending message 0x" + Integer.toHexString(message) + + " data length " + data.length); + for (int i = 0; i < associationIds.length; i++) { + if (mTransports.contains(associationIds[i])) { + try { + mTransports.get(associationIds[i]).sendMessage(message, data); + } catch (IOException e) { + Slog.e(TAG, "Failed to send message 0x" + Integer.toHexString(message) + + " data length " + data.length + " to association " + + associationIds[i]); + } + } + } + } + + /** * For the moment, we only offer transporting of system data to built-in * companion apps; future work will improve the security model to support * third-party companion apps. @@ -119,6 +195,8 @@ public class CompanionTransportManager { } initializeTransport(associationId, fd); + + notifyOnTransportsChanged(); } } @@ -130,16 +208,35 @@ public class CompanionTransportManager { mTransports.delete(associationId); transport.stop(); } + + notifyOnTransportsChanged(); } } @GuardedBy("mTransports") + private void notifyOnTransportsChanged() { + List<AssociationInfo> associations = new ArrayList<>(); + for (int i = 0; i < mTransports.size(); i++) { + AssociationInfo association = mAssociationStore.getAssociationById( + mTransports.keyAt(i)); + if (association != null) { + associations.add(association); + } + } + mTransportsListeners.broadcast(listener -> { + try { + listener.onTransportsChanged(associations); + } catch (RemoteException ignored) { + } + }); + } + + @GuardedBy("mTransports") private void initializeTransport(int associationId, ParcelFileDescriptor fd) { + Slog.i(TAG, "Initializing transport"); if (!isSecureTransportEnabled()) { Transport transport = new RawTransport(associationId, fd, mContext); - for (Map.Entry<Integer, Listener> entry : mListeners.entrySet()) { - transport.addListener(entry.getKey(), entry.getValue()); - } + addMessageListenersToTransport(transport); transport.start(); mTransports.put(associationId, transport); Slog.i(TAG, "RawTransport is created"); @@ -148,10 +245,21 @@ public class CompanionTransportManager { // Exchange platform info to decide which transport should be created mTempTransport = new RawTransport(associationId, fd, mContext); - for (Map.Entry<Integer, Listener> entry : mListeners.entrySet()) { - mTempTransport.addListener(entry.getKey(), entry.getValue()); - } - mTempTransport.addListener(MESSAGE_REQUEST_PLATFORM_INFO, this::onPlatformInfoReceived); + addMessageListenersToTransport(mTempTransport); + IOnMessageReceivedListener listener = new IOnMessageReceivedListener() { + @Override + public void onMessageReceived(int associationId, byte[] data) throws RemoteException { + synchronized (mTransports) { + onPlatformInfoReceived(associationId, data); + } + } + + @Override + public IBinder asBinder() { + return null; + } + }; + mTempTransport.addListener(MESSAGE_REQUEST_PLATFORM_INFO, listener); mTempTransport.start(); int sdk = Build.VERSION.SDK_INT; @@ -163,14 +271,21 @@ public class CompanionTransportManager { .put(release.getBytes()); // TODO: it should check if preSharedKey is given - mTempTransport.requestForResponse(MESSAGE_REQUEST_PLATFORM_INFO, data.array()); + try { + mTempTransport.sendMessage(MESSAGE_REQUEST_PLATFORM_INFO, data.array()); + } catch (IOException e) { + Slog.e(TAG, "Failed to exchange platform info"); + } } /** * Depending on the remote platform info to decide which transport should be created */ - @GuardedBy("mTransports") - private void onPlatformInfoReceived(byte[] data) { + @GuardedBy("CompanionTransportManager.this.mTransports") + private void onPlatformInfoReceived(int associationId, byte[] data) { + if (mTempTransport.getAssociationId() != associationId) { + return; + } // TODO: it should check if preSharedKey is given ByteBuffer buffer = ByteBuffer.wrap(data); @@ -198,12 +313,11 @@ public class CompanionTransportManager { Slog.i(TAG, "Creating a secure channel"); transport = new SecureTransport(transport.getAssociationId(), transport.getFd(), mContext); - for (Map.Entry<Integer, Listener> entry : mListeners.entrySet()) { - transport.addListener(entry.getKey(), entry.getValue()); - } + addMessageListenersToTransport(transport); transport.start(); } mTransports.put(transport.getAssociationId(), transport); + // Doesn't need to notifyTransportsChanged here, it'll be done in attachSystemDataTransport } public Future<?> requestPermissionRestore(int associationId, byte[] data) { @@ -228,4 +342,10 @@ public class CompanionTransportManager { return enabled; } + + private void addMessageListenersToTransport(Transport transport) { + for (int i = 0; i < mMessageListeners.size(); i++) { + transport.addListener(mMessageListeners.keyAt(i), mMessageListeners.valueAt(i)); + } + } } diff --git a/services/companion/java/com/android/server/companion/transport/Transport.java b/services/companion/java/com/android/server/companion/transport/Transport.java index e984c637b44c..d69ce8909c74 100644 --- a/services/companion/java/com/android/server/companion/transport/Transport.java +++ b/services/companion/java/com/android/server/companion/transport/Transport.java @@ -17,10 +17,12 @@ package com.android.server.companion.transport; import android.annotation.NonNull; +import android.companion.IOnMessageReceivedListener; import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; import android.os.ParcelFileDescriptor; +import android.os.RemoteException; import android.util.Slog; import android.util.SparseArray; @@ -45,7 +47,8 @@ public abstract class Transport { protected static final boolean DEBUG = Build.IS_DEBUGGABLE; static final int MESSAGE_REQUEST_PING = 0x63807378; // ?PIN - public static final int MESSAGE_REQUEST_PLATFORM_INFO = 0x63807086; // ?PFV + public static final int MESSAGE_REQUEST_PLATFORM_INFO = 0x63807073; // ?PFI + public static final int MESSAGE_REQUEST_CONTEXT_SYNC = 0x63678883; // ?CXS public static final int MESSAGE_REQUEST_PERMISSION_RESTORE = 0x63826983; // ?RES static final int MESSAGE_RESPONSE_SUCCESS = 0x33838567; // !SUC @@ -59,19 +62,14 @@ public abstract class Transport { protected final OutputStream mRemoteOut; protected final Context mContext; - /** Message type -> Listener */ - private final Map<Integer, Listener> mListeners; - /** - * Message listener + * Message type -> Listener + * + * For now, the transport only supports 1 listener for each message type. If there's a need in + * the future to allow multiple listeners to receive callbacks for the same message type, the + * value of the map can be a list. */ - public interface Listener { - /** - * Called when a message is received - * @param data data content in the message - */ - void onDataReceived(byte[] data); - } + private final Map<Integer, IOnMessageReceivedListener> mListeners; private static boolean isRequest(int message) { return (message & 0xFF000000) == 0x63000000; @@ -100,7 +98,7 @@ public abstract class Transport { * @param message Message type * @param listener Execute when a message with the type is received */ - public void addListener(int message, Listener listener) { + public void addListener(int message, IOnMessageReceivedListener listener) { mListeners.put(message, listener); } @@ -117,6 +115,13 @@ public abstract class Transport { protected abstract void sendMessage(int message, int sequence, @NonNull byte[] data) throws IOException; + /** + * Send a message + */ + public void sendMessage(int message, @NonNull byte[] data) throws IOException { + sendMessage(message, mNextSequence.incrementAndGet(), data); + } + public Future<byte[]> requestForResponse(int message, byte[] data) { if (DEBUG) Slog.d(TAG, "Requesting for response"); final int sequence = mNextSequence.incrementAndGet(); @@ -165,7 +170,8 @@ public abstract class Transport { sendMessage(MESSAGE_RESPONSE_SUCCESS, sequence, data); break; } - case MESSAGE_REQUEST_PLATFORM_INFO: { + case MESSAGE_REQUEST_PLATFORM_INFO: + case MESSAGE_REQUEST_CONTEXT_SYNC: { callback(message, data); sendMessage(MESSAGE_RESPONSE_SUCCESS, sequence, EmptyArray.BYTE); break; @@ -196,7 +202,13 @@ public abstract class Transport { private void callback(int message, byte[] data) { if (mListeners.containsKey(message)) { - mListeners.get(message).onDataReceived(data); + try { + mListeners.get(message).onMessageReceived(getAssociationId(), data); + Slog.i(TAG, "Message 0x" + Integer.toHexString(message) + + " is received from associationId " + mAssociationId + + ", sending data length " + data.length + " to the listener."); + } catch (RemoteException ignored) { + } } } diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java index 1c4602803426..34033e225b80 100644 --- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java +++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java @@ -134,7 +134,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController private final ArraySet<RunningAppsChangedListener> mRunningAppsChangedListeners = new ArraySet<>(); @Nullable private final SecureWindowCallback mSecureWindowCallback; - @Nullable private final List<String> mDisplayCategories; + @Nullable private final Set<String> mDisplayCategories; private final boolean mShowTasksInHostDeviceRecents; @@ -178,7 +178,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController @NonNull ActivityBlockedCallback activityBlockedCallback, @NonNull SecureWindowCallback secureWindowCallback, @NonNull IntentListenerCallback intentListenerCallback, - @NonNull List<String> displayCategories, + @NonNull Set<String> displayCategories, boolean showTasksInHostDeviceRecents) { super(); mAllowedUsers = allowedUsers; diff --git a/services/companion/java/com/android/server/companion/virtual/SensorController.java b/services/companion/java/com/android/server/companion/virtual/SensorController.java index 864fe0f5edc1..7df0d861dc22 100644 --- a/services/companion/java/com/android/server/companion/virtual/SensorController.java +++ b/services/companion/java/com/android/server/companion/virtual/SensorController.java @@ -99,6 +99,9 @@ public class SensorController { private int createSensorInternal(IBinder sensorToken, VirtualSensorConfig config) throws SensorCreationException { + if (config.getType() <= 0) { + throw new SensorCreationException("Received an invalid virtual sensor type."); + } final int handle = mSensorManagerInternal.createRuntimeSensor(mVirtualDeviceId, config.getType(), config.getName(), config.getVendor() == null ? "" : config.getVendor(), config.getFlags(), diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 2d010cf66e9b..b338d89a0169 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -97,6 +97,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; @@ -830,7 +831,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } private GenericWindowPolicyController createWindowPolicyController( - @NonNull List<String> displayCategories) { + @NonNull Set<String> displayCategories) { final GenericWindowPolicyController gwpc = new GenericWindowPolicyController(FLAG_SECURE, SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, diff --git a/services/core/java/com/android/server/LogMteState.java b/services/core/java/com/android/server/LogMteState.java new file mode 100644 index 000000000000..410dd8339b30 --- /dev/null +++ b/services/core/java/com/android/server/LogMteState.java @@ -0,0 +1,53 @@ +/* + * 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; + +import android.app.StatsManager; +import android.content.Context; +import android.util.StatsEvent; + +import com.android.internal.os.BackgroundThread; +import com.android.internal.os.Zygote; +import com.android.internal.util.FrameworkStatsLog; + +import java.util.List; + +public class LogMteState { + public static void register(Context context) { + context.getSystemService(StatsManager.class) + .setPullAtomCallback( + FrameworkStatsLog.MTE_STATE, + null, // use default PullAtomMetadata values + BackgroundThread.getExecutor(), + new StatsManager.StatsPullAtomCallback() { + @Override + public int onPullAtom(int atomTag, List<StatsEvent> data) { + if (atomTag != FrameworkStatsLog.MTE_STATE) { + throw new UnsupportedOperationException( + "Unknown tagId=" + atomTag); + } + data.add( + FrameworkStatsLog.buildStatsEvent( + FrameworkStatsLog.MTE_STATE, + Zygote.nativeSupportsMemoryTagging() + ? FrameworkStatsLog.MTE_STATE__STATE__ON + : FrameworkStatsLog.MTE_STATE__STATE__OFF)); + return StatsManager.PULL_SUCCESS; + } + }); + } +} diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index aaa376afa139..bffa3dd79754 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -1672,57 +1672,62 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { return; } - synchronized (mRecords) { - String str = "notifyServiceStateForSubscriber: subId=" + subId + " phoneId=" + phoneId - + " state=" + state; - if (VDBG) { - log(str); - } - mLocalLog.log(str); - // for service state updates, don't notify clients when subId is invalid. This prevents - // us from sending incorrect notifications like b/133140128 - // In the future, we can remove this logic for every notification here and add a - // callback so listeners know when their PhoneStateListener's subId becomes invalid, but - // for now we use the simplest fix. - if (validatePhoneId(phoneId) && SubscriptionManager.isValidSubscriptionId(subId)) { - mServiceState[phoneId] = state; + final long callingIdentity = Binder.clearCallingIdentity(); + try { + synchronized (mRecords) { + String str = "notifyServiceStateForSubscriber: subId=" + subId + " phoneId=" + + phoneId + " state=" + state; + if (VDBG) { + log(str); + } + mLocalLog.log(str); + // for service state updates, don't notify clients when subId is invalid. This + // prevents us from sending incorrect notifications like b/133140128 + // In the future, we can remove this logic for every notification here and add a + // callback so listeners know when their PhoneStateListener's subId becomes invalid, + // but for now we use the simplest fix. + if (validatePhoneId(phoneId) && SubscriptionManager.isValidSubscriptionId(subId)) { + mServiceState[phoneId] = state; - for (Record r : mRecords) { - if (VDBG) { - log("notifyServiceStateForSubscriber: r=" + r + " subId=" + subId - + " phoneId=" + phoneId + " state=" + state); - } - if (r.matchTelephonyCallbackEvent( - TelephonyCallback.EVENT_SERVICE_STATE_CHANGED) - && idMatch(r, subId, phoneId)) { + for (Record r : mRecords) { + if (VDBG) { + log("notifyServiceStateForSubscriber: r=" + r + " subId=" + subId + + " phoneId=" + phoneId + " state=" + state); + } + if (r.matchTelephonyCallbackEvent( + TelephonyCallback.EVENT_SERVICE_STATE_CHANGED) + && idMatch(r, subId, phoneId)) { - try { - ServiceState stateToSend; - if (checkFineLocationAccess(r, Build.VERSION_CODES.Q)) { - stateToSend = new ServiceState(state); - } else if (checkCoarseLocationAccess(r, Build.VERSION_CODES.Q)) { - stateToSend = state.createLocationInfoSanitizedCopy(false); - } else { - stateToSend = state.createLocationInfoSanitizedCopy(true); - } - if (DBG) { - log("notifyServiceStateForSubscriber: callback.onSSC r=" + r - + " subId=" + subId + " phoneId=" + phoneId - + " state=" + stateToSend); + try { + ServiceState stateToSend; + if (checkFineLocationAccess(r, Build.VERSION_CODES.Q)) { + stateToSend = new ServiceState(state); + } else if (checkCoarseLocationAccess(r, Build.VERSION_CODES.Q)) { + stateToSend = state.createLocationInfoSanitizedCopy(false); + } else { + stateToSend = state.createLocationInfoSanitizedCopy(true); + } + if (DBG) { + log("notifyServiceStateForSubscriber: callback.onSSC r=" + r + + " subId=" + subId + " phoneId=" + phoneId + + " state=" + stateToSend); + } + r.callback.onServiceStateChanged(stateToSend); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); } - r.callback.onServiceStateChanged(stateToSend); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); } } + } else { + log("notifyServiceStateForSubscriber: INVALID phoneId=" + phoneId + + " or subId=" + subId); } - } else { - log("notifyServiceStateForSubscriber: INVALID phoneId=" + phoneId - + " or subId=" + subId); + handleRemoveListLocked(); } - handleRemoveListLocked(); + broadcastServiceStateChanged(state, phoneId, subId); + } finally { + Binder.restoreCallingIdentity(callingIdentity); } - broadcastServiceStateChanged(state, phoneId, subId); } public void notifySimActivationStateChangedForPhoneId(int phoneId, int subId, @@ -3508,13 +3513,10 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { public static final String ACTION_SIGNAL_STRENGTH_CHANGED = "android.intent.action.SIG_STR"; private void broadcastServiceStateChanged(ServiceState state, int phoneId, int subId) { - final long ident = Binder.clearCallingIdentity(); try { mBatteryStats.notePhoneState(state.getState()); } catch (RemoteException re) { // Can't do much - } finally { - Binder.restoreCallingIdentity(ident); } // Send the broadcast exactly once to all possible disjoint sets of apps. @@ -3531,8 +3533,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { // - Sanitized ServiceState sent to all other apps with READ_PHONE_STATE // - Sanitized ServiceState sent to all other apps with READ_PRIVILEGED_PHONE_STATE but not // READ_PHONE_STATE - if (Binder.withCleanCallingIdentity(() -> - LocationAccessPolicy.isLocationModeEnabled(mContext, mContext.getUserId()))) { + if (LocationAccessPolicy.isLocationModeEnabled(mContext, mContext.getUserId())) { Intent fullIntent = createServiceStateIntent(state, subId, phoneId, false); mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions( fullIntent, diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index f90a3ce76e71..24c9e0f55ab9 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -2092,6 +2092,17 @@ public final class ActiveServices { r.appInfo.uid, r.intent.getIntent(), r, r.userId, BackgroundStartPrivileges.NONE, false /* isBindService */); + if (r.mAllowStartForeground == REASON_DENIED) { + Slog.w(TAG_SERVICE, "FGS type change to/from SHORT_SERVICE: " + + " BFSL DENIED."); + } else { + if (DEBUG_SHORT_SERVICE) { + Slog.w(TAG_SERVICE, "FGS type change to/from SHORT_SERVICE: " + + " BFSL Allowed: " + + PowerExemptionManager.reasonCodeToString( + r.mAllowStartForeground)); + } + } final boolean fgsStartAllowed = !isBgFgsRestrictionEnabledForService diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index e3f00ded026f..9e95e5fb0a40 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -995,7 +995,7 @@ final class ActivityManagerConstants extends ContentObserver { private static final String KEY_ENABLE_WAIT_FOR_FINISH_ATTACH_APPLICATION = "enable_wait_for_finish_attach_application"; - private static final boolean DEFAULT_ENABLE_WAIT_FOR_FINISH_ATTACH_APPLICATION = true; + private static final boolean DEFAULT_ENABLE_WAIT_FOR_FINISH_ATTACH_APPLICATION = false; /** @see #KEY_ENABLE_WAIT_FOR_FINISH_ATTACH_APPLICATION */ public volatile boolean mEnableWaitForFinishAttachApplication = diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 432c2093bee6..4ba6854b9234 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -101,7 +101,7 @@ import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; -import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED; +import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH; import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NEW_MUTABLE_IMPLICIT_PENDING_INTENT_RETRIEVED; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALL; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALLOWLISTS; @@ -344,6 +344,7 @@ import android.util.FeatureFlagUtils; import android.util.IndentingPrintWriter; import android.util.IntArray; import android.util.Log; +import android.util.LogWriter; import android.util.Pair; import android.util.PrintWriterPrinter; import android.util.Slog; @@ -484,9 +485,11 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; @@ -5585,8 +5588,11 @@ public class ActivityManagerService extends IActivityManager.Stub boolean isChangeEnabled = CompatChanges.isChangeEnabled( PendingIntent.BLOCK_MUTABLE_IMPLICIT_PENDING_INTENT, owningUid); - logUnsafeMutableImplicitPi(packageName, resolvedTypes, owningUid, i, intent, - isChangeEnabled); + String resolvedType = resolvedTypes == null + || i >= resolvedTypes.length ? null : resolvedTypes[i]; + ActivityManagerUtils.logUnsafeIntentEvent( + UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NEW_MUTABLE_IMPLICIT_PENDING_INTENT_RETRIEVED, + owningUid, intent, resolvedType, isChangeEnabled); if (isChangeEnabled) { String msg = packageName + ": Targeting U+ (version " + Build.VERSION_CODES.UPSIDE_DOWN_CAKE + " and above) disallows" @@ -5652,24 +5658,6 @@ public class ActivityManagerService extends IActivityManager.Stub } } - private void logUnsafeMutableImplicitPi(String packageName, String[] resolvedTypes, - int owningUid, int i, Intent intent, boolean isChangeEnabled) { - String[] categories = intent.getCategories() == null ? new String[0] - : intent.getCategories().toArray(String[]::new); - String resolvedType = resolvedTypes == null || i >= resolvedTypes.length ? null - : resolvedTypes[i]; - FrameworkStatsLog.write(UNSAFE_INTENT_EVENT_REPORTED, - UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NEW_MUTABLE_IMPLICIT_PENDING_INTENT_RETRIEVED, - owningUid, - null, - packageName, - intent.getAction(), - categories, - resolvedType, - intent.getScheme(), - isChangeEnabled); - } - @Override public int sendIntentSender(IApplicationThread caller, IIntentSender target, IBinder allowlistToken, int code, Intent intent, String resolvedType, @@ -12909,18 +12897,9 @@ public class ActivityManagerService extends IActivityManager.Stub boolean hasToBeExportedToMatch = platformCompat.isChangeEnabledByUid( ActivityManagerService.IMPLICIT_INTENTS_ONLY_MATCH_EXPORTED_COMPONENTS, callingUid); - String[] categories = intent.getCategories() == null ? new String[0] - : intent.getCategories().toArray(String[]::new); - FrameworkStatsLog.write(UNSAFE_INTENT_EVENT_REPORTED, - FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH, - callingUid, - componentInfo, - callerPackage, - intent.getAction(), - categories, - resolvedType, - intent.getScheme(), - hasToBeExportedToMatch); + ActivityManagerUtils.logUnsafeIntentEvent( + UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH, + callingUid, intent, resolvedType, hasToBeExportedToMatch); if (!hasToBeExportedToMatch) { return; } @@ -18865,10 +18844,11 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void waitForBroadcastBarrier() { - waitForBroadcastBarrier(/* printWriter= */ null, false); + waitForBroadcastBarrier(/* printWriter= */ null, false, false); } - public void waitForBroadcastBarrier(@Nullable PrintWriter pw, boolean flushBroadcastLoopers) { + public void waitForBroadcastBarrier(@Nullable PrintWriter pw, + boolean flushBroadcastLoopers, boolean flushApplicationThreads) { enforceCallingPermission(permission.DUMP, "waitForBroadcastBarrier()"); if (flushBroadcastLoopers) { BroadcastLoopers.waitForBarrier(pw); @@ -18876,6 +18856,76 @@ public class ActivityManagerService extends IActivityManager.Stub for (BroadcastQueue queue : mBroadcastQueues) { queue.waitForBarrier(pw); } + if (flushApplicationThreads) { + waitForApplicationBarrier(pw); + } + } + + /** + * Wait for all pending {@link IApplicationThread} events to be processed in + * all currently running apps. + */ + public void waitForApplicationBarrier(@Nullable PrintWriter pw) { + if (pw == null) { + pw = new PrintWriter(new LogWriter(Log.VERBOSE, TAG)); + } + + final CountDownLatch finishedLatch = new CountDownLatch(1); + final AtomicInteger pingCount = new AtomicInteger(0); + final AtomicInteger pongCount = new AtomicInteger(0); + final RemoteCallback pongCallback = new RemoteCallback((result) -> { + if (pongCount.incrementAndGet() == pingCount.get()) { + finishedLatch.countDown(); + } + }); + + // Insert an extra "ping" as a sentinel value to guard us from finishing + // too quickly in parallel below + pingCount.incrementAndGet(); + + synchronized (mProcLock) { + final ArrayMap<String, SparseArray<ProcessRecord>> pmap = + mProcessList.getProcessNamesLOSP().getMap(); + final int numProc = pmap.size(); + for (int iProc = 0; iProc < numProc; iProc++) { + final SparseArray<ProcessRecord> apps = pmap.valueAt(iProc); + for (int iApp = 0, numApps = apps.size(); iApp < numApps; iApp++) { + final ProcessRecord app = apps.valueAt(iApp); + final IApplicationThread thread = app.getOnewayThread(); + if (thread != null) { + mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(app, + OomAdjuster.OOM_ADJ_REASON_NONE); + pingCount.incrementAndGet(); + try { + thread.schedulePing(pongCallback); + } catch (RemoteException ignored) { + // When we failed to ping remote process, pretend as + // if we received the expected pong + pongCallback.sendResult(null); + } + } + } + } + } + + // Now that we've dispatched all "ping" events above, we can send our + // "pong" sentinel value + pongCallback.sendResult(null); + + // Wait for any remaining "pong" events to trickle in + for (int i = 0; i < 30; i++) { + try { + if (finishedLatch.await(1, TimeUnit.SECONDS)) { + pw.println("Finished application barriers!"); + return; + } else { + pw.println("Waiting for application barriers, at " + pongCount.get() + " of " + + pingCount.get() + "..."); + } + } catch (InterruptedException ignored) { + } + } + pw.println("Gave up waiting for application barriers!"); } void setIgnoreDeliveryGroupPolicy(@NonNull String broadcastAction) { diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 02c1b8b31300..523ed6923b26 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -359,6 +359,8 @@ final class ActivityManagerShellCommand extends ShellCommand { return runWaitForBroadcastIdle(pw); case "wait-for-broadcast-barrier": return runWaitForBroadcastBarrier(pw); + case "wait-for-application-barrier": + return runWaitForApplicationBarrier(pw); case "set-ignore-delivery-group-policy": return runSetIgnoreDeliveryGroupPolicy(pw); case "clear-ignore-delivery-group-policy": @@ -3332,16 +3334,24 @@ final class ActivityManagerShellCommand extends ShellCommand { int runWaitForBroadcastBarrier(PrintWriter pw) throws RemoteException { boolean flushBroadcastLoopers = false; + boolean flushApplicationThreads = false; String opt; while ((opt = getNextOption()) != null) { if (opt.equals("--flush-broadcast-loopers")) { flushBroadcastLoopers = true; + } else if (opt.equals("--flush-application-threads")) { + flushApplicationThreads = true; } else { getErrPrintWriter().println("Error: Unknown option: " + opt); return -1; } } - mInternal.waitForBroadcastBarrier(pw, flushBroadcastLoopers); + mInternal.waitForBroadcastBarrier(pw, flushBroadcastLoopers, flushApplicationThreads); + return 0; + } + + int runWaitForApplicationBarrier(PrintWriter pw) throws RemoteException { + mInternal.waitForApplicationBarrier(pw); return 0; } diff --git a/services/core/java/com/android/server/am/ActivityManagerUtils.java b/services/core/java/com/android/server/am/ActivityManagerUtils.java index 9be553c49a35..01466b845a61 100644 --- a/services/core/java/com/android/server/am/ActivityManagerUtils.java +++ b/services/core/java/com/android/server/am/ActivityManagerUtils.java @@ -17,11 +17,13 @@ package com.android.server.am; import android.app.ActivityThread; import android.content.ContentResolver; +import android.content.Intent; import android.provider.Settings; import android.util.ArrayMap; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -133,4 +135,25 @@ public class ActivityManagerUtils { public static int hashComponentNameForAtom(String shortInstanceName) { return getUnsignedHashUnCached(shortInstanceName) ^ getAndroidIdHash(); } + + /** + * Helper method to log an unsafe intent event. + */ + public static void logUnsafeIntentEvent(int event, int callingUid, + Intent intent, String resolvedType, boolean blocked) { + String[] categories = intent.getCategories() == null ? new String[0] + : intent.getCategories().toArray(String[]::new); + String component = intent.getComponent() == null ? null + : intent.getComponent().flattenToString(); + FrameworkStatsLog.write(FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED, + event, + callingUid, + component, + intent.getPackage(), + intent.getAction(), + categories, + resolvedType, + intent.getScheme(), + blocked); + } } diff --git a/services/core/java/com/android/server/am/AppExitInfoTracker.java b/services/core/java/com/android/server/am/AppExitInfoTracker.java index 32d20718c192..1ba326680fc2 100644 --- a/services/core/java/com/android/server/am/AppExitInfoTracker.java +++ b/services/core/java/com/android/server/am/AppExitInfoTracker.java @@ -674,7 +674,7 @@ public final class AppExitInfoTracker { break; } } - } catch (IOException | IllegalArgumentException | WireTypeMismatchException e) { + } catch (Exception e) { Slog.w(TAG, "Error in loading historical app exit info from persistent storage: " + e); } finally { if (fin != null) { diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 0ee883f745ca..f236a961fcb6 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -402,6 +402,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub public void systemServicesReady() { mStats.systemServicesReady(mContext); + mCpuWakeupStats.systemServicesReady(); mWorker.systemServicesReady(); final INetworkManagementService nms = INetworkManagementService.Stub.asInterface( ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)); diff --git a/services/core/java/com/android/server/am/BroadcastHistory.java b/services/core/java/com/android/server/am/BroadcastHistory.java index 6ac0e8bee58b..34658ca41356 100644 --- a/services/core/java/com/android/server/am/BroadcastHistory.java +++ b/services/core/java/com/android/server/am/BroadcastHistory.java @@ -17,6 +17,7 @@ package com.android.server.am; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Intent; import android.os.Bundle; import android.util.TimeUtils; @@ -26,6 +27,7 @@ import dalvik.annotation.optimization.NeverCompile; import java.io.PrintWriter; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; /** @@ -48,6 +50,11 @@ public class BroadcastHistory { } /** + * List of broadcasts which are being delivered or yet to be delivered. + */ + private final ArrayList<BroadcastRecord> mPendingBroadcasts = new ArrayList<>(); + + /** * Historical data of past broadcasts, for debugging. This is a ring buffer * whose last element is at mHistoryNext. */ @@ -70,7 +77,16 @@ public class BroadcastHistory { final long[] mSummaryHistoryDispatchTime; final long[] mSummaryHistoryFinishTime; - public void addBroadcastToHistoryLocked(BroadcastRecord original) { + void onBroadcastEnqueuedLocked(@NonNull BroadcastRecord r) { + mPendingBroadcasts.add(r); + } + + void onBroadcastFinishedLocked(@NonNull BroadcastRecord r) { + mPendingBroadcasts.remove(r); + addBroadcastToHistoryLocked(r); + } + + public void addBroadcastToHistoryLocked(@NonNull BroadcastRecord original) { // Note sometimes (only for sticky broadcasts?) we reuse BroadcastRecords, // So don't change the incoming record directly. final BroadcastRecord historyRecord = original.maybeStripForHistory(); @@ -93,7 +109,12 @@ public class BroadcastHistory { } @NeverCompile - public void dumpDebug(ProtoOutputStream proto) { + public void dumpDebug(@NonNull ProtoOutputStream proto) { + for (int i = 0; i < mPendingBroadcasts.size(); ++i) { + final BroadcastRecord r = mPendingBroadcasts.get(i); + r.dumpDebug(proto, BroadcastQueueProto.PENDING_BROADCASTS); + } + int lastIndex = mHistoryNext; int ringIndex = lastIndex; do { @@ -127,8 +148,20 @@ public class BroadcastHistory { } @NeverCompile - public boolean dumpLocked(PrintWriter pw, String dumpPackage, String queueName, - SimpleDateFormat sdf, boolean dumpAll, boolean needSep) { + public boolean dumpLocked(@NonNull PrintWriter pw, @Nullable String dumpPackage, + @NonNull String queueName, @NonNull SimpleDateFormat sdf, + boolean dumpAll, boolean needSep) { + pw.println(" Pending broadcasts:"); + if (mPendingBroadcasts.isEmpty()) { + pw.println(" <empty>"); + } else { + for (int idx = mPendingBroadcasts.size() - 1; idx >= 0; --idx) { + final BroadcastRecord r = mPendingBroadcasts.get(idx); + pw.print(" Broadcast #"); pw.print(idx); pw.println(":"); + r.dump(pw, " ", sdf); + } + } + int i; boolean printed = false; diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 84c03e59c658..32e5fd1ef3fd 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -1145,8 +1145,11 @@ class BroadcastProcessQueue { pw.print(" because "); pw.print(reasonToString(mRunnableAtReason)); pw.println(); - pw.print("mProcessCached="); pw.println(mProcessCached); + pw.increaseIndent(); + dumpProcessState(pw); + dumpBroadcastCounts(pw); + if (mActive != null) { dumpRecord("ACTIVE", now, pw, mActive, mActiveIndex); } @@ -1167,6 +1170,49 @@ class BroadcastProcessQueue { } @NeverCompile + private void dumpProcessState(@NonNull IndentingPrintWriter pw) { + final StringBuilder sb = new StringBuilder(); + if (mProcessCached) { + sb.append("CACHED"); + } + if (mProcessInstrumented) { + if (sb.length() > 0) sb.append("|"); + sb.append("INSTR"); + } + if (mProcessPersistent) { + if (sb.length() > 0) sb.append("|"); + sb.append("PER"); + } + if (sb.length() > 0) { + pw.print("state:"); pw.println(sb); + } + if (runningOomAdjusted) { + pw.print("runningOomAdjusted:"); pw.println(runningOomAdjusted); + } + } + + @NeverCompile + private void dumpBroadcastCounts(@NonNull IndentingPrintWriter pw) { + pw.print("e:"); pw.print(mCountEnqueued); + pw.print(" d:"); pw.print(mCountDeferred); + pw.print(" f:"); pw.print(mCountForeground); + pw.print(" fd:"); pw.print(mCountForegroundDeferred); + pw.print(" o:"); pw.print(mCountOrdered); + pw.print(" a:"); pw.print(mCountAlarm); + pw.print(" p:"); pw.print(mCountPrioritized); + pw.print(" pd:"); pw.print(mCountPrioritizedDeferred); + pw.print(" int:"); pw.print(mCountInteractive); + pw.print(" rt:"); pw.print(mCountResultTo); + pw.print(" ins:"); pw.print(mCountInstrumented); + pw.print(" m:"); pw.print(mCountManifest); + + pw.print(" csi:"); pw.print(mActiveCountSinceIdle); + pw.print(" ccu:"); pw.print(mActiveCountConsecutiveUrgent); + pw.print(" ccn:"); pw.print(mActiveCountConsecutiveNormal); + pw.println(); + } + + @NeverCompile private void dumpRecord(@Nullable String flavor, @UptimeMillisLong long now, @NonNull IndentingPrintWriter pw, @NonNull BroadcastRecord record, int recordIndex) { TimeUtils.formatDuration(record.enqueueTime, now, pw); diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 841b61e8e81f..81ca267b9f63 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -90,7 +90,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; @@ -602,6 +601,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { r.enqueueTime = SystemClock.uptimeMillis(); r.enqueueRealTime = SystemClock.elapsedRealtime(); r.enqueueClockTime = System.currentTimeMillis(); + mHistory.onBroadcastEnqueuedLocked(r); ArraySet<BroadcastRecord> replacedBroadcasts = mReplacedBroadcastsCache.getAndSet(null); if (replacedBroadcasts == null) { @@ -825,8 +825,9 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (app != null && app.isInFullBackup()) { return "isInFullBackup"; } - if (mSkipPolicy.shouldSkip(r, receiver)) { - return "mSkipPolicy"; + final String skipReason = mSkipPolicy.shouldSkipMessage(r, receiver); + if (skipReason != null) { + return skipReason; } final Intent receiverIntent = r.getReceiverIntent(receiver); if (receiverIntent == null) { @@ -1100,7 +1101,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { */ private void setDeliveryState(@Nullable BroadcastProcessQueue queue, @Nullable ProcessRecord app, @NonNull BroadcastRecord r, int index, - @NonNull Object receiver, @DeliveryState int newDeliveryState, String reason) { + @NonNull Object receiver, @DeliveryState int newDeliveryState, + @NonNull String reason) { final int cookie = traceBegin("setDeliveryState"); final int oldDeliveryState = getDeliveryState(r, index); boolean checkFinished = false; @@ -1108,7 +1110,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // 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); + r.setDeliveryState(index, newDeliveryState, reason); if (oldDeliveryState == BroadcastRecord.DELIVERY_DEFERRED) { r.deferredCount--; } else if (newDeliveryState == BroadcastRecord.DELIVERY_DEFERRED) { @@ -1659,7 +1661,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { mService.notifyBroadcastFinishedLocked(r); r.finishTime = SystemClock.uptimeMillis(); r.nextReceiver = r.receivers.size(); - mHistory.addBroadcastToHistoryLocked(r); + mHistory.onBroadcastFinishedLocked(r); BroadcastQueueImpl.logBootCompletedBroadcastCompletionLatencyIfPossible(r); @@ -1833,8 +1835,9 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (dumpConstants) { mConstants.dump(ipw); } + if (dumpHistory) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); needSep = mHistory.dumpLocked(ipw, dumpPackage, mQueueName, sdf, dumpAll, needSep); } return needSep; diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index f793c5078b5e..59f33ddb795d 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -99,6 +99,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 @NonNull String[] deliveryReasons; // reasons for 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 @@ -298,7 +299,7 @@ final class BroadcastRecord extends Binder { pw.print(" initialSticky="); pw.println(initialSticky); } if (nextReceiver != 0) { - pw.print(prefix); pw.print("nextReceiver="); pw.print(nextReceiver); + pw.print(prefix); pw.print("nextReceiver="); pw.println(nextReceiver); } if (curFilter != null) { pw.print(prefix); pw.print("curFilter="); pw.println(curFilter); @@ -328,6 +329,7 @@ final class BroadcastRecord extends Binder { } pw.print(prefix); pw.print("state="); pw.print(state); pw.println(stateStr); } + pw.print(prefix); pw.print("terminalCount="); pw.println(terminalCount); final int N = receivers != null ? receivers.size() : 0; String p2 = prefix + " "; PrintWriterPrinter printer = new PrintWriterPrinter(pw); @@ -346,6 +348,7 @@ final class BroadcastRecord extends Binder { TimeUtils.formatDuration(terminalTime[i] - scheduledTime[i], pw); pw.print(' '); } + pw.print("("); pw.print(blockedUntilTerminalCount[i]); pw.print(") "); pw.print("#"); pw.print(i); pw.print(": "); if (o instanceof BroadcastFilter) { pw.println(o); @@ -356,6 +359,9 @@ final class BroadcastRecord extends Binder { } else { pw.println(o); } + if (deliveryReasons[i] != null) { + pw.print(p2); pw.print("reason: "); pw.println(deliveryReasons[i]); + } } } @@ -393,6 +399,7 @@ final class BroadcastRecord extends Binder { options = _options; receivers = (_receivers != null) ? _receivers : EMPTY_RECEIVERS; delivery = new int[_receivers != null ? _receivers.size() : 0]; + deliveryReasons = new String[delivery.length]; deferUntilActive = options != null ? options.isDeferUntilActive() : false; deferredUntilActive = new boolean[deferUntilActive ? delivery.length : 0]; blockedUntilTerminalCount = calculateBlockedUntilTerminalCount(receivers, _serialized); @@ -448,6 +455,7 @@ final class BroadcastRecord extends Binder { options = from.options; receivers = from.receivers; delivery = from.delivery; + deliveryReasons = from.deliveryReasons; deferUntilActive = from.deferUntilActive; deferredUntilActive = from.deferredUntilActive; blockedUntilTerminalCount = from.blockedUntilTerminalCount; @@ -609,8 +617,10 @@ final class BroadcastRecord extends Binder { * Update the delivery state of the given {@link #receivers} index. * Automatically updates any time measurements related to state changes. */ - void setDeliveryState(int index, @DeliveryState int deliveryState) { + void setDeliveryState(int index, @DeliveryState int deliveryState, + @NonNull String reason) { delivery[index] = deliveryState; + deliveryReasons[index] = reason; if (deferUntilActive) deferredUntilActive[index] = false; switch (deliveryState) { case DELIVERY_DELIVERED: @@ -977,7 +987,8 @@ final class BroadcastRecord extends Binder { if (label == null) { label = intent.toString(); } - mCachedToShortString = label + "/u" + userId; + mCachedToShortString = Integer.toHexString(System.identityHashCode(this)) + + ":" + label + "/u" + userId; } return mCachedToShortString; } diff --git a/services/core/java/com/android/server/am/SameProcessApplicationThread.java b/services/core/java/com/android/server/am/SameProcessApplicationThread.java index 82dd5c2cf2c3..6deaf7b7fe4f 100644 --- a/services/core/java/com/android/server/am/SameProcessApplicationThread.java +++ b/services/core/java/com/android/server/am/SameProcessApplicationThread.java @@ -25,6 +25,7 @@ import android.content.pm.ActivityInfo; import android.content.res.CompatibilityInfo; import android.os.Bundle; import android.os.Handler; +import android.os.RemoteCallback; import android.os.RemoteException; import java.util.List; @@ -77,17 +78,23 @@ public class SameProcessApplicationThread extends IApplicationThread.Default { @Override public void scheduleReceiverList(List<ReceiverInfo> info) { - for (int i = 0; i < info.size(); i++) { - ReceiverInfo r = info.get(i); - if (r.registered) { - scheduleRegisteredReceiver(r.receiver, r.intent, - r.resultCode, r.data, r.extras, r.ordered, r.sticky, r.assumeDelivered, - r.sendingUser, r.processState, r.sendingUid, r.sendingPackage); - } else { - scheduleReceiver(r.intent, r.activityInfo, r.compatInfo, - r.resultCode, r.data, r.extras, r.sync, r.assumeDelivered, - r.sendingUser, r.processState, r.sendingUid, r.sendingPackage); + mHandler.post(() -> { + try { + mWrapped.scheduleReceiverList(info); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public void schedulePing(RemoteCallback pong) { + mHandler.post(() -> { + try { + mWrapped.schedulePing(pong); + } catch (RemoteException e) { + throw new RuntimeException(e); } - } + }); } } diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index ae8ceab62896..61801177ffba 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -468,6 +468,8 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN long fgToken = proto.start(ServiceRecordProto.FOREGROUND); proto.write(ServiceRecordProto.Foreground.ID, foregroundId); foregroundNoti.dumpDebug(proto, ServiceRecordProto.Foreground.NOTIFICATION); + proto.write(ServiceRecordProto.Foreground.FOREGROUND_SERVICE_TYPE, + foregroundServiceType); proto.end(fgToken); } ProtoUtils.toDuration(proto, ServiceRecordProto.CREATE_REAL_TIME, createRealTime, nowReal); diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java index 2d6966ad0cf8..23a384ff5d3b 100644 --- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java +++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java @@ -67,8 +67,8 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { private SparseIntArray mPendingUidStates = new SparseIntArray(); private SparseIntArray mCapability = new SparseIntArray(); private SparseIntArray mPendingCapability = new SparseIntArray(); - private SparseBooleanArray mVisibleAppWidget = new SparseBooleanArray(); - private SparseBooleanArray mPendingVisibleAppWidget = new SparseBooleanArray(); + private SparseBooleanArray mAppWidgetVisible = new SparseBooleanArray(); + private SparseBooleanArray mPendingAppWidgetVisible = new SparseBooleanArray(); private SparseLongArray mPendingCommitTime = new SparseLongArray(); private SparseBooleanArray mPendingGone = new SparseBooleanArray(); @@ -140,7 +140,7 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { private int evalModeInternal(int uid, int code, int uidState, int uidCapability) { - if (getUidVisibleAppWidget(uid) || mActivityManagerInternal.isPendingTopUid(uid) + if (getUidAppWidgetVisible(uid) || mActivityManagerInternal.isPendingTopUid(uid) || mActivityManagerInternal.isTempAllowlistedForFgsWhileInUse(uid)) { return MODE_ALLOWED; } @@ -205,7 +205,7 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { int numUids = uidPackageNames.size(); for (int i = 0; i < numUids; i++) { int uid = uidPackageNames.keyAt(i); - mPendingVisibleAppWidget.put(uid, visible); + mPendingAppWidgetVisible.put(uid, visible); commitUidPendingState(uid); } @@ -213,8 +213,6 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { @Override public void updateUidProcState(int uid, int procState, int capability) { - mEventLog.logUpdateUidProcState(uid, procState, capability); - int uidState = processStateToUidState(procState); int prevUidState = mUidStates.get(uid, AppOpsManager.MIN_PRIORITY_UID_STATE); @@ -226,6 +224,10 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { && (uidState != prevUidState || capability != prevCapability)) || (pendingStateCommitTime != 0 && (uidState != pendingUidState || capability != pendingCapability))) { + + // If this process update results in a capability or uid state change, log it. It's + // not interesting otherwise. + mEventLog.logUpdateUidProcState(uid, procState, capability); mPendingUidStates.put(uid, uidState); mPendingCapability.put(uid, capability); @@ -289,9 +291,9 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { ActivityManager.printCapabilitiesFull(pw, pendingCapability); pw.println(); } - boolean appWidgetVisible = mVisibleAppWidget.get(uid, false); + boolean appWidgetVisible = mAppWidgetVisible.get(uid, false); // if no pendingAppWidgetVisible set to appWidgetVisible to suppress output - boolean pendingAppWidgetVisible = mPendingVisibleAppWidget.get(uid, appWidgetVisible); + boolean pendingAppWidgetVisible = mPendingAppWidgetVisible.get(uid, appWidgetVisible); pw.print(" appWidgetVisible="); pw.println(appWidgetVisible); if (appWidgetVisible != pendingAppWidgetVisible) { @@ -331,25 +333,25 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { mUidStates.get(uid, MIN_PRIORITY_UID_STATE)); int pendingCapability = mPendingCapability.get(uid, mCapability.get(uid, PROCESS_CAPABILITY_NONE)); - boolean pendingVisibleAppWidget = mPendingVisibleAppWidget.get(uid, - mVisibleAppWidget.get(uid, false)); + boolean pendingAppWidgetVisible = mPendingAppWidgetVisible.get(uid, + mAppWidgetVisible.get(uid, false)); int uidState = mUidStates.get(uid, MIN_PRIORITY_UID_STATE); int capability = mCapability.get(uid, PROCESS_CAPABILITY_NONE); - boolean visibleAppWidget = mVisibleAppWidget.get(uid, false); + boolean appWidgetVisible = mAppWidgetVisible.get(uid, false); if (uidState != pendingUidState || capability != pendingCapability - || visibleAppWidget != pendingVisibleAppWidget) { + || appWidgetVisible != pendingAppWidgetVisible) { boolean foregroundChange = uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED != pendingUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED || capability != pendingCapability - || visibleAppWidget != pendingVisibleAppWidget; + || appWidgetVisible != pendingAppWidgetVisible; if (foregroundChange) { // To save on memory usage, log only interesting changes. mEventLog.logCommitUidState(uid, pendingUidState, pendingCapability, - pendingVisibleAppWidget); + pendingAppWidgetVisible, appWidgetVisible != pendingAppWidgetVisible); } for (int i = 0; i < mUidStateChangedCallbacks.size(); i++) { @@ -365,17 +367,17 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { if (mPendingGone.get(uid, false)) { mUidStates.delete(uid); mCapability.delete(uid); - mVisibleAppWidget.delete(uid); + mAppWidgetVisible.delete(uid); mPendingGone.delete(uid); } else { mUidStates.put(uid, pendingUidState); mCapability.put(uid, pendingCapability); - mVisibleAppWidget.put(uid, pendingVisibleAppWidget); + mAppWidgetVisible.put(uid, pendingAppWidgetVisible); } mPendingUidStates.delete(uid); mPendingCapability.delete(uid); - mPendingVisibleAppWidget.delete(uid); + mPendingAppWidgetVisible.delete(uid); mPendingCommitTime.delete(uid); } @@ -383,21 +385,22 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { return mCapability.get(uid, ActivityManager.PROCESS_CAPABILITY_NONE); } - private boolean getUidVisibleAppWidget(int uid) { - return mVisibleAppWidget.get(uid, false); + private boolean getUidAppWidgetVisible(int uid) { + return mAppWidgetVisible.get(uid, false); } private static class EventLog { - // These seems a bit too verbose and not as useful, turning off for now. - // DCE should be able to remove most associated code. // Memory usage: 16 * size bytes - private static final int UPDATE_UID_PROC_STATE_LOG_MAX_SIZE = 0; + private static final int UPDATE_UID_PROC_STATE_LOG_MAX_SIZE = 200; // Memory usage: 20 * size bytes private static final int COMMIT_UID_STATE_LOG_MAX_SIZE = 200; // Memory usage: 24 * size bytes private static final int EVAL_FOREGROUND_MODE_MAX_SIZE = 200; + private static final int APP_WIDGET_VISIBLE = 1 << 0; + private static final int APP_WIDGET_VISIBLE_CHANGED = 1 << 1; + private final DelayableExecutor mExecutor; private final Thread mExecutorThread; @@ -446,16 +449,18 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { mUpdateUidProcStateLogTimestamps[idx] = timestamp; } - void logCommitUidState(int uid, int uidState, int capability, boolean visible) { + void logCommitUidState(int uid, int uidState, int capability, boolean appWidgetVisible, + boolean appWidgetVisibleChanged) { if (COMMIT_UID_STATE_LOG_MAX_SIZE == 0) { return; } mExecutor.execute(PooledLambda.obtainRunnable(EventLog::logCommitUidStateAsync, - this, System.currentTimeMillis(), uid, uidState, capability, visible)); + this, System.currentTimeMillis(), uid, uidState, capability, appWidgetVisible, + appWidgetVisibleChanged)); } void logCommitUidStateAsync(long timestamp, int uid, int uidState, int capability, - boolean visible) { + boolean appWidgetVisible, boolean appWidgetVisibleChanged) { int idx = (mCommitUidStateLogHead + mCommitUidStateLogSize) % COMMIT_UID_STATE_LOG_MAX_SIZE; if (mCommitUidStateLogSize == COMMIT_UID_STATE_LOG_MAX_SIZE) { @@ -468,7 +473,13 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { mCommitUidStateLog[idx][0] = uid; mCommitUidStateLog[idx][1] = uidState; mCommitUidStateLog[idx][2] = capability; - mCommitUidStateLog[idx][3] = visible ? 1 : 0; + mCommitUidStateLog[idx][3] = 0; + if (appWidgetVisible) { + mCommitUidStateLog[idx][3] += APP_WIDGET_VISIBLE; + } + if (appWidgetVisibleChanged) { + mCommitUidStateLog[idx][3] += APP_WIDGET_VISIBLE_CHANGED; + } mCommitUidStateLogTimestamps[idx] = timestamp; } @@ -570,7 +581,9 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { int uid = mCommitUidStateLog[idx][0]; int uidState = mCommitUidStateLog[idx][1]; int capability = mCommitUidStateLog[idx][2]; - boolean visibleAppWidget = mCommitUidStateLog[idx][3] != 0; + boolean appWidgetVisible = (mCommitUidStateLog[idx][3] & APP_WIDGET_VISIBLE) != 0; + boolean appWidgetVisibleChanged = + (mCommitUidStateLog[idx][3] & APP_WIDGET_VISIBLE_CHANGED) != 0; TimeUtils.dumpTime(pw, timestamp); @@ -585,8 +598,12 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { pw.print(" capability="); pw.print(ActivityManager.getCapabilitiesSummary(capability) + " "); - pw.print(" visibleAppWidget="); - pw.print(visibleAppWidget); + pw.print(" appWidgetVisible="); + pw.print(appWidgetVisible); + + if (appWidgetVisibleChanged) { + pw.print(" (changed)"); + } pw.println(); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 0d0e5764b522..c50e275ddfa0 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -8588,7 +8588,9 @@ public class AudioService extends IAudioService.Stub if (isMutable()) { // For call stream, align mute only when muted, not when index is set to 0 mVolumeGroupState.mute( - forceMuteState ? mIsMuted : groupIndex == 0 || mIsMuted); + forceMuteState ? mIsMuted : + (groupIndex == 0 && !isCallStream(mStreamType)) + || mIsMuted); } } } diff --git a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java index 42be95b7377a..ecb7e7ca08fb 100644 --- a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java +++ b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java @@ -20,8 +20,13 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Intent; import android.hardware.biometrics.IBiometricContextListener; +import android.hardware.biometrics.common.AuthenticateReason; import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.common.OperationReason; +import android.hardware.biometrics.common.WakeReason; +import android.hardware.face.FaceAuthenticateOptions; +import android.hardware.fingerprint.FingerprintAuthenticateOptions; +import android.os.PowerManager; import android.view.Surface; /** @@ -50,12 +55,127 @@ public class OperationContextExt { mAidlContext = context; } - /** Gets the subset of the context that can be shared with the HAL. */ + /** + * Gets the subset of the context that can be shared with the HAL. + * + * When starting a new operation use methods like to update & fetch the context: + * <ul> + * <li>{@link #toAidlContext(FaceAuthenticateOptions)} + * <li>{@link #toAidlContext(FingerprintAuthenticateOptions)} + * </ul> + * + * Use this method for any subsequent calls to the HAL or for operations that do + * not accept any options. + * + * @return the underlying AIDL context + */ @NonNull public OperationContext toAidlContext() { return mAidlContext; } + /** + * Gets the subset of the context that can be shared with the HAL and updates + * it with the given options. + * + * @param options authenticate options + * @return the underlying AIDL context + */ + @NonNull + public OperationContext toAidlContext(@NonNull FaceAuthenticateOptions options) { + mAidlContext.authenticateReason = AuthenticateReason + .faceAuthenticateReason(getAuthReason(options)); + mAidlContext.wakeReason = getWakeReason(options); + + return mAidlContext; + } + + /** + * Gets the subset of the context that can be shared with the HAL and updates + * it with the given options. + * + * @param options authenticate options + * @return the underlying AIDL context + */ + @NonNull + public OperationContext toAidlContext(@NonNull FingerprintAuthenticateOptions options) { + mAidlContext.authenticateReason = AuthenticateReason + .fingerprintAuthenticateReason(getAuthReason(options)); + mAidlContext.wakeReason = getWakeReason(options); + + return mAidlContext; + } + + @AuthenticateReason.Face + private int getAuthReason(@NonNull FaceAuthenticateOptions options) { + switch (options.getAuthenticateReason()) { + case FaceAuthenticateOptions.AUTHENTICATE_REASON_STARTED_WAKING_UP: + return AuthenticateReason.Face.STARTED_WAKING_UP; + case FaceAuthenticateOptions.AUTHENTICATE_REASON_PRIMARY_BOUNCER_SHOWN: + return AuthenticateReason.Face.PRIMARY_BOUNCER_SHOWN; + case FaceAuthenticateOptions.AUTHENTICATE_REASON_ASSISTANT_VISIBLE: + return AuthenticateReason.Face.ASSISTANT_VISIBLE; + case FaceAuthenticateOptions.AUTHENTICATE_REASON_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN: + return AuthenticateReason.Face.ALTERNATE_BIOMETRIC_BOUNCER_SHOWN; + case FaceAuthenticateOptions.AUTHENTICATE_REASON_NOTIFICATION_PANEL_CLICKED: + return AuthenticateReason.Face.NOTIFICATION_PANEL_CLICKED; + case FaceAuthenticateOptions.AUTHENTICATE_REASON_OCCLUDING_APP_REQUESTED: + return AuthenticateReason.Face.OCCLUDING_APP_REQUESTED; + case FaceAuthenticateOptions.AUTHENTICATE_REASON_PICK_UP_GESTURE_TRIGGERED: + return AuthenticateReason.Face.PICK_UP_GESTURE_TRIGGERED; + case FaceAuthenticateOptions.AUTHENTICATE_REASON_QS_EXPANDED: + return AuthenticateReason.Face.QS_EXPANDED; + case FaceAuthenticateOptions.AUTHENTICATE_REASON_SWIPE_UP_ON_BOUNCER: + return AuthenticateReason.Face.SWIPE_UP_ON_BOUNCER; + case FaceAuthenticateOptions.AUTHENTICATE_REASON_UDFPS_POINTER_DOWN: + return AuthenticateReason.Face.UDFPS_POINTER_DOWN; + default: + return AuthenticateReason.Face.UNKNOWN; + } + } + + @WakeReason + private int getWakeReason(@NonNull FaceAuthenticateOptions options) { + switch (options.getWakeReason()) { + case PowerManager.WAKE_REASON_POWER_BUTTON: + return WakeReason.POWER_BUTTON; + case PowerManager.WAKE_REASON_GESTURE: + return WakeReason.GESTURE; + case PowerManager.WAKE_REASON_WAKE_KEY: + return WakeReason.WAKE_KEY; + case PowerManager.WAKE_REASON_WAKE_MOTION: + return WakeReason.WAKE_MOTION; + case PowerManager.WAKE_REASON_DISPLAY_GROUP_ADDED: + return WakeReason.DISPLAY_GROUP_ADDED; + case PowerManager.WAKE_REASON_TAP: + return WakeReason.TAP; + case PowerManager.WAKE_REASON_LIFT: + return WakeReason.LIFT; + case PowerManager.WAKE_REASON_BIOMETRIC: + return WakeReason.BIOMETRIC; + case PowerManager.WAKE_REASON_CAMERA_LAUNCH: + case PowerManager.WAKE_REASON_HDMI: + case PowerManager.WAKE_REASON_DISPLAY_GROUP_TURNED_ON: + case PowerManager.WAKE_REASON_UNFOLD_DEVICE: + case PowerManager.WAKE_REASON_DREAM_FINISHED: + case PowerManager.WAKE_REASON_TILT: + case PowerManager.WAKE_REASON_APPLICATION: + case PowerManager.WAKE_REASON_PLUGGED_IN: + default: + return WakeReason.UNKNOWN; + } + } + + @AuthenticateReason.Fingerprint + private int getAuthReason(@NonNull FingerprintAuthenticateOptions options) { + return AuthenticateReason.Fingerprint.UNKNOWN; + } + + @WakeReason + private int getWakeReason(@NonNull FingerprintAuthenticateOptions options) { + return WakeReason.UNKNOWN; + } + /** {@link OperationContext#id}. */ public int getId() { return mAidlContext.id; diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index 005ad20a2d48..7b9fc36e8d61 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -74,6 +74,7 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions> @Nullable private final TaskStackListener mTaskStackListener; private final LockoutTracker mLockoutTracker; + private final O mOptions; private final boolean mIsRestricted; private final boolean mAllowBackgroundAuthentication; // TODO: This is currently hard to maintain, as each AuthenticationClient subclass must update @@ -110,6 +111,7 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions> mAllowBackgroundAuthentication = allowBackgroundAuthentication; mShouldUseLockoutTracker = lockoutTracker != null; mSensorStrength = sensorStrength; + mOptions = options; } @LockoutTracker.LockoutMode @@ -151,6 +153,11 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions> return Utils.isSettings(getContext(), getOwnerString()); } + /** The options requested at the start of the operation. */ + protected O getOptions() { + return mOptions; + } + @Override protected boolean isCryptoOperation() { return mOperationId != 0; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java index 976f1cbe1e5c..84e2fb4a5966 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java @@ -166,7 +166,7 @@ class FaceAuthenticationClient extends AuthenticationClient<AidlSession, FaceAut if (session.hasContextMethods()) { return session.getSession().authenticateWithContext( - mOperationId, getOperationContext().toAidlContext()); + mOperationId, getOperationContext().toAidlContext(getOptions())); } else { return session.getSession().authenticate(mOperationId); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java index e65202dca5cd..fa23ccd482fb 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java @@ -47,6 +47,7 @@ public class FaceDetectClient extends AcquisitionClient<AidlSession> implements private static final String TAG = "FaceDetectClient"; private final boolean mIsStrongBiometric; + private final FaceAuthenticateOptions mOptions; @Nullable private ICancellationSignal mCancellationSignal; @Nullable private SensorPrivacyManager mSensorPrivacyManager; @@ -74,6 +75,7 @@ public class FaceDetectClient extends AcquisitionClient<AidlSession> implements setRequestId(requestId); mIsStrongBiometric = isStrongBiometric; mSensorPrivacyManager = sensorPrivacyManager; + mOptions = options; } @Override @@ -118,7 +120,7 @@ public class FaceDetectClient extends AcquisitionClient<AidlSession> implements if (session.hasContextMethods()) { return session.getSession().detectInteractionWithContext( - getOperationContext().toAidlContext()); + getOperationContext().toAidlContext(mOptions)); } else { return session.getSession().detectInteraction(); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index 0f81f9f2660e..435e81d688bd 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -285,7 +285,7 @@ class FingerprintAuthenticationClient if (session.hasContextMethods()) { return session.getSession().authenticateWithContext( - mOperationId, opContext.toAidlContext()); + mOperationId, opContext.toAidlContext(getOptions())); } else { return session.getSession().authenticate(mOperationId); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java index 376d23187fb8..16d16fc95c5b 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java @@ -48,6 +48,7 @@ class FingerprintDetectClient extends AcquisitionClient<AidlSession> implements private static final String TAG = "FingerprintDetectClient"; private final boolean mIsStrongBiometric; + private final FingerprintAuthenticateOptions mOptions; @NonNull private final SensorOverlays mSensorOverlays; @Nullable private ICancellationSignal mCancellationSignal; @@ -66,6 +67,7 @@ class FingerprintDetectClient extends AcquisitionClient<AidlSession> implements mIsStrongBiometric = isStrongBiometric; mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController*/, udfpsOverlay); + mOptions = options; } @Override @@ -105,7 +107,7 @@ class FingerprintDetectClient extends AcquisitionClient<AidlSession> implements if (session.hasContextMethods()) { return session.getSession().detectInteractionWithContext( - getOperationContext().toAidlContext()); + getOperationContext().toAidlContext(mOptions)); } else { return session.getSession().detectInteraction(); } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 21cc172f5908..ea157c89f675 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -128,6 +128,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.Spline; +import android.view.ContentRecordingSession; import android.view.Display; import android.view.DisplayEventReceiver; import android.view.DisplayInfo; @@ -250,6 +251,7 @@ public final class DisplayManagerService extends SystemService { private ActivityManagerInternal mActivityManagerInternal; private ActivityManager mActivityManager; private UidImportanceListener mUidImportanceListener = new UidImportanceListener(); + @Nullable private IMediaProjectionManager mProjectionService; private DeviceStateManagerInternal mDeviceStateManager; @GuardedBy("mSyncRoot") @@ -1494,8 +1496,9 @@ public final class DisplayManagerService extends SystemService { final long token = Binder.clearCallingIdentity(); try { + final int displayId; synchronized (mSyncRoot) { - final int displayId = + displayId = createVirtualDisplayLocked( callback, projection, @@ -1509,8 +1512,39 @@ public final class DisplayManagerService extends SystemService { mDisplayWindowPolicyControllers.put( displayId, Pair.create(virtualDevice, dwpc)); } - return displayId; } + + // When calling setContentRecordingSession into the WindowManagerService, the WMS + // attempts to acquire a lock before executing its main body. Due to this, we need + // to be sure that it isn't called while the DisplayManagerService is also holding + // a lock, to avoid a deadlock scenario. + final ContentRecordingSession session = + virtualDisplayConfig.getContentRecordingSession(); + + if (displayId != Display.INVALID_DISPLAY && session != null) { + // Only attempt to set content recording session if there are details to set and a + // VirtualDisplay has been successfully constructed. + session.setDisplayId(displayId); + + // We set the content recording session here on the server side instead of using + // a second AIDL call in MediaProjection. By ensuring that a virtual display has + // been constructed before calling setContentRecordingSession, we avoid a race + // condition between the DMS & WMS which could lead to the MediaProjection + // being pre-emptively torn down. + if (!mWindowManagerInternal.setContentRecordingSession(session)) { + // Unable to start mirroring, so tear down projection & release VirtualDisplay. + try { + getProjectionService().stopActiveProjection(); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to tell MediaProjectionManagerService to stop the " + + "active projection", e); + } + releaseVirtualDisplayInternal(callback.asBinder()); + return Display.INVALID_DISPLAY; + } + } + + return displayId; } finally { Binder.restoreCallingIdentity(token); } @@ -2126,16 +2160,24 @@ public final class DisplayManagerService extends SystemService { autoHdrOutputTypes = getEnabledAutoHdrTypesLocked(); } + int conversionMode = hdrConversionMode.getConversionMode(); + int preferredHdrType = hdrConversionMode.getPreferredHdrOutputType(); // If the HDR conversion is disabled by an app through WindowManager.LayoutParams, then // set HDR conversion mode to HDR_CONVERSION_PASSTHROUGH. if (mOverrideHdrConversionMode == null) { - mSystemPreferredHdrOutputType = - mInjector.setHdrConversionMode(hdrConversionMode.getConversionMode(), - hdrConversionMode.getPreferredHdrOutputType(), autoHdrOutputTypes); + // HDR_CONVERSION_FORCE with HDR_TYPE_INVALID is used to represent forcing SDR type. + // But, internally SDR is selected by using passthrough mode. + if (conversionMode == HdrConversionMode.HDR_CONVERSION_FORCE + && preferredHdrType == Display.HdrCapabilities.HDR_TYPE_INVALID) { + conversionMode = HdrConversionMode.HDR_CONVERSION_PASSTHROUGH; + } } else { - mInjector.setHdrConversionMode(mOverrideHdrConversionMode.getConversionMode(), - mOverrideHdrConversionMode.getPreferredHdrOutputType(), null); + conversionMode = mOverrideHdrConversionMode.getConversionMode(); + preferredHdrType = mOverrideHdrConversionMode.getPreferredHdrOutputType(); + autoHdrOutputTypes = null; } + mSystemPreferredHdrOutputType = mInjector.setHdrConversionMode( + conversionMode, preferredHdrType, autoHdrOutputTypes); } } @@ -2796,8 +2838,7 @@ public final class DisplayManagerService extends SystemService { private IMediaProjectionManager getProjectionService() { if (mProjectionService == null) { - IBinder b = ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE); - mProjectionService = IMediaProjectionManager.Stub.asInterface(b); + mProjectionService = mInjector.getProjectionService(); } return mProjectionService; } @@ -2956,6 +2997,11 @@ public final class DisplayManagerService extends SystemService { boolean getHdrOutputConversionSupport() { return DisplayControl.getHdrOutputConversionSupport(); } + + IMediaProjectionManager getProjectionService() { + IBinder b = ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE); + return IMediaProjectionManager.Stub.asInterface(b); + } } @VisibleForTesting diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index 364d53ba3c10..4f7a2ba58570 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -88,7 +88,17 @@ public class VirtualDisplayAdapter extends DisplayAdapter { // Called with SyncRoot lock held. public VirtualDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context, Handler handler, Listener listener) { - this(syncRoot, context, handler, listener, DisplayControl::createDisplay); + this(syncRoot, context, handler, listener, new SurfaceControlDisplayFactory() { + @Override + public IBinder createDisplay(String name, boolean secure, float requestedRefreshRate) { + return DisplayControl.createDisplay(name, secure, requestedRefreshRate); + } + + @Override + public void destroyDisplay(IBinder displayToken) { + DisplayControl.destroyDisplay(displayToken); + } + }); } @VisibleForTesting @@ -285,7 +295,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mUniqueIndex = uniqueIndex; mIsDisplayOn = surface != null; mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror(); - mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroring(); + mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroringEnabled(); } @Override @@ -311,7 +321,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mSurface.release(); mSurface = null; } - DisplayControl.destroyDisplay(getDisplayTokenLocked()); + mSurfaceControlDisplayFactory.destroyDisplay(getDisplayTokenLocked()); if (mProjection != null && mMediaProjectionCallback != null) { try { mProjection.unregisterCallback(mMediaProjectionCallback); @@ -653,5 +663,12 @@ public class VirtualDisplayAdapter extends DisplayAdapter { * @return The token reference for the display in SurfaceFlinger. */ IBinder createDisplay(String name, boolean secure, float requestedRefreshRate); + + /** + * Destroy a display in SurfaceFlinger. + * + * @param displayToken The display token for the display to be destroyed. + */ + void destroyDisplay(IBinder displayToken); } } diff --git a/services/core/java/com/android/server/dreams/DreamShellCommand.java b/services/core/java/com/android/server/dreams/DreamShellCommand.java index ab84ae4c08a2..df70a32c232c 100644 --- a/services/core/java/com/android/server/dreams/DreamShellCommand.java +++ b/services/core/java/com/android/server/dreams/DreamShellCommand.java @@ -39,26 +39,24 @@ public class DreamShellCommand extends ShellCommand { @Override public int onCommand(String cmd) { - final int callingUid = Binder.getCallingUid(); - if (callingUid != Process.ROOT_UID) { - Slog.e(TAG, "Must be root before calling Dream shell commands"); - return -1; - } - - if (TextUtils.isEmpty(cmd)) { - return super.handleDefaultCommands(cmd); - } if (DEBUG) { Slog.d(TAG, "onCommand:" + cmd); } - switch (cmd) { - case "start-dreaming": - return startDreaming(); - case "stop-dreaming": - return stopDreaming(); - default: - return super.handleDefaultCommands(cmd); + try { + switch (cmd) { + case "start-dreaming": + enforceCallerIsRoot(); + return startDreaming(); + case "stop-dreaming": + enforceCallerIsRoot(); + return stopDreaming(); + default: + return super.handleDefaultCommands(cmd); + } + } catch (SecurityException e) { + getOutPrintWriter().println(e); + return -1; } } @@ -72,6 +70,12 @@ public class DreamShellCommand extends ShellCommand { return 0; } + private void enforceCallerIsRoot() { + if (Binder.getCallingUid() != Process.ROOT_UID) { + throw new SecurityException("Must be root to call Dream shell commands"); + } + } + @Override public void onHelp() { PrintWriter pw = getOutPrintWriter(); diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index e0253fc3d30c..088740e83c42 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -150,6 +150,10 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { @Override public void onInputDeviceAdded(int deviceId) { onInputDeviceChanged(deviceId); + if (useNewSettingsUi()) { + // Force native callback to set up keyboard layout overlay for newly added keyboards + reloadKeyboardLayouts(); + } } @Override @@ -283,7 +287,8 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { public KeyboardLayout[] getKeyboardLayoutsForInputDevice( final InputDeviceIdentifier identifier) { if (useNewSettingsUi()) { - return new KeyboardLayout[0]; + // Provide all supported keyboard layouts since Ime info is not provided + return getKeyboardLayouts(); } final String[] enabledLayoutDescriptors = getEnabledKeyboardLayoutsForInputDevice(identifier); diff --git a/services/core/java/com/android/server/location/gnss/GnssManagerService.java b/services/core/java/com/android/server/location/gnss/GnssManagerService.java index 2174f4044ffd..c962bc4c20d8 100644 --- a/services/core/java/com/android/server/location/gnss/GnssManagerService.java +++ b/services/core/java/com/android/server/location/gnss/GnssManagerService.java @@ -295,6 +295,7 @@ public class GnssManagerService { } ipw.println("Capabilities: " + mGnssNative.getCapabilities()); + ipw.println("GNSS Hardware Model Name: " + getGnssHardwareModelName()); if (mGnssStatusProvider.isSupported()) { ipw.println("Status Provider:"); diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 4f28432a20a2..cc41207eaee1 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -3101,6 +3101,16 @@ public class LockSettingsService extends ILockSettings.Stub { @Override protected void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, printWriter)) return; + + final long identity = Binder.clearCallingIdentity(); + try { + dumpInternal(printWriter); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + private void dumpInternal(PrintWriter printWriter) { IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " "); pw.println("Current lock settings service state:"); diff --git a/services/core/java/com/android/server/media/AudioAttributesUtils.java b/services/core/java/com/android/server/media/AudioAttributesUtils.java new file mode 100644 index 000000000000..b9c9bae8e62b --- /dev/null +++ b/services/core/java/com/android/server/media/AudioAttributesUtils.java @@ -0,0 +1,112 @@ +/* + * 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.media; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.MediaRoute2Info; + +/* package */ final class AudioAttributesUtils { + + /* package */ static final AudioAttributes ATTRIBUTES_MEDIA = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .build(); + + private AudioAttributesUtils() { + // no-op to prevent instantiation. + } + + @MediaRoute2Info.Type + /* package */ static int mapToMediaRouteType( + @NonNull AudioDeviceAttributes audioDeviceAttributes) { + switch (audioDeviceAttributes.getType()) { + case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE: + case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER: + return MediaRoute2Info.TYPE_BUILTIN_SPEAKER; + case AudioDeviceInfo.TYPE_WIRED_HEADSET: + return MediaRoute2Info.TYPE_WIRED_HEADSET; + case AudioDeviceInfo.TYPE_WIRED_HEADPHONES: + return MediaRoute2Info.TYPE_WIRED_HEADPHONES; + case AudioDeviceInfo.TYPE_DOCK: + case AudioDeviceInfo.TYPE_DOCK_ANALOG: + return MediaRoute2Info.TYPE_DOCK; + case AudioDeviceInfo.TYPE_HDMI: + return MediaRoute2Info.TYPE_HDMI; + case AudioDeviceInfo.TYPE_USB_DEVICE: + return MediaRoute2Info.TYPE_USB_DEVICE; + case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP: + return MediaRoute2Info.TYPE_BLUETOOTH_A2DP; + case AudioDeviceInfo.TYPE_BLE_HEADSET: + return MediaRoute2Info.TYPE_BLE_HEADSET; + case AudioDeviceInfo.TYPE_HEARING_AID: + return MediaRoute2Info.TYPE_HEARING_AID; + default: + return MediaRoute2Info.TYPE_UNKNOWN; + } + } + + + /* package */ static boolean isDeviceOutputAttributes( + @Nullable AudioDeviceAttributes audioDeviceAttributes) { + if (audioDeviceAttributes == null) { + return false; + } + + if (audioDeviceAttributes.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) { + return false; + } + + switch (audioDeviceAttributes.getType()) { + case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE: + case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER: + case AudioDeviceInfo.TYPE_WIRED_HEADSET: + case AudioDeviceInfo.TYPE_WIRED_HEADPHONES: + case AudioDeviceInfo.TYPE_DOCK: + case AudioDeviceInfo.TYPE_DOCK_ANALOG: + case AudioDeviceInfo.TYPE_HDMI: + case AudioDeviceInfo.TYPE_USB_DEVICE: + return true; + default: + return false; + } + } + + /* package */ static boolean isBluetoothOutputAttributes( + @Nullable AudioDeviceAttributes audioDeviceAttributes) { + if (audioDeviceAttributes == null) { + return false; + } + + if (audioDeviceAttributes.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) { + return false; + } + + switch (audioDeviceAttributes.getType()) { + case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP: + case AudioDeviceInfo.TYPE_BLE_HEADSET: + case AudioDeviceInfo.TYPE_BLE_SPEAKER: + case AudioDeviceInfo.TYPE_HEARING_AID: + return true; + default: + return false; + } + } + +} diff --git a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java new file mode 100644 index 000000000000..182aa6fcef02 --- /dev/null +++ b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java @@ -0,0 +1,240 @@ +/* + * 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.media; + +import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; +import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO; +import static android.media.MediaRoute2Info.FEATURE_LOCAL_PLAYBACK; +import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_DOCK; +import static android.media.MediaRoute2Info.TYPE_HDMI; +import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.media.AudioManager; +import android.media.AudioRoutesInfo; +import android.media.IAudioRoutesObserver; +import android.media.IAudioService; +import android.media.MediaRoute2Info; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Objects; + + +/* package */ final class AudioPoliciesDeviceRouteController implements DeviceRouteController { + + private static final String TAG = "APDeviceRoutesController"; + + private static final String DEVICE_ROUTE_ID = "DEVICE_ROUTE"; + + @NonNull + private final Context mContext; + @NonNull + private final AudioManager mAudioManager; + @NonNull + private final IAudioService mAudioService; + + @NonNull + private final OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; + @NonNull + private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver(); + + private int mDeviceVolume; + + @NonNull + private MediaRoute2Info mDeviceRoute; + @Nullable + private MediaRoute2Info mSelectedRoute; + + @VisibleForTesting + /* package */ AudioPoliciesDeviceRouteController(@NonNull Context context, + @NonNull AudioManager audioManager, + @NonNull IAudioService audioService, + @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) { + Objects.requireNonNull(context); + Objects.requireNonNull(audioManager); + Objects.requireNonNull(audioService); + Objects.requireNonNull(onDeviceRouteChangedListener); + + mContext = context; + mOnDeviceRouteChangedListener = onDeviceRouteChangedListener; + + mAudioManager = audioManager; + mAudioService = audioService; + + AudioRoutesInfo newAudioRoutes = null; + try { + newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver); + } catch (RemoteException e) { + Slog.w(TAG, "Cannot connect to audio service to start listen to routes", e); + } + + mDeviceRoute = createRouteFromAudioInfo(newAudioRoutes); + } + + @Override + public synchronized boolean selectRoute(@Nullable Integer type) { + if (type == null) { + mSelectedRoute = null; + return true; + } + + if (!isDeviceRouteType(type)) { + return false; + } + + mSelectedRoute = createRouteFromAudioInfo(type); + return true; + } + + @Override + @NonNull + public synchronized MediaRoute2Info getDeviceRoute() { + if (mSelectedRoute != null) { + return mSelectedRoute; + } + return mDeviceRoute; + } + + @Override + public synchronized boolean updateVolume(int volume) { + if (mDeviceVolume == volume) { + return false; + } + + mDeviceVolume = volume; + + if (mSelectedRoute != null) { + mSelectedRoute = new MediaRoute2Info.Builder(mSelectedRoute) + .setVolume(volume) + .build(); + } + + mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute) + .setVolume(volume) + .build(); + + return true; + } + + @NonNull + private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) { + int type = TYPE_BUILTIN_SPEAKER; + + if (newRoutes != null) { + if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) { + type = TYPE_WIRED_HEADPHONES; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { + type = TYPE_WIRED_HEADSET; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { + type = TYPE_DOCK; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) { + type = TYPE_HDMI; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) { + type = TYPE_USB_DEVICE; + } + } + + return createRouteFromAudioInfo(type); + } + + @NonNull + private MediaRoute2Info createRouteFromAudioInfo(@MediaRoute2Info.Type int type) { + int name = R.string.default_audio_route_name; + + switch (type) { + case TYPE_WIRED_HEADPHONES: + case TYPE_WIRED_HEADSET: + name = R.string.default_audio_route_name_headphones; + break; + case TYPE_DOCK: + name = R.string.default_audio_route_name_dock_speakers; + break; + case TYPE_HDMI: + name = R.string.default_audio_route_name_external_device; + break; + case TYPE_USB_DEVICE: + name = R.string.default_audio_route_name_usb; + break; + } + + synchronized (this) { + return new MediaRoute2Info.Builder( + DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) + .setVolumeHandling(mAudioManager.isVolumeFixed() + ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED + : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) + .setVolume(mDeviceVolume) + .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) + .setType(type) + .addFeature(FEATURE_LIVE_AUDIO) + .addFeature(FEATURE_LIVE_VIDEO) + .addFeature(FEATURE_LOCAL_PLAYBACK) + .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED) + .build(); + } + } + + /** + * Checks if the given type is a device route. + * + * <p>Device route means a route which is either built-in or wired to the current device. + * + * @param type specifies the type of the device. + * @return {@code true} if the device is wired or built-in and {@code false} otherwise. + */ + private boolean isDeviceRouteType(@MediaRoute2Info.Type int type) { + switch (type) { + case TYPE_BUILTIN_SPEAKER: + case TYPE_WIRED_HEADPHONES: + case TYPE_WIRED_HEADSET: + case TYPE_DOCK: + case TYPE_HDMI: + case TYPE_USB_DEVICE: + return true; + default: + return false; + } + } + + private class AudioRoutesObserver extends IAudioRoutesObserver.Stub { + + @Override + public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) { + boolean isDeviceRouteChanged; + MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes); + + synchronized (AudioPoliciesDeviceRouteController.this) { + mDeviceRoute = deviceRoute; + isDeviceRouteChanged = mSelectedRoute == null; + } + + if (isDeviceRouteChanged) { + mOnDeviceRouteChangedListener.onDeviceRouteChanged(deviceRoute); + } + } + } + +} diff --git a/services/core/java/com/android/server/media/BluetoothRouteController.java b/services/core/java/com/android/server/media/BluetoothRouteController.java index d4a118458952..66985e0b2533 100644 --- a/services/core/java/com/android/server/media/BluetoothRouteController.java +++ b/services/core/java/com/android/server/media/BluetoothRouteController.java @@ -53,7 +53,16 @@ import java.util.Objects; return new NoOpBluetoothRouteController(); } - return new LegacyBluetoothRouteController(context, btAdapter, listener); + MediaFeatureFlagManager flagManager = MediaFeatureFlagManager.getInstance(); + boolean isUsingLegacyController = flagManager.getBoolean( + MediaFeatureFlagManager.FEATURE_AUDIO_STRATEGIES_IS_USING_LEGACY_CONTROLLER, + true); + + if (isUsingLegacyController) { + return new LegacyBluetoothRouteController(context, btAdapter, listener); + } else { + return new AudioPoliciesBluetoothRouteController(context, btAdapter, listener); + } } /** diff --git a/services/core/java/com/android/server/media/DeviceRouteController.java b/services/core/java/com/android/server/media/DeviceRouteController.java index 8bd6416a6ddb..3875c84e618b 100644 --- a/services/core/java/com/android/server/media/DeviceRouteController.java +++ b/services/core/java/com/android/server/media/DeviceRouteController.java @@ -16,32 +16,14 @@ package com.android.server.media; -import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; -import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO; -import static android.media.MediaRoute2Info.FEATURE_LOCAL_PLAYBACK; -import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; -import static android.media.MediaRoute2Info.TYPE_DOCK; -import static android.media.MediaRoute2Info.TYPE_HDMI; -import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; -import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; -import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; - import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.media.AudioManager; -import android.media.AudioRoutesInfo; import android.media.IAudioRoutesObserver; import android.media.IAudioService; import android.media.MediaRoute2Info; -import android.os.RemoteException; import android.os.ServiceManager; -import android.util.Slog; - -import com.android.internal.R; -import com.android.internal.annotations.VisibleForTesting; - -import java.util.Objects; /** * Controls device routes. @@ -49,145 +31,77 @@ import java.util.Objects; * <p>A device route is a system wired route, for example, built-in speaker, wired * headsets and headphones, dock, hdmi, or usb devices. * - * <p>Thread safe. - * * @see SystemMediaRoute2Provider */ -/* package */ final class DeviceRouteController { - - private static final String TAG = "WiredRoutesController"; - - private static final String DEVICE_ROUTE_ID = "DEVICE_ROUTE"; - - @NonNull - private final Context mContext; - @NonNull - private final AudioManager mAudioManager; - @NonNull - private final IAudioService mAudioService; - - @NonNull - private final OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; - @NonNull - private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver(); - - private int mDeviceVolume; - private MediaRoute2Info mDeviceRoute; +/* package */ interface DeviceRouteController { + /** + * Returns a new instance of {@link DeviceRouteController}. + */ /* package */ static DeviceRouteController createInstance(@NonNull Context context, @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) { AudioManager audioManager = context.getSystemService(AudioManager.class); IAudioService audioService = IAudioService.Stub.asInterface( ServiceManager.getService(Context.AUDIO_SERVICE)); - return new DeviceRouteController(context, - audioManager, - audioService, - onDeviceRouteChangedListener); - } - - @VisibleForTesting - /* package */ DeviceRouteController(@NonNull Context context, - @NonNull AudioManager audioManager, - @NonNull IAudioService audioService, - @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) { - Objects.requireNonNull(context); - Objects.requireNonNull(audioManager); - Objects.requireNonNull(audioService); - Objects.requireNonNull(onDeviceRouteChangedListener); - - mContext = context; - mOnDeviceRouteChangedListener = onDeviceRouteChangedListener; - - mAudioManager = audioManager; - mAudioService = audioService; - - AudioRoutesInfo newAudioRoutes = null; - try { - newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver); - } catch (RemoteException e) { - Slog.w(TAG, "Cannot connect to audio service to start listen to routes", e); + MediaFeatureFlagManager flagManager = MediaFeatureFlagManager.getInstance(); + boolean isUsingLegacyController = flagManager.getBoolean( + MediaFeatureFlagManager.FEATURE_AUDIO_STRATEGIES_IS_USING_LEGACY_CONTROLLER, + true); + + if (isUsingLegacyController) { + return new LegacyDeviceRouteController(context, + audioManager, + audioService, + onDeviceRouteChangedListener); + } else { + return new AudioPoliciesDeviceRouteController(context, + audioManager, + audioService, + onDeviceRouteChangedListener); } - - mDeviceRoute = createRouteFromAudioInfo(newAudioRoutes); } + /** + * Select the route with the given built-in or wired {@link MediaRoute2Info.Type}. + * + * <p>If the type is {@code null} then unselects the route and falls back to the default device + * route observed from + * {@link com.android.server.audio.AudioService#startWatchingRoutes(IAudioRoutesObserver)}. + * + * @param type device type. May be {@code null} to unselect currently selected route. + * @return whether the selection succeeds. If the selection fails the state of the controller + * remains intact. + */ + boolean selectRoute(@Nullable @MediaRoute2Info.Type Integer type); + + /** + * Returns currently selected device (built-in or wired) route. + * + * @return non-null device route. + */ @NonNull - /* package */ synchronized MediaRoute2Info getDeviceRoute() { - return mDeviceRoute; - } - - /* package */ synchronized boolean updateVolume(int volume) { - if (mDeviceVolume == volume) { - return false; - } - - mDeviceVolume = volume; - mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute) - .setVolume(volume) - .build(); - - return true; - } - - private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) { - int name = R.string.default_audio_route_name; - int type = TYPE_BUILTIN_SPEAKER; - - if (newRoutes != null) { - if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) { - type = TYPE_WIRED_HEADPHONES; - name = com.android.internal.R.string.default_audio_route_name_headphones; - } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { - type = TYPE_WIRED_HEADSET; - name = com.android.internal.R.string.default_audio_route_name_headphones; - } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { - type = TYPE_DOCK; - name = com.android.internal.R.string.default_audio_route_name_dock_speakers; - } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) { - type = TYPE_HDMI; - name = com.android.internal.R.string.default_audio_route_name_external_device; - } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) { - type = TYPE_USB_DEVICE; - name = com.android.internal.R.string.default_audio_route_name_usb; - } - } - - synchronized (this) { - return new MediaRoute2Info.Builder( - DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) - .setVolumeHandling(mAudioManager.isVolumeFixed() - ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED - : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) - .setVolume(mDeviceVolume) - .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) - .setType(type) - .addFeature(FEATURE_LIVE_AUDIO) - .addFeature(FEATURE_LIVE_VIDEO) - .addFeature(FEATURE_LOCAL_PLAYBACK) - .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED) - .build(); - } - } - - private void notifyDeviceRouteUpdate(@NonNull MediaRoute2Info deviceRoute) { - mOnDeviceRouteChangedListener.onDeviceRouteChanged(deviceRoute); - } - - /* package */ interface OnDeviceRouteChangedListener { + MediaRoute2Info getDeviceRoute(); + + /** + * Updates device route volume. + * + * @param volume specifies a volume for the device route or 0 for unknown. + * @return {@code true} if updated successfully and {@code false} otherwise. + */ + boolean updateVolume(int volume); + + /** + * Interface for receiving events when device route has changed. + */ + interface OnDeviceRouteChangedListener { + + /** + * Called when device route has changed. + * + * @param deviceRoute non-null device route. + */ void onDeviceRouteChanged(@NonNull MediaRoute2Info deviceRoute); } - private class AudioRoutesObserver extends IAudioRoutesObserver.Stub { - - @Override - public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) { - MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes); - synchronized (DeviceRouteController.this) { - mDeviceRoute = deviceRoute; - } - notifyDeviceRouteUpdate(deviceRoute); - } - } - } diff --git a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java new file mode 100644 index 000000000000..971d11f24b9c --- /dev/null +++ b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java @@ -0,0 +1,184 @@ +/* + * 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.media; + +import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; +import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO; +import static android.media.MediaRoute2Info.FEATURE_LOCAL_PLAYBACK; +import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_DOCK; +import static android.media.MediaRoute2Info.TYPE_HDMI; +import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.media.AudioManager; +import android.media.AudioRoutesInfo; +import android.media.IAudioRoutesObserver; +import android.media.IAudioService; +import android.media.MediaRoute2Info; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Objects; + +/** + * Controls device routes. + * + * <p>A device route is a system wired route, for example, built-in speaker, wired + * headsets and headphones, dock, hdmi, or usb devices. + * + * <p>Thread safe. + * + * @see SystemMediaRoute2Provider + */ +/* package */ final class LegacyDeviceRouteController implements DeviceRouteController { + + private static final String TAG = "LDeviceRouteController"; + + private static final String DEVICE_ROUTE_ID = "DEVICE_ROUTE"; + + @NonNull + private final Context mContext; + @NonNull + private final AudioManager mAudioManager; + @NonNull + private final IAudioService mAudioService; + + @NonNull + private final OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; + @NonNull + private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver(); + + private int mDeviceVolume; + private MediaRoute2Info mDeviceRoute; + + @VisibleForTesting + /* package */ LegacyDeviceRouteController(@NonNull Context context, + @NonNull AudioManager audioManager, + @NonNull IAudioService audioService, + @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) { + Objects.requireNonNull(context); + Objects.requireNonNull(audioManager); + Objects.requireNonNull(audioService); + Objects.requireNonNull(onDeviceRouteChangedListener); + + mContext = context; + mOnDeviceRouteChangedListener = onDeviceRouteChangedListener; + + mAudioManager = audioManager; + mAudioService = audioService; + + AudioRoutesInfo newAudioRoutes = null; + try { + newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver); + } catch (RemoteException e) { + Slog.w(TAG, "Cannot connect to audio service to start listen to routes", e); + } + + mDeviceRoute = createRouteFromAudioInfo(newAudioRoutes); + } + + @Override + public boolean selectRoute(@Nullable Integer type) { + // No-op as the controller does not support selection from the outside of the class. + return false; + } + + @Override + @NonNull + public synchronized MediaRoute2Info getDeviceRoute() { + return mDeviceRoute; + } + + @Override + public synchronized boolean updateVolume(int volume) { + if (mDeviceVolume == volume) { + return false; + } + + mDeviceVolume = volume; + mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute) + .setVolume(volume) + .build(); + + return true; + } + + private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) { + int name = R.string.default_audio_route_name; + int type = TYPE_BUILTIN_SPEAKER; + + if (newRoutes != null) { + if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) { + type = TYPE_WIRED_HEADPHONES; + name = com.android.internal.R.string.default_audio_route_name_headphones; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { + type = TYPE_WIRED_HEADSET; + name = com.android.internal.R.string.default_audio_route_name_headphones; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { + type = TYPE_DOCK; + name = com.android.internal.R.string.default_audio_route_name_dock_speakers; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) { + type = TYPE_HDMI; + name = com.android.internal.R.string.default_audio_route_name_external_device; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) { + type = TYPE_USB_DEVICE; + name = com.android.internal.R.string.default_audio_route_name_usb; + } + } + + synchronized (this) { + return new MediaRoute2Info.Builder( + DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) + .setVolumeHandling(mAudioManager.isVolumeFixed() + ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED + : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) + .setVolume(mDeviceVolume) + .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) + .setType(type) + .addFeature(FEATURE_LIVE_AUDIO) + .addFeature(FEATURE_LIVE_VIDEO) + .addFeature(FEATURE_LOCAL_PLAYBACK) + .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED) + .build(); + } + } + + private void notifyDeviceRouteUpdate(@NonNull MediaRoute2Info deviceRoute) { + mOnDeviceRouteChangedListener.onDeviceRouteChanged(deviceRoute); + } + + private class AudioRoutesObserver extends IAudioRoutesObserver.Stub { + + @Override + public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) { + MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes); + synchronized (LegacyDeviceRouteController.this) { + mDeviceRoute = deviceRoute; + } + notifyDeviceRouteUpdate(deviceRoute); + } + } + +} diff --git a/services/core/java/com/android/server/media/MediaFeatureFlagManager.java b/services/core/java/com/android/server/media/MediaFeatureFlagManager.java index 723cda056694..70ee38f8f7b0 100644 --- a/services/core/java/com/android/server/media/MediaFeatureFlagManager.java +++ b/services/core/java/com/android/server/media/MediaFeatureFlagManager.java @@ -32,7 +32,7 @@ import java.lang.annotation.Target; private static final String NAMESPACE_MEDIA_BETTER_TOGETHER = "media_better_together"; @StringDef(prefix = "FEATURE_", value = { - FEATURE_IS_USING_LEGACY_BLUETOOTH_CONTROLLER + FEATURE_AUDIO_STRATEGIES_IS_USING_LEGACY_CONTROLLER }) @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) @Retention(RetentionPolicy.SOURCE) @@ -43,7 +43,7 @@ import java.lang.annotation.Target; * 'Audio Strategies'-aware controller. */ /* package */ static final @MediaFeatureFlag String - FEATURE_IS_USING_LEGACY_BLUETOOTH_CONTROLLER = + FEATURE_AUDIO_STRATEGIES_IS_USING_LEGACY_CONTROLLER = "BluetoothRouteController__enable_legacy_bluetooth_routes_controller"; private static final MediaFeatureFlagManager sInstance = new MediaFeatureFlagManager(); @@ -52,7 +52,7 @@ import java.lang.annotation.Target; // Empty to prevent instantiation. } - /* package */ MediaFeatureFlagManager getInstance() { + /* package */ static MediaFeatureFlagManager getInstance() { return sInstance; } diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 6619e6c3d26f..5d5c621eb3f5 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -16,11 +16,14 @@ package com.android.server.media; +import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; import android.media.AudioManager; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; @@ -37,6 +40,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import java.util.List; import java.util.Objects; /** @@ -71,6 +75,26 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { private final AudioManagerBroadcastReceiver mAudioReceiver = new AudioManagerBroadcastReceiver(); + private final AudioManager.OnDevicesForAttributesChangedListener + mOnDevicesForAttributesChangedListener = + new AudioManager.OnDevicesForAttributesChangedListener() { + @Override + public void onDevicesForAttributesChanged(@NonNull AudioAttributes attributes, + @NonNull List<AudioDeviceAttributes> devices) { + if (attributes.getUsage() != AudioAttributes.USAGE_MEDIA) { + return; + } + + mHandler.post(() -> { + updateSelectedAudioDevice(devices); + notifyProviderState(); + if (updateSessionInfosIfNeeded()) { + notifySessionInfoUpdated(); + } + }); + } + }; + private final Object mRequestLock = new Object(); @GuardedBy("mRequestLock") private volatile SessionCreationRequest mPendingSessionCreationRequest; @@ -100,8 +124,15 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { }); }); + mAudioManager.addOnDevicesForAttributesChangedListener( + AudioAttributesUtils.ATTRIBUTES_MEDIA, mContext.getMainExecutor(), + mOnDevicesForAttributesChangedListener); + // These methods below should be called after all fields are initialized, as they // access the fields inside. + List<AudioDeviceAttributes> devices = + mAudioManager.getDevicesForAttributes(AudioAttributesUtils.ATTRIBUTES_MEDIA); + updateSelectedAudioDevice(devices); updateProviderState(); updateSessionInfosIfNeeded(); } @@ -239,6 +270,26 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } } + private void updateSelectedAudioDevice(@NonNull List<AudioDeviceAttributes> devices) { + if (devices.isEmpty()) { + Slog.w(TAG, "The list of preferred devices was empty."); + return; + } + + AudioDeviceAttributes audioDeviceAttributes = devices.get(0); + + if (AudioAttributesUtils.isDeviceOutputAttributes(audioDeviceAttributes)) { + mDeviceRouteController.selectRoute( + AudioAttributesUtils.mapToMediaRouteType(audioDeviceAttributes)); + mBluetoothRouteController.selectRoute(null); + } else if (AudioAttributesUtils.isBluetoothOutputAttributes(audioDeviceAttributes)) { + mDeviceRouteController.selectRoute(null); + mBluetoothRouteController.selectRoute(audioDeviceAttributes.getAddress()); + } else { + Slog.w(TAG, "Unknown audio attributes: " + audioDeviceAttributes); + } + } + private void updateProviderState() { MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder(); diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index 53e841d50b33..73440b7f2eec 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -892,6 +892,7 @@ abstract public class ManagedServices { return allowedComponents; } + @NonNull protected List<String> getAllowedPackages(int userId) { final List<String> allowedPackages = new ArrayList<>(); synchronized (mApproved) { @@ -1181,25 +1182,6 @@ abstract public class ManagedServices { return installed; } - protected Set<String> getAllowedPackages() { - final Set<String> allowedPackages = new ArraySet<>(); - synchronized (mApproved) { - for (int k = 0; k < mApproved.size(); k++) { - ArrayMap<Boolean, ArraySet<String>> allowedByType = mApproved.valueAt(k); - for (int i = 0; i < allowedByType.size(); i++) { - final ArraySet<String> allowed = allowedByType.valueAt(i); - for (int j = 0; j < allowed.size(); j++) { - String pkgName = getPackageName(allowed.valueAt(j)); - if (!TextUtils.isEmpty(pkgName)) { - allowedPackages.add(pkgName); - } - } - } - } - } - return allowedPackages; - } - private void trimApprovedListsAccordingToInstalledServices(int userId) { synchronized (mApproved) { final ArrayMap<Boolean, ArraySet<String>> approvedByType = mApproved.get(userId); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 0d394570ab8e..53b03d58beae 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2709,16 +2709,18 @@ public class NotificationManagerService extends SystemService { } private void sendRegisteredOnlyBroadcast(String action) { - Intent intent = new Intent(action); - getContext().sendBroadcastAsUser(intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY), - UserHandle.ALL, null); + int[] userIds = mUmInternal.getProfileIds(mAmi.getCurrentUserId(), true); + Intent intent = new Intent(action).addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + for (int userId : userIds) { + getContext().sendBroadcastAsUser(intent, UserHandle.of(userId), null); + } // explicitly send the broadcast to all DND packages, even if they aren't currently running - intent.setFlags(0); - final Set<String> dndApprovedPackages = mConditionProviders.getAllowedPackages(); - for (String pkg : dndApprovedPackages) { - intent.setPackage(pkg); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - getContext().sendBroadcastAsUser(intent, UserHandle.ALL); + for (int userId : userIds) { + for (String pkg : mConditionProviders.getAllowedPackages(userId)) { + Intent pkgIntent = new Intent(action).setPackage(pkg).setFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + getContext().sendBroadcastAsUser(pkgIntent, UserHandle.of(userId)); + } } } diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java index 9329f063aee5..0d417e457509 100644 --- a/services/core/java/com/android/server/pm/Installer.java +++ b/services/core/java/com/android/server/pm/Installer.java @@ -136,9 +136,7 @@ public class Installer extends SystemService { } /** - * @param isolated indicates if this object should <em>not</em> connect to - * the real {@code installd}. All remote calls will be ignored - * unless you extend this class and intercept them. + * @param isolated Make the installer isolated. See {@link isIsolated}. */ public Installer(Context context, boolean isolated) { super(context); @@ -153,6 +151,15 @@ public class Installer extends SystemService { mWarnIfHeld = warnIfHeld; } + /** + * Returns true if the installer is isolated, i.e. if this object should <em>not</em> connect to + * the real {@code installd}. All remote calls will be ignored unless you extend this class and + * intercept them. + */ + public boolean isIsolated() { + return mIsolated; + } + @Override public void onStart() { if (mIsolated) { diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 9e01c7af0f0b..84bee50b77b0 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -39,6 +39,7 @@ import android.app.ActivityOptions; import android.app.AppGlobals; import android.app.IApplicationThread; import android.app.PendingIntent; +import android.app.admin.DevicePolicyCache; import android.app.admin.DevicePolicyManager; import android.app.usage.UsageStatsManagerInternal; import android.content.ActivityNotFoundException; @@ -85,6 +86,7 @@ import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.util.ArrayMap; import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -107,6 +109,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutionException; @@ -623,7 +626,7 @@ public class LauncherAppsService extends SystemService { // package does not exist; should not happen return null; } - return new LauncherActivityInfoInternal(activityInfo, incrementalStatesInfo); + return new LauncherActivityInfoInternal(activityInfo, incrementalStatesInfo, user); } finally { Binder.restoreCallingIdentity(ident); } @@ -676,7 +679,7 @@ public class LauncherAppsService extends SystemService { continue; } results.add(new LauncherActivityInfoInternal(ri.activityInfo, - incrementalStatesInfo)); + incrementalStatesInfo, user)); } return results; } @@ -1078,6 +1081,55 @@ public class LauncherAppsService extends SystemService { } @Override + @NonNull + public Map<String, LauncherActivityInfoInternal> getActivityOverrides(String callingPackage, + int userId) { + ensureShortcutPermission(callingPackage); + int callingUid = Binder.getCallingUid(); + final long callerIdentity = Binder.clearCallingIdentity(); + try { + Map<String, LauncherActivityInfoInternal> shortcutOverridesInfo = new ArrayMap<>(); + UserHandle managedUserHandle = getManagedProfile(userId); + if (managedUserHandle == null) { + return shortcutOverridesInfo; + } + + List<String> packagesToOverride = + DevicePolicyCache.getInstance().getLauncherShortcutOverrides(); + for (String packageName : packagesToOverride) { + Intent intent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setPackage(packageName); + + List<LauncherActivityInfoInternal> possibleShortcutOverrides = + queryIntentLauncherActivities( + intent, + callingUid, + managedUserHandle + ); + + if (!possibleShortcutOverrides.isEmpty()) { + shortcutOverridesInfo.put(packageName, possibleShortcutOverrides.get(0)); + } + } + return shortcutOverridesInfo; + } finally { + Binder.restoreCallingIdentity(callerIdentity); + } + } + + + @Nullable + private UserHandle getManagedProfile(int userId) { + for (UserInfo profile : mUm.getProfiles(userId)) { + if (profile.isManagedProfile()) { + return profile.getUserHandle(); + } + } + return null; + } + + @Override public boolean startShortcut(String callingPackage, String packageName, String featureId, String shortcutId, Rect sourceBounds, Bundle startActivityOptions, int targetUserId) { diff --git a/services/core/java/com/android/server/pm/OtaDexoptService.java b/services/core/java/com/android/server/pm/OtaDexoptService.java index 767c0a73bc54..6a2ddc8f94b0 100644 --- a/services/core/java/com/android/server/pm/OtaDexoptService.java +++ b/services/core/java/com/android/server/pm/OtaDexoptService.java @@ -16,6 +16,7 @@ package com.android.server.pm; +import static com.android.server.pm.DexOptHelper.useArtService; import static com.android.server.pm.InstructionSets.getAppDexInstructionSets; import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; @@ -301,6 +302,15 @@ public class OtaDexoptService extends IOtaDexopt.Stub { throws InstallerException { final StringBuilder builder = new StringBuilder(); + if (useArtService()) { + if ((dexFlags & DEXOPT_SECONDARY_DEX) != 0) { + // installd may change the reference profile in place for secondary dex + // files, which isn't safe with the lock free approach in ART Service. + throw new IllegalArgumentException( + "Invalid OTA dexopt call for secondary dex"); + } + } + // The current version. For v10, see b/115993344. builder.append("10 "); @@ -353,7 +363,6 @@ public class OtaDexoptService extends IOtaDexopt.Stub { PackageDexOptimizer optimizer = new OTADexoptPackageDexOptimizer( collectingInstaller, mPackageManagerService.mInstallLock, mContext); - // TODO(b/251903639): Allow this use of legacy dexopt code even when ART Service is enabled. try { optimizer.performDexOpt(pkg, pkgSetting, null /* ISAs */, null /* CompilerStats.PackageStats */, @@ -362,9 +371,19 @@ public class OtaDexoptService extends IOtaDexopt.Stub { new DexoptOptions(pkg.getPackageName(), compilationReason, DexoptOptions.DEXOPT_BOOT_COMPLETE)); } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); + // OTA is still allowed to use the legacy dexopt code even when ART Service is enabled. + // The installer is isolated and won't call into installd, and the dexopt() method is + // overridden to only collect the command above. Hence we shouldn't go into any code + // path where this exception is thrown. + Slog.wtf(TAG, e); } + // ART Service compat note: These commands are consumed by the otapreopt binary, which uses + // the same legacy dexopt code as installd to invoke dex2oat. It provides output path + // implementations (see calculate_odex_file_path and create_cache_path in + // frameworks/native/cmds/installd/otapreopt.cpp) to write to different odex files than + // those used by ART Service in its ordinary operations, so it doesn't interfere with ART + // Service even when dalvik.vm.useartservice is true. return commands; } diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java index 0a90e7a30db6..8a4080ff029d 100644 --- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java +++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java @@ -18,6 +18,7 @@ package com.android.server.pm; import static android.content.pm.ApplicationInfo.HIDDEN_API_ENFORCEMENT_DISABLED; +import static com.android.server.pm.DexOptHelper.useArtService; import static com.android.server.pm.Installer.DEXOPT_BOOTCOMPLETE; import static com.android.server.pm.Installer.DEXOPT_DEBUGGABLE; import static com.android.server.pm.Installer.DEXOPT_ENABLE_HIDDEN_API_CHECKS; @@ -329,8 +330,22 @@ public class PackageDexOptimizer { String profileName = ArtManager.getProfileName( i == 0 ? null : pkg.getSplitNames()[i - 1]); - final boolean isUsedByOtherApps = options.isDexoptAsSharedLibrary() - || packageUseInfo.isUsedByOtherApps(path); + + final boolean isUsedByOtherApps; + if (options.isDexoptAsSharedLibrary()) { + isUsedByOtherApps = true; + } else if (useArtService()) { + // We get here when collecting dexopt commands in OTA preopt, even when ART Service + // is in use. packageUseInfo isn't useful in that case since the legacy dex use + // database hasn't been updated. So we'd have to query ART Service instead, but it + // doesn't provide that API. Just cop-out and bypass the cloud profile handling. + // That means such apps will get preopted wrong, and we'll leave it to a later + // background dexopt after reboot instead. + isUsedByOtherApps = false; + } else { + isUsedByOtherApps = packageUseInfo.isUsedByOtherApps(path); + } + String compilerFilter = getRealCompilerFilter(pkg, options.getCompilerFilter()); // If the app is used by other apps, we must not use the existing profile because it // may contain user data, unless the profile is newly created on install. @@ -446,6 +461,14 @@ public class PackageDexOptimizer { private boolean prepareCloudProfile(AndroidPackage pkg, String profileName, String path, @Nullable String dexMetadataPath) throws LegacyDexoptDisabledException { if (dexMetadataPath != null) { + if (mInstaller.isIsolated()) { + // If the installer is isolated, the two calls to it below will return immediately, + // so this only short-circuits that a bit. We need to do it to avoid the + // LegacyDexoptDisabledException getting thrown first, when we get here during OTA + // preopt and ART Service is enabled. + return true; + } + try { // Make sure we don't keep any existing contents. mInstaller.deleteReferenceProfile(pkg.getPackageName(), profileName); @@ -879,7 +902,12 @@ public class PackageDexOptimizer { private int getDexoptNeeded(String packageName, String path, String isa, String compilerFilter, String classLoaderContext, int profileAnalysisResult, boolean downgrade, int dexoptFlags, String oatDir) throws LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); + // Allow calls from OtaDexoptService even when ART Service is in use. The installer is + // isolated in that case so later calls to it won't call into installd anyway. + if (!mInstaller.isIsolated()) { + Installer.checkLegacyDexoptDisabled(); + } + final boolean shouldBePublic = (dexoptFlags & DEXOPT_PUBLIC) != 0; final boolean isProfileGuidedFilter = (dexoptFlags & DEXOPT_PROFILE_GUIDED) != 0; boolean newProfile = profileAnalysisResult == PROFILE_ANALYSIS_OPTIMIZE; @@ -948,6 +976,8 @@ public class PackageDexOptimizer { */ private int analyseProfiles(AndroidPackage pkg, int uid, String profileName, String compilerFilter) throws LegacyDexoptDisabledException { + Installer.checkLegacyDexoptDisabled(); + // Check if we are allowed to merge and if the compiler filter is profile guided. if (!isProfileGuidedCompilerFilter(compilerFilter)) { return PROFILE_ANALYSIS_DONT_OPTIMIZE_SMALL_DELTA; diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 928ffa718c6f..3f9a0bc89641 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -24,6 +24,7 @@ import static android.system.OsConstants.O_CREAT; import static android.system.OsConstants.O_RDWR; import static com.android.internal.content.NativeLibraryHelper.LIB_DIR_NAME; +import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH; import static com.android.server.LocalManagerRegistry.ManagerNotFoundException; import static com.android.server.pm.PackageManagerService.COMPRESSED_EXTENSION; import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION; @@ -94,6 +95,7 @@ import com.android.server.EventLogTags; import com.android.server.IntentResolver; import com.android.server.LocalManagerRegistry; import com.android.server.Watchdog; +import com.android.server.am.ActivityManagerUtils; import com.android.server.compat.PlatformCompat; import com.android.server.pm.dex.PackageDexUsage; import com.android.server.pm.pkg.AndroidPackage; @@ -1186,12 +1188,6 @@ public class PackageManagerServiceUtils { continue; } - // Only enforce filter matching if target app's target SDK >= T - if (!compat.isChangeEnabledInternal( - ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS, info.applicationInfo)) { - continue; - } - final ParsedMainComponent comp; if (info instanceof ActivityInfo) { if (isReceiver) { @@ -1210,6 +1206,10 @@ public class PackageManagerServiceUtils { continue; } + // Only enforce filter matching if target app's target SDK >= T + final boolean enforce = compat.isChangeEnabledInternal( + ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS, info.applicationInfo); + boolean match = false; for (int j = 0, size = comp.getIntents().size(); j < size; ++j) { IntentFilter intentFilter = comp.getIntents().get(j).getIntentFilter(); @@ -1219,14 +1219,19 @@ public class PackageManagerServiceUtils { } } if (!match) { - Slog.w(TAG, "Intent does not match component's intent filter: " + intent); - Slog.w(TAG, "Access blocked: " + comp.getComponentName()); - if (DEBUG_INTENT_MATCHING) { - Slog.v(TAG, "Component intent filters:"); - comp.getIntents().forEach(f -> f.getIntentFilter().dump(logPrinter, " ")); - Slog.v(TAG, "-----------------------------"); + ActivityManagerUtils.logUnsafeIntentEvent( + UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH, + filterCallingUid, intent, resolvedType, enforce); + if (enforce) { + Slog.w(TAG, "Intent does not match component's intent filter: " + intent); + Slog.w(TAG, "Access blocked: " + comp.getComponentName()); + if (DEBUG_INTENT_MATCHING) { + Slog.v(TAG, "Component intent filters:"); + comp.getIntents().forEach(f -> f.getIntentFilter().dump(logPrinter, " ")); + Slog.v(TAG, "-----------------------------"); + } + resolveInfos.remove(i); } - resolveInfos.remove(i); } } } diff --git a/services/core/java/com/android/server/pm/ResolveIntentHelper.java b/services/core/java/com/android/server/pm/ResolveIntentHelper.java index a13c568f87a6..7ed10a4df1db 100644 --- a/services/core/java/com/android/server/pm/ResolveIntentHelper.java +++ b/services/core/java/com/android/server/pm/ResolveIntentHelper.java @@ -18,6 +18,7 @@ package com.android.server.pm; import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; +import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH; import static com.android.server.pm.PackageManagerService.DEBUG_INSTANT; import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING; import static com.android.server.pm.PackageManagerService.TAG; @@ -55,9 +56,9 @@ import android.util.Slog; import com.android.internal.app.ResolverActivity; import com.android.internal.util.ArrayUtils; -import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; import com.android.server.am.ActivityManagerService; +import com.android.server.am.ActivityManagerUtils; import com.android.server.compat.PlatformCompat; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; @@ -130,18 +131,9 @@ final class ResolveIntentHelper { boolean hasToBeExportedToMatch = platformCompat.isChangeEnabledByUid( ActivityManagerService.IMPLICIT_INTENTS_ONLY_MATCH_EXPORTED_COMPONENTS, filterCallingUid); - String[] categories = intent.getCategories() == null ? new String[0] - : intent.getCategories().toArray(String[]::new); - FrameworkStatsLog.write(FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED, - FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH, - filterCallingUid, - query.get(i).getComponentInfo().getComponentName().flattenToShortString(), - callerPackage, - intent.getAction(), - categories, - resolvedType, - intent.getScheme(), - hasToBeExportedToMatch); + ActivityManagerUtils.logUnsafeIntentEvent( + UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH, + filterCallingUid, intent, resolvedType, hasToBeExportedToMatch); if (callback != null) { handler.post(() -> { try { diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 45dc49dba2ca..37a59da33ad8 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -2977,7 +2977,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } break; case KeyEvent.KEYCODE_H: - if (down && event.isMetaPressed()) { + if (event.isMetaPressed()) { return handleHomeShortcuts(displayId, focusedToken, event); } break; @@ -3018,11 +3018,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) { enterStageSplitFromRunningApp(true /* leftOrTop */); return key_consumed; - } else if (!down && event.isMetaPressed()) { - boolean backKeyHandled = backKeyPress(); - if (backKeyHandled) { - return key_consumed; - } } break; case KeyEvent.KEYCODE_DPAD_RIGHT: @@ -3031,14 +3026,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { return key_consumed; } break; - case KeyEvent.KEYCODE_GRAVE: - if (!down && event.isMetaPressed()) { - boolean backKeyHandled = backKeyPress(); - if (backKeyHandled) { - return key_consumed; - } - } - break; case KeyEvent.KEYCODE_SLASH: if (down && repeatCount == 0 && event.isMetaPressed() && !keyguardOn) { toggleKeyboardShortcutsMenu(event.getDeviceId()); diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index f11c864edba0..bc23020e8dbe 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -12688,8 +12688,8 @@ public class BatteryStatsImpl extends BatteryStats { energy = info.getControllerEnergyUsed(); if (!info.getUidTraffic().isEmpty()) { for (UidTraffic traffic : info.getUidTraffic()) { - uidRxBytes.incrementValue(traffic.getUid(), traffic.getRxBytes()); - uidTxBytes.incrementValue(traffic.getUid(), traffic.getTxBytes()); + uidRxBytes.put(traffic.getUid(), traffic.getRxBytes()); + uidTxBytes.put(traffic.getUid(), traffic.getTxBytes()); } } } diff --git a/services/core/java/com/android/server/power/stats/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/CpuWakeupStats.java index e8c0e5924252..b05b662dc1e8 100644 --- a/services/core/java/com/android/server/power/stats/CpuWakeupStats.java +++ b/services/core/java/com/android/server/power/stats/CpuWakeupStats.java @@ -22,7 +22,9 @@ import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_WIFI; import android.content.Context; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.util.IndentingPrintWriter; import android.util.IntArray; import android.util.LongSparseArray; @@ -40,6 +42,8 @@ import com.android.internal.util.IntPair; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -52,13 +56,13 @@ public class CpuWakeupStats { private static final String SUBSYSTEM_ALARM_STRING = "Alarm"; private static final String SUBSYSTEM_ALARM_WIFI = "Wifi"; @VisibleForTesting - static final long WAKEUP_RETENTION_MS = 3 * 24 * 60 * 60_000; // 3 days. - @VisibleForTesting static final long WAKEUP_REASON_HALF_WINDOW_MS = 500; - private static final long WAKEUP_WRITE_DELAY_MS = 2 * 60 * 1000; // 2 minutes. + private static final long WAKEUP_WRITE_DELAY_MS = TimeUnit.MINUTES.toMillis(2); private final Handler mHandler; private final IrqDeviceMap mIrqDeviceMap; + @VisibleForTesting + final Config mConfig = new Config(); private final WakingActivityHistory mRecentWakingActivity = new WakingActivityHistory(); @VisibleForTesting @@ -72,6 +76,14 @@ public class CpuWakeupStats { mHandler = handler; } + /** + * Called on the boot phase SYSTEM_SERVICES_READY. + * This ensures that DeviceConfig is ready for calls to read properties. + */ + public synchronized void systemServicesReady() { + mConfig.register(new HandlerExecutor(mHandler)); + } + private static int subsystemToStatsReason(int subsystem) { switch (subsystem) { case CPU_WAKEUP_SUBSYSTEM_ALARM: @@ -126,21 +138,25 @@ public class CpuWakeupStats { /** Notes a wakeup reason as reported by SuspendControlService to battery stats. */ public synchronized void noteWakeupTimeAndReason(long elapsedRealtime, long uptime, String rawReason) { - final Wakeup parsedWakeup = new Wakeup(rawReason, elapsedRealtime, uptime); + final Wakeup parsedWakeup = Wakeup.parseWakeup(rawReason, elapsedRealtime, uptime); + if (parsedWakeup == null) { + return; + } mWakeupEvents.put(elapsedRealtime, parsedWakeup); attemptAttributionFor(parsedWakeup); // Assuming that wakeups always arrive in monotonically increasing elapsedRealtime order, // we can delete all history that will not be useful in attributing future wakeups. mRecentWakingActivity.clearAllBefore(elapsedRealtime - WAKEUP_REASON_HALF_WINDOW_MS); - // Limit history of wakeups and their attribution to the last WAKEUP_RETENTION_MS. Note that + // Limit history of wakeups and their attribution to the last retentionDuration. Note that // the last wakeup and its attribution (if computed) is always stored, even if that wakeup - // had occurred before WAKEUP_RETENTION_MS. - int lastIdx = mWakeupEvents.closestIndexOnOrBefore(elapsedRealtime - WAKEUP_RETENTION_MS); + // had occurred before retentionDuration. + final long retentionDuration = mConfig.WAKEUP_STATS_RETENTION_MS; + int lastIdx = mWakeupEvents.closestIndexOnOrBefore(elapsedRealtime - retentionDuration); for (int i = lastIdx; i >= 0; i--) { mWakeupEvents.removeAt(i); } - lastIdx = mWakeupAttribution.closestIndexOnOrBefore(elapsedRealtime - WAKEUP_RETENTION_MS); + lastIdx = mWakeupAttribution.closestIndexOnOrBefore(elapsedRealtime - retentionDuration); for (int i = lastIdx; i >= 0; i--) { mWakeupAttribution.removeAt(i); } @@ -223,6 +239,9 @@ public class CpuWakeupStats { pw.println("CPU wakeup stats:"); pw.increaseIndent(); + mConfig.dump(pw); + pw.println(); + mIrqDeviceMap.dump(pw); pw.println(); @@ -293,7 +312,8 @@ public class CpuWakeupStats { } private static final class WakingActivityHistory { - private static final long WAKING_ACTIVITY_RETENTION_MS = 3 * 60 * 60_000; // 3 hours. + private static final long WAKING_ACTIVITY_RETENTION_MS = TimeUnit.MINUTES.toMillis(10); + private SparseArray<TimeSparseArray<SparseBooleanArray>> mWakingActivity = new SparseArray<>(); @@ -451,28 +471,25 @@ public class CpuWakeupStats { private static final String PARSER_TAG = "CpuWakeupStats.Wakeup"; private static final String ABORT_REASON_PREFIX = "Abort"; private static final Pattern sIrqPattern = Pattern.compile("^(\\d+)\\s+(\\S+)"); - - String mRawReason; long mElapsedMillis; long mUptimeMillis; IrqDevice[] mDevices; - Wakeup(String rawReason, long elapsedMillis, long uptimeMillis) { - mRawReason = rawReason; + private Wakeup(IrqDevice[] devices, long elapsedMillis, long uptimeMillis) { mElapsedMillis = elapsedMillis; mUptimeMillis = uptimeMillis; - mDevices = parseIrqDevices(rawReason); + mDevices = devices; } - private static IrqDevice[] parseIrqDevices(String rawReason) { + static Wakeup parseWakeup(String rawReason, long elapsedMillis, long uptimeMillis) { final String[] components = rawReason.split(":"); if (ArrayUtils.isEmpty(components) || components[0].startsWith(ABORT_REASON_PREFIX)) { - // We don't support parsing aborts yet. + // Accounting of aborts is not supported yet. return null; } int parsedDeviceCount = 0; - IrqDevice[] parsedDevices = new IrqDevice[components.length]; + final IrqDevice[] parsedDevices = new IrqDevice[components.length]; for (String component : components) { final Matcher matcher = sIrqPattern.matcher(component.trim()); @@ -490,14 +507,17 @@ public class CpuWakeupStats { parsedDevices[parsedDeviceCount++] = new IrqDevice(line, device); } } - return (parsedDeviceCount > 0) ? Arrays.copyOf(parsedDevices, parsedDeviceCount) : null; + if (parsedDeviceCount == 0) { + return null; + } + return new Wakeup(Arrays.copyOf(parsedDevices, parsedDeviceCount), elapsedMillis, + uptimeMillis); } @Override public String toString() { return "Wakeup{" - + "mRawReason='" + mRawReason + '\'' - + ", mElapsedMillis=" + mElapsedMillis + + "mElapsedMillis=" + mElapsedMillis + ", mUptimeMillis=" + TimeUtils.formatDuration(mUptimeMillis) + ", mDevices=" + Arrays.toString(mDevices) + '}'; @@ -514,8 +534,56 @@ public class CpuWakeupStats { @Override public String toString() { - return "IrqDevice{" + "mLine=" + mLine + ", mDevice='" + mDevice + '\'' + '}'; + return "IrqDevice{" + "mLine=" + mLine + ", mDevice=\'" + mDevice + '\'' + '}'; } } } + + static final class Config implements DeviceConfig.OnPropertiesChangedListener { + static final String KEY_WAKEUP_STATS_RETENTION_MS = "wakeup_stats_retention_ms"; + + private static final String[] PROPERTY_NAMES = { + KEY_WAKEUP_STATS_RETENTION_MS, + }; + + static final long DEFAULT_WAKEUP_STATS_RETENTION_MS = TimeUnit.DAYS.toMillis(3); + + /** + * Wakeup stats are retained only for this duration. + */ + public volatile long WAKEUP_STATS_RETENTION_MS = DEFAULT_WAKEUP_STATS_RETENTION_MS; + + void register(Executor executor) { + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_BATTERY_STATS, + executor, this); + onPropertiesChanged(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_BATTERY_STATS, + PROPERTY_NAMES)); + } + + @Override + public void onPropertiesChanged(DeviceConfig.Properties properties) { + for (String name : properties.getKeyset()) { + if (name == null) { + continue; + } + switch (name) { + case KEY_WAKEUP_STATS_RETENTION_MS: + WAKEUP_STATS_RETENTION_MS = properties.getLong( + KEY_WAKEUP_STATS_RETENTION_MS, DEFAULT_WAKEUP_STATS_RETENTION_MS); + break; + } + } + } + + void dump(IndentingPrintWriter pw) { + pw.println("Config:"); + + pw.increaseIndent(); + + pw.print(KEY_WAKEUP_STATS_RETENTION_MS, WAKEUP_STATS_RETENTION_MS); + pw.println(); + + pw.decreaseIndent(); + } + } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 5bace0ebe13a..88d64df99d48 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -151,6 +151,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S_V2) static final long REQUEST_LISTENING_MUST_MATCH_PACKAGE = 172251878L; + /** + * @hide + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) + static final long REQUEST_LISTENING_OTHER_USER_NOOP = 242194868L; + private final Context mContext; private final Handler mHandler = new Handler(); @@ -1888,7 +1895,12 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D // Check current user if (userId != currentUser) { - throw new IllegalArgumentException("User " + userId + " is not the current user."); + if (CompatChanges.isChangeEnabled(REQUEST_LISTENING_OTHER_USER_NOOP, callingUid)) { + return; + } else { + throw new IllegalArgumentException( + "User " + userId + " is not the current user."); + } } } if (mBar != null) { diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 404ca017929d..c6684dfdb6e0 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -2125,6 +2125,26 @@ public final class TvInputManagerService extends SystemService { } @Override + public void notifyTvMessage(IBinder sessionToken, String type, Bundle data, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "timeShiftEnablePositionTracking"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId) + .notifyTvMessage(type, data); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in timeShiftEnablePositionTracking", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void startRecording(IBinder sessionToken, @Nullable Uri programUri, @Nullable Bundle params, int userId) { final int callingUid = Binder.getCallingUid(); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 7a733592b30c..c9eef387eeb2 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -923,6 +923,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (DEBUG) Slog.v(TAG, "Adding window token: " + mToken); mWindowManagerInternal.addWindowToken(mToken, TYPE_WALLPAPER, mDisplayId, null /* options */); + mWindowManagerInternal.setWallpaperShowWhenLocked( + mToken, (wallpaper.mWhich & FLAG_LOCK) != 0); final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(mDisplayId); try { @@ -1415,12 +1417,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub try { if (connector.mEngine != null) { connector.mEngine.setWallpaperFlags(which); + mWindowManagerInternal.setWallpaperShowWhenLocked( + connector.mToken, (which & FLAG_LOCK) != 0); } } catch (RemoteException e) { Slog.e(TAG, "Failed to update wallpaper engine flags", e); } - } - ); + }); } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9def87c3d77a..41fee1c5187d 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5228,8 +5228,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mTaskSupervisor.mAppVisibilitiesChangedSinceLastPause = true; } - @VisibleForTesting - void setVisibility(boolean visible, boolean deferHidingClient) { + private void setVisibility(boolean visible, boolean deferHidingClient) { final AppTransition appTransition = getDisplayContent().mAppTransition; // Don't set visibility to false if we were already not visible. This prevents WM from @@ -8146,7 +8145,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } // Clear config override in #updateCompatDisplayInsets(). - onRequestedOverrideConfigurationChanged(EMPTY); + final int activityType = getActivityType(); + final Configuration overrideConfig = getRequestedOverrideConfiguration(); + overrideConfig.unset(); + // Keep the activity type which was set when attaching to a task to prevent leaving it + // undefined. + overrideConfig.windowConfiguration.setActivityType(activityType); + onRequestedOverrideConfigurationChanged(overrideConfig); } @Override diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index df471c56fec9..710c4af56dd1 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -1729,14 +1729,11 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid)) { - Toast toast = Toast.makeText(mService.mContext, + UiThread.getHandler().post(() -> Toast.makeText(mService.mContext, (ActivitySecurityModelFeatureFlags.DOC_LINK - + (restrictActivitySwitch - ? "returned home due to " - : "would return home due to ") - + callingLabel), - Toast.LENGTH_LONG); - UiThread.getHandler().post(toast::show); + + (restrictActivitySwitch ? " returned home due to " + : " would return home due to ") + + callingLabel), Toast.LENGTH_LONG).show()); } // If the activity switch should be restricted, return home rather than the diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java index f73c68a42ec7..939cf1ae471b 100644 --- a/services/core/java/com/android/server/wm/AppTransition.java +++ b/services/core/java/com/android/server/wm/AppTransition.java @@ -888,8 +888,11 @@ public class AppTransition implements Dump { } else { int animAttr = mapOpenCloseTransitTypes(transit, enter); if (animAttr != 0) { - a = loadCustomActivityAnimation(animAttr, enter, container); - if (a == null) { + final CustomAppTransition customAppTransition = + getCustomAppTransition(animAttr, container); + if (customAppTransition != null) { + a = loadCustomActivityAnimation(customAppTransition, enter, container); + } else { if (canCustomizeAppTransition) { a = loadAnimationAttr(lp, animAttr, transit); } else { @@ -911,7 +914,7 @@ public class AppTransition implements Dump { return a; } - Animation loadCustomActivityAnimation(int animAttr, boolean enter, WindowContainer container) { + CustomAppTransition getCustomAppTransition(int animAttr, WindowContainer container) { ActivityRecord customAnimationSource = container.asActivityRecord(); if (customAnimationSource == null) { return null; @@ -927,31 +930,28 @@ public class AppTransition implements Dump { return null; } } - final CustomAppTransition custom; switch (animAttr) { case WindowAnimation_activityOpenEnterAnimation: case WindowAnimation_activityOpenExitAnimation: - custom = customAnimationSource.getCustomAnimation(true /* open */); - break; + return customAnimationSource.getCustomAnimation(true /* open */); case WindowAnimation_activityCloseEnterAnimation: case WindowAnimation_activityCloseExitAnimation: - custom = customAnimationSource.getCustomAnimation(false /* open */); - break; - default: - return null; - } - if (custom != null) { - final Animation a = mTransitionAnimation.loadAppTransitionAnimation( - customAnimationSource.packageName, enter - ? custom.mEnterAnim : custom.mExitAnim); - if (a != null && custom.mBackgroundColor != 0) { - a.setBackdropColor(custom.mBackgroundColor); - a.setShowBackdrop(true); - } - return a; + return customAnimationSource.getCustomAnimation(false /* open */); } return null; } + private Animation loadCustomActivityAnimation(@NonNull CustomAppTransition custom, + boolean enter, WindowContainer container) { + final ActivityRecord customAnimationSource = container.asActivityRecord(); + final Animation a = mTransitionAnimation.loadAppTransitionAnimation( + customAnimationSource.packageName, enter + ? custom.mEnterAnim : custom.mExitAnim); + if (a != null && custom.mBackgroundColor != 0) { + a.setBackdropColor(custom.mBackgroundColor); + a.setShowBackdrop(true); + } + return a; + } int getAppRootTaskClipMode() { return mNextAppTransitionRequests.contains(TRANSIT_RELAUNCH) diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index 0dc6e0ff1054..5c9c81300e0d 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -364,6 +364,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume /** Hides the window immediately until it is drawn in new rotation. */ void hideImmediately(WindowToken windowToken) { + if (isTargetToken(windowToken)) return; final boolean original = mHideImmediately; mHideImmediately = true; final Operation op = new Operation(Operation.ACTION_FADE); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 947eddeaa3eb..87f5703bdbc5 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4158,13 +4158,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** @see WindowManagerInternal#onToggleImeRequested */ void onShowImeRequested() { - if (mImeLayeringTarget == null || mInputMethodWindow == null) { + if (mInputMethodWindow == null) { return; } // If IME window will be shown on the rotated activity, share the transformed state to // IME window so it can compute rotated frame with rotated configuration. - if (mImeLayeringTarget.mToken.isFixedRotationTransforming()) { - mInputMethodWindow.mToken.linkFixedRotationTransform(mImeLayeringTarget.mToken); + if (mFixedRotationLaunchingApp != null) { + mInputMethodWindow.mToken.linkFixedRotationTransform(mFixedRotationLaunchingApp); // Hide the window until the rotation is done to avoid intermediate artifacts if the // parent surface of IME container is changed. if (mAsyncRotationController != null) { diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 210a7d9538c7..c1f2b2be3ea7 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -580,6 +580,13 @@ class InsetsPolicy { // Notification shade has control anyways, no reason to force anything. return focusedWin; } + if (focusedWin != null) { + final InsetsSourceProvider provider = focusedWin.getControllableInsetProvider(); + if (provider != null && provider.getSource().getType() == Type.navigationBars()) { + // Navigation bar has control if it is focused. + return focusedWin; + } + } if (mPolicy.isForceShowNavigationBarEnabled() && focusedWin != null && focusedWin.getActivityType() == ACTIVITY_TYPE_STANDARD) { // When "force show navigation bar" is enabled, it means both force visible is true, and diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index b3b56f273f3d..e147219de4c6 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -33,6 +33,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_PIP; +import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_WAKE; @@ -2329,6 +2330,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } void applySleepTokens(boolean applyToRootTasks) { + boolean builtSleepTransition = false; for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) { // Set the sleeping state of the display. final DisplayContent display = getChildAt(displayNdx); @@ -2338,6 +2340,30 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } display.setIsSleeping(displayShouldSleep); + if (display.mTransitionController.isShellTransitionsEnabled() && !builtSleepTransition + // Only care if there are actual sleep tokens. + && displayShouldSleep && !display.mAllSleepTokens.isEmpty()) { + builtSleepTransition = true; + // We don't actually care about collecting anything here. We really just want + // this as a signal to the transition-player. + final Transition transition = new Transition(TRANSIT_SLEEP, 0 /* flags */, + display.mTransitionController, mWmService.mSyncEngine); + final Runnable sendSleepTransition = () -> { + display.mTransitionController.requestStartTransition(transition, + null /* trigger */, null /* remote */, null /* display */); + // Force playing immediately so that unrelated ops can't be collected. + transition.playNow(); + }; + if (display.mTransitionController.isCollecting()) { + mWmService.mSyncEngine.queueSyncSet( + () -> display.mTransitionController.moveToCollecting(transition), + sendSleepTransition); + } else { + display.mTransitionController.moveToCollecting(transition); + sendSleepTransition.run(); + } + } + if (!applyToRootTasks) { continue; } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index a30ab11d9f6d..bf6983b90fbb 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -218,6 +218,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { final TransitionController.Logger mLogger = new TransitionController.Logger(); + /** Whether this transition was forced to play early (eg for a SLEEP signal). */ + private boolean mForcePlaying = false; + /** * {@code false} if this transition runs purely in WMCore (meaning Shell is completely unaware * of it). Currently, this happens before the display is ready since nothing can be seen yet. @@ -389,6 +392,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return mState == STATE_COLLECTING || mState == STATE_STARTED; } + boolean isAborted() { + return mState == STATE_ABORT; + } + boolean isStarted() { return mState == STATE_STARTED; } @@ -997,6 +1004,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { void abort() { // This calls back into itself via controller.abort, so just early return here. if (mState == STATE_ABORT) return; + if (mState == STATE_PENDING) { + // hasn't started collecting, so can jump directly to aborted state. + mState = STATE_ABORT; + return; + } if (mState != STATE_COLLECTING && mState != STATE_STARTED) { throw new IllegalStateException("Too late to abort. state=" + mState); } @@ -1007,6 +1019,27 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.dispatchLegacyAppTransitionCancelled(); } + /** Immediately moves this to playing even if it isn't started yet. */ + void playNow() { + if (!(mState == STATE_COLLECTING || mState == STATE_STARTED)) { + return; + } + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Force Playing Transition: %d", + mSyncId); + mForcePlaying = true; + setAllReady(); + if (mState == STATE_COLLECTING) { + start(); + } + // Don't wait for actual surface-placement. We don't want anything else collected in this + // transition. + mSyncEngine.onSurfacePlacement(); + } + + boolean isForcePlaying() { + return mForcePlaying; + } + void setRemoteTransition(RemoteTransition remoteTransition) { mRemoteTransition = remoteTransition; } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 79eb63462d0b..6c951bfc5f9a 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -209,6 +209,12 @@ class TransitionController { if (mCollectingTransition != null) { throw new IllegalStateException("Simultaneous transition collection not supported."); } + if (mTransitionPlayer == null) { + // If sysui has been killed (by a test) or crashed, we can temporarily have no player + // In this case, abort the transition. + transition.abort(); + return; + } mCollectingTransition = transition; // Distinguish change type because the response time is usually expected to be not too long. final long timeoutMs = @@ -511,6 +517,14 @@ class TransitionController { transition.getToken(), null)); return transition; } + if (mTransitionPlayer == null || transition.isAborted()) { + // Apparently, some tests will kill(and restart) systemui, so there is a chance that + // the player might be transiently null. + if (transition.isCollecting()) { + transition.abort(); + } + return transition; + } try { ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Requesting StartTransition: %s", transition); diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 16541c10d9db..2b848d57e2f9 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -37,6 +37,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.H.WALLPAPER_DRAW_PENDING_TIMEOUT; import android.annotation.Nullable; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.Rect; @@ -55,6 +56,7 @@ import android.view.WindowManager; import android.view.animation.Animation; import android.window.ScreenCapture; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLogImpl; import com.android.internal.protolog.common.ProtoLog; @@ -72,7 +74,7 @@ import java.util.function.Consumer; class WallpaperController { private static final String TAG = TAG_WITH_CLASS_NAME ? "WallpaperController" : TAG_WM; private WindowManagerService mService; - private final DisplayContent mDisplayContent; + private DisplayContent mDisplayContent; private final ArrayList<WallpaperWindowToken> mWallpaperTokens = new ArrayList<>(); @@ -120,9 +122,19 @@ class WallpaperController { private boolean mShouldOffsetWallpaperCenter; + final boolean mEnableSeparateLockScreenEngine; + private final ToBooleanFunction<WindowState> mFindWallpaperTargetFunction = w -> { if ((w.mAttrs.type == TYPE_WALLPAPER)) { if (mFindResults.topWallpaper == null || mFindResults.resetTopWallpaper) { + WallpaperWindowToken token = w.mToken.asWallpaperToken(); + if (token == null) { + Slog.w(TAG, "Window " + w + " has wallpaper type but not wallpaper token"); + return false; + } + if (!token.canShowWhenLocked() && mDisplayContent.isKeyguardLocked()) { + return false; + } mFindResults.setTopWallpaper(w); mFindResults.resetTopWallpaper = false; } @@ -249,11 +261,14 @@ class WallpaperController { WallpaperController(WindowManagerService service, DisplayContent displayContent) { mService = service; mDisplayContent = displayContent; - mMaxWallpaperScale = service.mContext.getResources() - .getFloat(com.android.internal.R.dimen.config_wallpaperMaxScale); - mShouldOffsetWallpaperCenter = service.mContext.getResources() - .getBoolean( + Resources resources = service.mContext.getResources(); + mMaxWallpaperScale = + resources.getFloat(com.android.internal.R.dimen.config_wallpaperMaxScale); + mShouldOffsetWallpaperCenter = + resources.getBoolean( com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay); + mEnableSeparateLockScreenEngine = + resources.getBoolean(R.bool.config_independentLockscreenLiveWallpaper); } void resetLargestDisplay(Display display) { @@ -753,10 +768,10 @@ class WallpaperController { result.setWallpaperTarget(wallpaperTarget); } - private void updateWallpaperTokens(boolean visible) { + private void updateWallpaperTokens(boolean visibility, boolean locked) { for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) { final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx); - token.updateWallpaperWindows(visible); + token.updateWallpaperWindows(visibility && (!locked || token.canShowWhenLocked())); } } @@ -794,7 +809,13 @@ class WallpaperController { } } - updateWallpaperTokens(visible); + // Keep both wallpapers visible unless the keyguard is locked (then hide private wp) + updateWallpaperTokens(visible, mDisplayContent.isKeyguardLocked()); + + if (DEBUG_WALLPAPER) { + Slog.v(TAG, "adjustWallpaperWindows: wallpaper visibility " + visible + + ", lock visibility " + mDisplayContent.isKeyguardLocked()); + } if (visible && mLastFrozen != mFindResults.isWallpaperTargetForLetterbox) { mLastFrozen = mFindResults.isWallpaperTargetForLetterbox; @@ -896,7 +917,6 @@ class WallpaperController { mWallpaperTokens.remove(token); } - @VisibleForTesting boolean canScreenshotWallpaper() { return canScreenshotWallpaper(getTopVisibleWallpaper()); diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 8708f73980c6..17ab551b5c1e 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -76,14 +76,18 @@ class WallpaperWindowToken extends WindowToken { return; } mShowWhenLocked = showWhenLocked; - - // Move the window token to the front (private) or back (showWhenLocked). This is possible - // because the DisplayArea underneath TaskDisplayArea only contains TYPE_WALLPAPER windows. - final int position = showWhenLocked ? POSITION_BOTTOM : POSITION_TOP; - - // Note: Moving all the way to the front or back breaks ordering based on addition times. - // We should never have more than one non-animating token of each type. - getParent().positionChildAt(position, this /* child */, false /*includingParents */); + if (mDisplayContent.mWallpaperController.mEnableSeparateLockScreenEngine) { + // Move the window token to the front (private) or back (showWhenLocked). This is + // possible + // because the DisplayArea underneath TaskDisplayArea only contains TYPE_WALLPAPER + // windows. + final int position = showWhenLocked ? POSITION_BOTTOM : POSITION_TOP; + + // Note: Moving all the way to the front or back breaks ordering based on addition + // times. + // We should never have more than one non-animating token of each type. + getParent().positionChildAt(position, this /* child */, false /*includingParents */); + } } boolean canShowWhenLocked() { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index b9cb59a17a2e..495d7ce4e90b 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -317,7 +317,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } transition = mTransitionController.createTransition(type); } - if (!transition.isCollecting()) { + if (!transition.isCollecting() && !transition.isForcePlaying()) { Slog.e(TAG, "Trying to start a transition that isn't collecting. This probably" + " means Shell took too long to respond to a request. WM State may be" + " incorrect now, please file a bug"); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 87e87b95fdfd..cf0fc0995a9b 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -2018,16 +2018,19 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP /** * Like isOnScreen(), but we don't return true if the window is part - * of a transition that has not yet been started. + * of a transition but has not yet started animating. */ boolean isReadyForDisplay() { - if (mToken.waitingToShow && getDisplayContent().mAppTransition.isTransitionSet()) { + if (!mHasSurface || mDestroying || !isVisibleByPolicy()) { + return false; + } + if (mToken.waitingToShow && getDisplayContent().mAppTransition.isTransitionSet() + && !isAnimating(TRANSITION | PARENTS, ANIMATION_TYPE_APP_TRANSITION)) { return false; } final boolean parentAndClientVisible = !isParentWindowHidden() && mViewVisibility == View.VISIBLE && mToken.isVisible(); - return mHasSurface && isVisibleByPolicy() && !mDestroying - && (parentAndClientVisible || isAnimating(TRANSITION | PARENTS)); + return parentAndClientVisible || isAnimating(TRANSITION | PARENTS, ANIMATION_TYPE_ALL); } boolean isFullyTransparent() { @@ -2342,9 +2345,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @Override public void onConfigurationChanged(Configuration newParentConfig) { - mTempConfiguration.setTo(getConfiguration()); + // Get from super to avoid using the updated global config from the override method. + final Configuration selfConfiguration = super.getConfiguration(); + mTempConfiguration.setTo(selfConfiguration); super.onConfigurationChanged(newParentConfig); - final int diff = getConfiguration().diff(mTempConfiguration); + final int diff = selfConfiguration.diff(mTempConfiguration); if (diff != 0) { mLastConfigReportedToClient = false; } diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java index 6bf18c27ee05..e84f0cc17d05 100644 --- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java @@ -20,17 +20,18 @@ import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.credentials.ClearCredentialStateRequest; +import android.credentials.CredentialProviderInfo; import android.credentials.IClearCredentialStateCallback; import android.credentials.ui.ProviderData; import android.credentials.ui.RequestInfo; import android.os.CancellationSignal; import android.os.RemoteException; import android.service.credentials.CallingAppInfo; -import android.service.credentials.CredentialProviderInfo; import android.util.Log; import com.android.server.credentials.metrics.ApiName; import com.android.server.credentials.metrics.ApiStatus; +import com.android.server.credentials.metrics.ProviderStatusForMetrics; import java.util.ArrayList; @@ -119,24 +120,23 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta private void respondToClientWithResponseAndFinish() { Log.i(TAG, "respondToClientWithResponseAndFinish"); if (isSessionCancelled()) { - // TODO: Differentiate btw cancelled and false mChosenProviderMetric.setChosenProviderStatus( - MetricUtilities.METRICS_PROVIDER_STATUS_FINAL_SUCCESS); + ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode()); logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_CLIENT_CANCELED); + ApiStatus.CLIENT_CANCELED); finishSession(/*propagateCancellation=*/true); return; } try { mClientCallback.onSuccess(); logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_SUCCESS); + ApiStatus.SUCCESS); } catch (RemoteException e) { mChosenProviderMetric.setChosenProviderStatus( - MetricUtilities.METRICS_PROVIDER_STATUS_FINAL_FAILURE); + ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode()); Log.i(TAG, "Issue while propagating the response to the client"); logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_FAILURE); + ApiStatus.FAILURE); } finishSession(/*propagateCancellation=*/false); } @@ -144,9 +144,8 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { Log.i(TAG, "respondToClientWithErrorAndFinish"); if (isSessionCancelled()) { - // TODO: Differentiate btw cancelled and false logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_CLIENT_CANCELED); + ApiStatus.CLIENT_CANCELED); finishSession(/*propagateCancellation=*/true); return; } @@ -156,7 +155,7 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta e.printStackTrace(); } logApiCall(ApiName.CLEAR_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_FAILURE); + ApiStatus.FAILURE); finishSession(/*propagateCancellation=*/false); } diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index 656e44c4bff2..7e1780d05d75 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -16,9 +16,6 @@ package com.android.server.credentials; -import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_FINAL_FAILURE; -import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_FINAL_SUCCESS; - import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; @@ -27,17 +24,18 @@ import android.credentials.CreateCredentialException; import android.credentials.CreateCredentialRequest; import android.credentials.CreateCredentialResponse; import android.credentials.CredentialManager; +import android.credentials.CredentialProviderInfo; import android.credentials.ICreateCredentialCallback; import android.credentials.ui.ProviderData; import android.credentials.ui.RequestInfo; import android.os.CancellationSignal; import android.os.RemoteException; import android.service.credentials.CallingAppInfo; -import android.service.credentials.CredentialProviderInfo; import android.util.Log; import com.android.server.credentials.metrics.ApiName; import com.android.server.credentials.metrics.ApiStatus; +import com.android.server.credentials.metrics.ProviderStatusForMetrics; import java.util.ArrayList; @@ -103,11 +101,11 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR setChosenMetric(componentName); if (response != null) { mChosenProviderMetric.setChosenProviderStatus( - METRICS_PROVIDER_STATUS_FINAL_SUCCESS); + ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode()); respondToClientWithResponseAndFinish(response); } else { mChosenProviderMetric.setChosenProviderStatus( - METRICS_PROVIDER_STATUS_FINAL_FAILURE); + ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode()); respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREATE_OPTIONS, "Invalid response"); } @@ -143,20 +141,19 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR return; } if (isSessionCancelled()) { - // TODO: Differentiate btw cancelled and false logApiCall(ApiName.CREATE_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_CLIENT_CANCELED); + ApiStatus.CLIENT_CANCELED); finishSession(/*propagateCancellation=*/true); return; } try { mClientCallback.onResponse(response); logApiCall(ApiName.CREATE_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_SUCCESS); + ApiStatus.SUCCESS); } catch (RemoteException e) { Log.i(TAG, "Issue while responding to client: " + e.getMessage()); logApiCall(ApiName.CREATE_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_FAILURE); + ApiStatus.FAILURE); } finishSession(/*propagateCancellation=*/false); } @@ -168,9 +165,8 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR return; } if (isSessionCancelled()) { - // TODO: Differentiate btw cancelled and false logApiCall(ApiName.CREATE_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_CLIENT_CANCELED); + ApiStatus.CLIENT_CANCELED); finishSession(/*propagateCancellation=*/true); return; } @@ -186,10 +182,10 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR private void logFailureOrUserCancel(String errorType) { if (CreateCredentialException.TYPE_USER_CANCELED.equals(errorType)) { logApiCall(ApiName.CREATE_CREDENTIAL, - /* apiStatus */ ApiStatus.METRICS_API_STATUS_USER_CANCELED); + /* apiStatus */ ApiStatus.USER_CANCELED); } else { logApiCall(ApiName.CREATE_CREDENTIAL, - /* apiStatus */ ApiStatus.METRICS_API_STATUS_FAILURE); + /* apiStatus */ ApiStatus.FAILURE); } } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 41ae9118d965..9c870050449c 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -36,15 +36,14 @@ import android.credentials.CreateCredentialRequest; import android.credentials.CredentialDescription; import android.credentials.CredentialManager; import android.credentials.CredentialOption; +import android.credentials.CredentialProviderInfo; import android.credentials.GetCredentialException; import android.credentials.GetCredentialRequest; import android.credentials.IClearCredentialStateCallback; import android.credentials.ICreateCredentialCallback; import android.credentials.ICredentialManager; import android.credentials.IGetCredentialCallback; -import android.credentials.IListEnabledProvidersCallback; import android.credentials.ISetEnabledProvidersCallback; -import android.credentials.ListEnabledProvidersResponse; import android.credentials.RegisterCredentialDescriptionRequest; import android.credentials.UnregisterCredentialDescriptionRequest; import android.credentials.ui.IntentFactory; @@ -56,7 +55,7 @@ import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.Settings; import android.service.credentials.CallingAppInfo; -import android.service.credentials.CredentialProviderInfo; +import android.service.credentials.CredentialProviderInfoFactory; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -118,10 +117,11 @@ public final class CredentialManagerService int resolvedUserId) { List<CredentialManagerServiceImpl> services = new ArrayList<>(); List<CredentialProviderInfo> serviceInfos = - CredentialProviderInfo.getAvailableSystemServices( + CredentialProviderInfoFactory.getAvailableSystemServices( mContext, resolvedUserId, - /* disableSystemAppVerificationForTests= */ false); + /* disableSystemAppVerificationForTests= */ false, + new HashSet<>()); serviceInfos.forEach( info -> { services.add( @@ -222,10 +222,16 @@ public final class CredentialManagerService return hasPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS); } - private void verifyPermission(String permission) throws SecurityException { - if (!hasPermission(permission)) { - throw new SecurityException("Caller is missing permission: " + permission); + private void verifyGetProvidersPermission() throws SecurityException { + if (hasPermission(android.Manifest.permission.QUERY_ALL_PACKAGES)) { + return; } + + if (hasPermission(android.Manifest.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS)) { + return; + } + + throw new SecurityException("Caller is missing permission: QUERY_ALL_PACKAGES or LIST_ENABLED_CREDENTIAL_PROVIDERS"); } private boolean hasPermission(String permission) { @@ -550,40 +556,6 @@ public final class CredentialManagerService providerSessions.forEach(ProviderSession::invokeSession); } - @SuppressWarnings("GuardedBy") // ErrorProne requires listEnabledProviders - // to be guarded by 'service.mLock', which is the same as mLock. - @Override - public ICancellationSignal listEnabledProviders(IListEnabledProvidersCallback callback) { - Log.i(TAG, "listEnabledProviders"); - ICancellationSignal cancelTransport = CancellationSignal.createTransport(); - - if (!hasWriteSecureSettingsPermission()) { - try { - callback.onError( - PERMISSION_DENIED_ERROR, PERMISSION_DENIED_WRITE_SECURE_SETTINGS_ERROR); - } catch (RemoteException e) { - Log.e(TAG, "Issue with invoking response: " + e.getMessage()); - } - return cancelTransport; - } - - List<String> enabledProviders = new ArrayList<>(); - runForUser( - (service) -> { - enabledProviders.add(service.getComponentName().flattenToString()); - }); - - // Call the callback. - try { - callback.onResponse(ListEnabledProvidersResponse.create(enabledProviders)); - } catch (RemoteException e) { - Log.i(TAG, "Issue with invoking response: " + e.getMessage()); - // TODO: Propagate failure - } - - return cancelTransport; - } - @Override public void setEnabledProviders( List<String> providers, int userId, ISetEnabledProvidersCallback callback) { @@ -659,7 +631,7 @@ public final class CredentialManagerService // The component name and the package name do not match. MetricUtilities.logApiCalled( ApiName.IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE, - ApiStatus.METRICS_API_STATUS_FAILURE, callingUid); + ApiStatus.FAILURE, callingUid); Log.w( TAG, "isEnabledCredentialProviderService: Component name does not" @@ -667,7 +639,7 @@ public final class CredentialManagerService return false; } MetricUtilities.logApiCalled(ApiName.IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE, - ApiStatus.METRICS_API_STATUS_SUCCESS, callingUid); + ApiStatus.SUCCESS, callingUid); return true; } } @@ -677,20 +649,35 @@ public final class CredentialManagerService } @Override - public List<ServiceInfo> getCredentialProviderServices( - int userId, boolean disableSystemAppVerificationForTests, int providerFilter) { + public List<CredentialProviderInfo> getCredentialProviderServices( + int userId, int providerFilter) { Log.i(TAG, "getCredentialProviderServices"); - verifyPermission(android.Manifest.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS); - - List<ServiceInfo> services = new ArrayList<>(); - List<CredentialProviderInfo> providers = - CredentialProviderInfo.getCredentialProviderServices( - mContext, userId, disableSystemAppVerificationForTests, providerFilter); - for (CredentialProviderInfo p : providers) { - services.add(p.getServiceInfo()); - } + verifyGetProvidersPermission(); - return services; + return CredentialProviderInfoFactory.getCredentialProviderServices( + mContext, userId, providerFilter, getEnabledProviders()); + } + + @Override + public List<CredentialProviderInfo> getCredentialProviderServicesForTesting( + int providerFilter) { + Log.i(TAG, "getCredentialProviderServicesForTesting"); + verifyGetProvidersPermission(); + + final int userId = UserHandle.getCallingUserId(); + return CredentialProviderInfoFactory.getCredentialProviderServicesForTesting( + mContext, userId, providerFilter, getEnabledProviders()); + } + + private Set<ServiceInfo> getEnabledProviders() { + Set<ServiceInfo> enabledProviders = new HashSet<>(); + synchronized (mLock) { + runForUser( + (service) -> { + enabledProviders.add(service.getCredentialProviderInfo().getServiceInfo()); + }); + } + return enabledProviders; } @Override @@ -828,11 +815,11 @@ public final class CredentialManagerService } private List<CredentialProviderInfo> getServicesForCredentialDescription(int userId) { - return CredentialProviderInfo.getCredentialProviderServices( + return CredentialProviderInfoFactory.getCredentialProviderServices( mContext, userId, - /* disableSystemAppVerificationForTests= */ false, - CredentialManager.PROVIDER_FILTER_ALL_PROVIDERS); + CredentialManager.PROVIDER_FILTER_ALL_PROVIDERS, + new HashSet<>()); } } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index 546c48fe05f4..ee55a1ccc357 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -21,7 +21,8 @@ import android.annotation.Nullable; import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -import android.service.credentials.CredentialProviderInfo; +import android.credentials.CredentialProviderInfo; +import android.service.credentials.CredentialProviderInfoFactory; import android.util.Log; import android.util.Slog; @@ -82,7 +83,7 @@ public final class CredentialManagerServiceImpl extends Log.i(TAG, "newServiceInfoLocked with null mInfo , " + serviceComponent.getPackageName()); } - mInfo = new CredentialProviderInfo( + mInfo = CredentialProviderInfoFactory.create( getContext(), serviceComponent, mUserId, /*isSystemProvider=*/false); return mInfo.getServiceInfo(); diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index 2c6c0d8b4018..546c37ff95af 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ServiceInfo; import android.credentials.CredentialManager; +import android.credentials.CredentialProviderInfo; import android.credentials.ui.DisabledProviderData; import android.credentials.ui.IntentFactory; import android.credentials.ui.ProviderData; @@ -31,11 +32,12 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.ResultReceiver; -import android.service.credentials.CredentialProviderInfo; +import android.service.credentials.CredentialProviderInfoFactory; import android.util.Log; import android.util.Slog; import java.util.ArrayList; +import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -118,11 +120,11 @@ public class CredentialManagerUi { .map(ProviderData::getProviderFlattenedComponentName) .collect(Collectors.toUnmodifiableSet()); Set<String> allProviders = - CredentialProviderInfo.getCredentialProviderServices( + CredentialProviderInfoFactory.getCredentialProviderServices( mContext, mUserId, - /* disableSystemAppVerificationForTests= */ false, - CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY) + CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY, + new HashSet<>()) .stream() .map(CredentialProviderInfo::getServiceInfo) .map(ServiceInfo::getComponentName) diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index ce26c885d55f..8c6e5cea3bba 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -16,12 +16,10 @@ package com.android.server.credentials; -import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_FINAL_FAILURE; -import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_FINAL_SUCCESS; - import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; +import android.credentials.CredentialProviderInfo; import android.credentials.GetCredentialException; import android.credentials.GetCredentialRequest; import android.credentials.GetCredentialResponse; @@ -31,11 +29,11 @@ import android.credentials.ui.RequestInfo; import android.os.CancellationSignal; import android.os.RemoteException; import android.service.credentials.CallingAppInfo; -import android.service.credentials.CredentialProviderInfo; import android.util.Log; import com.android.server.credentials.metrics.ApiName; import com.android.server.credentials.metrics.ApiStatus; +import com.android.server.credentials.metrics.ProviderStatusForMetrics; import java.util.ArrayList; @@ -95,11 +93,11 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest setChosenMetric(componentName); if (response != null) { mChosenProviderMetric.setChosenProviderStatus( - METRICS_PROVIDER_STATUS_FINAL_SUCCESS); + ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode()); respondToClientWithResponseAndFinish(response); } else { mChosenProviderMetric.setChosenProviderStatus( - METRICS_PROVIDER_STATUS_FINAL_FAILURE); + ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode()); respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, "Invalid response from provider"); } @@ -119,20 +117,19 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest return; } if (isSessionCancelled()) { - // TODO: Differentiate btw cancelled and false logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_CLIENT_CANCELED); + ApiStatus.CLIENT_CANCELED); finishSession(/*propagateCancellation=*/true); return; } try { mClientCallback.onResponse(response); logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_SUCCESS); + ApiStatus.SUCCESS); } catch (RemoteException e) { Log.i(TAG, "Issue while responding to client with a response : " + e.getMessage()); logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_FAILURE); + ApiStatus.FAILURE); } finishSession(/*propagateCancellation=*/false); } @@ -144,7 +141,7 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest } if (isSessionCancelled()) { logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ - ApiStatus.METRICS_API_STATUS_CLIENT_CANCELED); + ApiStatus.CLIENT_CANCELED); finishSession(/*propagateCancellation=*/true); return; } @@ -161,10 +158,10 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest private void logFailureOrUserCancel(String errorType) { if (GetCredentialException.TYPE_USER_CANCELED.equals(errorType)) { logApiCall(ApiName.GET_CREDENTIAL, - /* apiStatus */ ApiStatus.METRICS_API_STATUS_USER_CANCELED); + /* apiStatus */ ApiStatus.USER_CANCELED); } else { logApiCall(ApiName.GET_CREDENTIAL, - /* apiStatus */ ApiStatus.METRICS_API_STATUS_FAILURE); + /* apiStatus */ ApiStatus.FAILURE); } } diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index f75a9b685d10..880ae6d4201e 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -16,12 +16,6 @@ package com.android.server.credentials; -import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_FINAL_FAILURE; -import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_FINAL_SUCCESS; -import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_QUERY_FAILURE; -import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_QUERY_SUCCESS; -import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_UNKNOWN; - import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; @@ -43,20 +37,8 @@ public class MetricUtilities { private static final String TAG = "MetricUtilities"; - private static final int DEFAULT_INT_32 = -1; - private static final int[] DEFAULT_REPEATED_INT_32 = new int[0]; - - // Metrics constants TODO(b/269290341) migrate to enums eventually to improve - protected static final int METRICS_PROVIDER_STATUS_FINAL_FAILURE = - CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_FINAL_FAILURE; - protected static final int METRICS_PROVIDER_STATUS_QUERY_FAILURE = - CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_QUERY_FAILURE; - protected static final int METRICS_PROVIDER_STATUS_FINAL_SUCCESS = - CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_FINAL_SUCCESS; - protected static final int METRICS_PROVIDER_STATUS_QUERY_SUCCESS = - CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_QUERY_SUCCESS; - protected static final int METRICS_PROVIDER_STATUS_UNKNOWN = - CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_UNKNOWN; + public static final int DEFAULT_INT_32 = -1; + public static final int[] DEFAULT_REPEATED_INT_32 = new int[0]; /** @@ -80,6 +62,21 @@ public class MetricUtilities { } /** + * Given any two timestamps in nanoseconds, this gets the difference and converts to + * milliseconds. Assumes the difference is not larger than the maximum int size. + * + * @param t2 the final timestamp + * @param t1 the initial timestamp + * @return the timestamp difference converted to microseconds + */ + protected static int getMetricTimestampDifferenceMicroseconds(long t2, long t1) { + if (t2 - t1 > Integer.MAX_VALUE) { + throw new ArithmeticException("Input timestamps are too far apart and unsupported"); + } + return (int) ((t2 - t1) / 1000); + } + + /** * The most common logging helper, handles the overall status of the API request with the * provider status and latencies. Other versions of this method may be more useful depending * on the situation, as this is geared towards the logging of {@link ProviderSession} types. @@ -102,7 +99,7 @@ public class MetricUtilities { for (var session : providerSessions) { CandidateProviderMetric metric = session.mCandidateProviderMetric; candidateUidList[index] = metric.getCandidateUid(); - candidateQueryRoundTripTimeList[index] = metric.getQueryLatencyMs(); + candidateQueryRoundTripTimeList[index] = metric.getQueryLatencyMicroseconds(); candidateStatusList[index] = metric.getProviderQueryStatus(); index++; } @@ -116,9 +113,11 @@ public class MetricUtilities { /* repeated_candidate_provider_status */ candidateStatusList, /* chosen_provider_uid */ chosenProviderMetric.getChosenUid(), /* chosen_provider_round_trip_time_overall_microseconds */ - chosenProviderMetric.getEntireProviderLatencyMs(), - /* chosen_provider_final_phase_microseconds */ - chosenProviderMetric.getFinalPhaseLatencyMs(), + chosenProviderMetric.getEntireProviderLatencyMicroseconds(), + /* chosen_provider_final_phase_microseconds (backwards compat only) */ + getMetricTimestampDifferenceMicroseconds(chosenProviderMetric + .getFinalFinishTimeNanoseconds(), + chosenProviderMetric.getUiCallEndTimeNanoseconds()), /* chosen_provider_status */ chosenProviderMetric.getChosenProviderStatus()); } diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java index ce9fca753a06..b7a4cd581a1a 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java @@ -20,11 +20,11 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.Context; import android.credentials.ClearCredentialStateException; +import android.credentials.CredentialProviderInfo; import android.credentials.ui.ProviderData; import android.credentials.ui.ProviderPendingIntentResponse; import android.service.credentials.CallingAppInfo; import android.service.credentials.ClearCredentialStateRequest; -import android.service.credentials.CredentialProviderInfo; import android.util.Log; import android.util.Slog; @@ -119,8 +119,8 @@ public final class ProviderClearSession extends ProviderSession<ClearCredential @Override protected void invokeSession() { if (mRemoteCredentialService != null) { + mCandidateProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime()); mRemoteCredentialService.onClearCredentialState(mProviderRequest, this); - mCandidateProviderMetric.setStartTimeNanoseconds(System.nanoTime()); } } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 97b78116bbfa..640cc3331b1a 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -24,6 +24,7 @@ import android.content.Context; import android.content.Intent; import android.credentials.CreateCredentialException; import android.credentials.CreateCredentialResponse; +import android.credentials.CredentialProviderInfo; import android.credentials.ui.CreateCredentialProviderData; import android.credentials.ui.Entry; import android.credentials.ui.ProviderPendingIntentResponse; @@ -33,7 +34,6 @@ import android.service.credentials.BeginCreateCredentialResponse; import android.service.credentials.CallingAppInfo; import android.service.credentials.CreateCredentialRequest; import android.service.credentials.CreateEntry; -import android.service.credentials.CredentialProviderInfo; import android.service.credentials.CredentialProviderService; import android.service.credentials.RemoteEntry; import android.util.Log; @@ -137,7 +137,8 @@ public final class ProviderCreateSession extends ProviderSession< remoteCredentialService); mCompleteRequest = completeCreateRequest; setStatus(Status.PENDING); - mProviderResponseDataHandler = new ProviderResponseDataHandler(hybridService); + mProviderResponseDataHandler = new ProviderResponseDataHandler( + ComponentName.unflattenFromString(hybridService)); } @Override @@ -225,8 +226,8 @@ public final class ProviderCreateSession extends ProviderSession< @Override protected void invokeSession() { if (mRemoteCredentialService != null) { + mCandidateProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime()); mRemoteCredentialService.onCreateCredential(mProviderRequest, this); - mCandidateProviderMetric.setStartTimeNanoseconds(System.nanoTime()); } } @@ -297,21 +298,23 @@ public final class ProviderCreateSession extends ProviderSession< } private class ProviderResponseDataHandler { - private final ComponentName mExpectedRemoteEntryProviderService; + @Nullable private final ComponentName mExpectedRemoteEntryProviderService; @NonNull private final Map<String, Pair<CreateEntry, Entry>> mUiCreateEntries = new HashMap<>(); @Nullable private Pair<String, Pair<RemoteEntry, Entry>> mUiRemoteEntry = null; - ProviderResponseDataHandler(String hybridService) { - mExpectedRemoteEntryProviderService = ComponentName.unflattenFromString(hybridService); + ProviderResponseDataHandler(@Nullable ComponentName expectedRemoteEntryProviderService) { + mExpectedRemoteEntryProviderService = expectedRemoteEntryProviderService; } public void addResponseContent(List<CreateEntry> createEntries, RemoteEntry remoteEntry) { createEntries.forEach(this::addCreateEntry); - setRemoteEntry(remoteEntry); + if (remoteEntry != null) { + setRemoteEntry(remoteEntry); + } } public void addCreateEntry(CreateEntry createEntry) { String id = generateUniqueId(); @@ -321,13 +324,13 @@ public final class ProviderCreateSession extends ProviderSession< } public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) { - if (remoteEntry == null) { - mUiRemoteEntry = null; + if (!enforceRemoteEntryRestrictions(mExpectedRemoteEntryProviderService)) { + Log.i(TAG, "Remote entry being dropped as it does not meet the restriction" + + "checks."); return; } - if (!mComponentName.equals(mExpectedRemoteEntryProviderService)) { - Log.i(TAG, "Remote entry being dropped as it is not from the service " - + "configured by the OEM."); + if (remoteEntry == null) { + mUiRemoteEntry = null; return; } String id = generateUniqueId(); diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index ee813e90ddfe..07e2f877bfc0 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -23,6 +23,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.credentials.CredentialOption; +import android.credentials.CredentialProviderInfo; import android.credentials.GetCredentialException; import android.credentials.GetCredentialResponse; import android.credentials.ui.AuthenticationEntry; @@ -35,7 +36,6 @@ import android.service.credentials.BeginGetCredentialRequest; import android.service.credentials.BeginGetCredentialResponse; import android.service.credentials.CallingAppInfo; import android.service.credentials.CredentialEntry; -import android.service.credentials.CredentialProviderInfo; import android.service.credentials.CredentialProviderService; import android.service.credentials.GetCredentialRequest; import android.service.credentials.RemoteEntry; @@ -181,7 +181,6 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential /** Called when the provider response has been updated by an external source. */ @Override // Callback from the remote provider public void onProviderResponseSuccess(@Nullable BeginGetCredentialResponse response) { - Log.i(TAG, "in onProviderResponseSuccess"); onSetInitialRemoteResponse(response); } @@ -269,8 +268,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential @Override protected void invokeSession() { if (mRemoteCredentialService != null) { + mCandidateProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime()); mRemoteCredentialService.onBeginGetCredential(mProviderRequest, this); - mCandidateProviderMetric.setStartTimeNanoseconds(System.nanoTime()); } } @@ -393,7 +392,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential .extractResponseContent(providerPendingIntentResponse .getResultData()); if (response != null && !mProviderResponseDataHandler.isEmptyResponse(response)) { - addToInitialRemoteResponse(response); + addToInitialRemoteResponse(response, /*isInitialResponse=*/ false); // Additional content received is in the form of new response content. return true; } @@ -401,7 +400,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential return false; } - private void addToInitialRemoteResponse(BeginGetCredentialResponse content) { + private void addToInitialRemoteResponse(BeginGetCredentialResponse content, + boolean isInitialResponse) { if (content == null) { return; } @@ -409,7 +409,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential content.getCredentialEntries(), content.getActions(), content.getAuthenticationActions(), - content.getRemoteCredentialEntry() + content.getRemoteCredentialEntry(), + isInitialResponse ); } @@ -424,7 +425,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential /** Updates the response being maintained in state by this provider session. */ private void onSetInitialRemoteResponse(BeginGetCredentialResponse response) { mProviderResponse = response; - addToInitialRemoteResponse(response); + addToInitialRemoteResponse(response, /*isInitialResponse=*/true); if (mProviderResponseDataHandler.isEmptyResponse(response)) { updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE); return; @@ -463,7 +464,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential } private class ProviderResponseDataHandler { - private final ComponentName mExpectedRemoteEntryProviderService; + @Nullable private final ComponentName mExpectedRemoteEntryProviderService; @NonNull private final Map<String, Pair<CredentialEntry, Entry>> mUiCredentialEntries = new HashMap<>(); @@ -475,26 +476,33 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential @Nullable private Pair<String, Pair<RemoteEntry, Entry>> mUiRemoteEntry = null; - ProviderResponseDataHandler(ComponentName expectedRemoteEntryProviderService) { + ProviderResponseDataHandler(@Nullable ComponentName expectedRemoteEntryProviderService) { mExpectedRemoteEntryProviderService = expectedRemoteEntryProviderService; } public void addResponseContent(List<CredentialEntry> credentialEntries, List<Action> actions, List<Action> authenticationActions, - RemoteEntry remoteEntry) { + RemoteEntry remoteEntry, boolean isInitialResponse) { credentialEntries.forEach(this::addCredentialEntry); actions.forEach(this::addAction); authenticationActions.forEach( authenticationAction -> addAuthenticationAction(authenticationAction, AuthenticationEntry.STATUS_LOCKED)); - setRemoteEntry(remoteEntry); + // In the query phase, it is likely most providers will return a null remote entry + // so no need to invoke the setter since it adds the overhead of checking for the + // hybrid permission, and then sets an already null value to null. + // If this is not the query phase, e.g. response after a locked entry is unlocked + // then it is valid for the provider to remove the remote entry, and so we allow + // them to set it to null. + if (remoteEntry != null || !isInitialResponse) { + setRemoteEntry(remoteEntry); + } } public void addCredentialEntry(CredentialEntry credentialEntry) { String id = generateUniqueId(); Entry entry = new Entry(CREDENTIAL_ENTRY_KEY, id, credentialEntry.getSlice(), - setUpFillInIntent(credentialEntry - .getBeginGetCredentialOption().getId())); + setUpFillInIntent(credentialEntry.getBeginGetCredentialOptionId())); mUiCredentialEntries.put(id, new Pair<>(credentialEntry, entry)); } @@ -524,12 +532,13 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential } public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) { - if (remoteEntry == null) { + if (!enforceRemoteEntryRestrictions(mExpectedRemoteEntryProviderService)) { + Log.i(TAG, "Remote entry being dropped as it does not meet the restriction" + + " checks."); return; } - if (!mComponentName.equals(mExpectedRemoteEntryProviderService)) { - Log.i(TAG, "Remote entry being dropped as it is not from the service " - + "configured by the OEM."); + if (remoteEntry == null) { + mUiRemoteEntry = null; return; } String id = generateUniqueId(); @@ -538,6 +547,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential mUiRemoteEntry = new Pair<>(generateUniqueId(), new Pair<>(remoteEntry, entry)); } + + public GetCredentialProviderData toGetCredentialProviderData() { return new GetCredentialProviderData.Builder( mComponentName.flattenToString()).setActionChips(prepareActionEntries()) @@ -571,7 +582,6 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential return credEntries; } - private Entry prepareRemoteEntry() { if (mUiRemoteEntry == null || mUiRemoteEntry.first == null || mUiRemoteEntry.second == null) { diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index ecddcf30f88d..a85769572972 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -16,22 +16,23 @@ package com.android.server.credentials; -import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_QUERY_FAILURE; -import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_QUERY_SUCCESS; - +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.credentials.Credential; +import android.credentials.CredentialProviderInfo; import android.credentials.ui.ProviderData; import android.credentials.ui.ProviderPendingIntentResponse; import android.os.ICancellationSignal; import android.os.RemoteException; -import android.service.credentials.CredentialProviderInfo; import android.util.Log; import com.android.server.credentials.metrics.CandidateProviderMetric; +import com.android.server.credentials.metrics.ProviderStatusForMetrics; import java.util.UUID; @@ -202,9 +203,11 @@ public abstract class ProviderSession<T, R> mCandidateProviderMetric .setQueryFinishTimeNanoseconds(System.nanoTime()); if (isTerminatingStatus(status)) { - mCandidateProviderMetric.setProviderQueryStatus(METRICS_PROVIDER_STATUS_QUERY_FAILURE); + mCandidateProviderMetric.setProviderQueryStatus(ProviderStatusForMetrics.QUERY_FAILURE + .getMetricCode()); } else if (isCompletionStatus(status)) { - mCandidateProviderMetric.setProviderQueryStatus(METRICS_PROVIDER_STATUS_QUERY_SUCCESS); + mCandidateProviderMetric.setProviderQueryStatus(ProviderStatusForMetrics.QUERY_SUCCESS + .getMetricCode()); } } @@ -228,6 +231,39 @@ public abstract class ProviderSession<T, R> return mProviderResponse; } + protected boolean enforceRemoteEntryRestrictions( + @Nullable ComponentName expectedRemoteEntryProviderService) { + // Check if the service is the one set by the OEM. If not silently reject this entry + if (!mComponentName.equals(expectedRemoteEntryProviderService)) { + Log.i(TAG, "Remote entry being dropped as it is not from the service " + + "configured by the OEM."); + return false; + } + // Check if the service has the hybrid permission .If not, silently reject this entry. + // This check is in addition to the permission check happening in the provider's process. + try { + ApplicationInfo appInfo = mContext.getPackageManager().getApplicationInfo( + mComponentName.getPackageName(), + PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY)); + if (appInfo != null + && mContext.checkPermission( + Manifest.permission.PROVIDE_REMOTE_CREDENTIALS, + /*pId=*/-1, appInfo.uid) == PackageManager.PERMISSION_GRANTED) { + return true; + } + } catch (SecurityException e) { + Log.i(TAG, "Error getting info for " + + mComponentName.flattenToString() + ": " + e.getMessage()); + return false; + } catch (PackageManager.NameNotFoundException e) { + Log.i(TAG, "Error getting info for " + + mComponentName.flattenToString() + ": " + e.getMessage()); + return false; + } + Log.i(TAG, "In enforceRemoteEntryRestrictions - remote entry checks fail"); + return false; + } + /** Should be overridden to prepare, and stores state for {@link ProviderData} to be * shown on the UI. */ @Nullable protected abstract ProviderData prepareUiData(); diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java index 702261ea43f5..ff4e3b680131 100644 --- a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java +++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java @@ -121,8 +121,6 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr ProviderCallbacks<BeginGetCredentialResponse> callback) { Log.i(TAG, "In onGetCredentials in RemoteCredentialService"); AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>(); - AtomicReference<CompletableFuture<BeginGetCredentialResponse>> futureRef = - new AtomicReference<>(); CompletableFuture<BeginGetCredentialResponse> connectThenExecute = postAsync(service -> { CompletableFuture<BeginGetCredentialResponse> getCredentials = @@ -134,7 +132,6 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr new IBeginGetCredentialCallback.Stub() { @Override public void onSuccess(BeginGetCredentialResponse response) { - Log.i(TAG, "In onSuccess in RemoteCredentialService"); getCredentials.complete(response); } @@ -147,22 +144,15 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr new GetCredentialException(errorType, errorMsg)); } }); - CompletableFuture<BeginGetCredentialResponse> future = futureRef.get(); - if (future != null && future.isCancelled()) { - dispatchCancellationSignal(cancellationSignal); - } else { - cancellationSink.set(cancellationSignal); - } + cancellationSink.set(cancellationSignal); return getCredentials; } finally { Binder.restoreCallingIdentity(originalCallingUidToken); } }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS); - futureRef.set(connectThenExecute); connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> handleExecutionResponse(result, error, cancellationSink, callback))); - return cancellationSink.get(); } @@ -178,8 +168,6 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr ProviderCallbacks<BeginCreateCredentialResponse> callback) { Log.i(TAG, "In onCreateCredential in RemoteCredentialService"); AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>(); - AtomicReference<CompletableFuture<BeginCreateCredentialResponse>> futureRef = - new AtomicReference<>(); CompletableFuture<BeginCreateCredentialResponse> connectThenExecute = postAsync(service -> { @@ -205,19 +193,13 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr new CreateCredentialException(errorType, errorMsg)); } }); - CompletableFuture<BeginCreateCredentialResponse> future = futureRef.get(); - if (future != null && future.isCancelled()) { - dispatchCancellationSignal(cancellationSignal); - } else { - cancellationSink.set(cancellationSignal); - } + cancellationSink.set(cancellationSignal); return createCredentialFuture; } finally { Binder.restoreCallingIdentity(originalCallingUidToken); } }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS); - futureRef.set(connectThenExecute); connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> handleExecutionResponse(result, error, cancellationSink, callback))); @@ -236,7 +218,6 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr ProviderCallbacks<Void> callback) { Log.i(TAG, "In onClearCredentialState in RemoteCredentialService"); AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>(); - AtomicReference<CompletableFuture<Void>> futureRef = new AtomicReference<>(); CompletableFuture<Void> connectThenExecute = postAsync(service -> { @@ -263,19 +244,13 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr errorMsg)); } }); - CompletableFuture<Void> future = futureRef.get(); - if (future != null && future.isCancelled()) { - dispatchCancellationSignal(cancellationSignal); - } else { - cancellationSink.set(cancellationSignal); - } + cancellationSink.set(cancellationSignal); return clearCredentialFuture; } finally { Binder.restoreCallingIdentity(originalCallingUidToken); } }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS); - futureRef.set(connectThenExecute); connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> handleExecutionResponse(result, error, cancellationSink, callback))); diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index 86e05cf62f5f..c1f35d0f8195 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; +import android.credentials.CredentialProviderInfo; import android.credentials.ui.ProviderData; import android.credentials.ui.UserSelectionDialogResult; import android.os.Binder; @@ -30,7 +31,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.service.credentials.CallingAppInfo; -import android.service.credentials.CredentialProviderInfo; import android.util.Log; import com.android.internal.R; @@ -78,7 +78,8 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan //TODO improve design to allow grouped metrics per request protected final String mHybridService; - @NonNull protected RequestSessionStatus mRequestSessionStatus = + @NonNull + protected RequestSessionStatus mRequestSessionStatus = RequestSessionStatus.IN_PROGRESS; /** The status in which a given request session is. */ @@ -213,6 +214,7 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan /** * Called by RequestSession's upon chosen metric determination. + * * @param componentName the componentName to associate with a provider */ protected void setChosenMetric(ComponentName componentName) { @@ -220,8 +222,8 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan .mCandidateProviderMetric; mChosenProviderMetric.setChosenUid(metric.getCandidateUid()); mChosenProviderMetric.setFinalFinishTimeNanoseconds(System.nanoTime()); - mChosenProviderMetric.setQueryFinishTimeNanoseconds( - metric.getQueryFinishTimeNanoseconds()); - mChosenProviderMetric.setStartTimeNanoseconds(metric.getStartTimeNanoseconds()); + mChosenProviderMetric.setQueryPhaseLatencyMicroseconds( + metric.getQueryLatencyMicroseconds()); + mChosenProviderMetric.setQueryStartTimeNanoseconds(metric.getStartQueryTimeNanoseconds()); } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java b/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java index 36a1f2df6d24..22cab707387d 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java @@ -22,11 +22,11 @@ import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_USER_CANCELED; public enum ApiStatus { - METRICS_API_STATUS_SUCCESS(CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_SUCCESS), - METRICS_API_STATUS_FAILURE(CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_FAILURE), - METRICS_API_STATUS_CLIENT_CANCELED( + SUCCESS(CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_SUCCESS), + FAILURE(CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_FAILURE), + CLIENT_CANCELED( CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_CLIENT_CANCELED), - METRICS_API_STATUS_USER_CANCELED( + USER_CANCELED( CREDENTIAL_MANAGER_API_CALLED__API_STATUS__API_STATUS_USER_CANCELED); private final int mInnerMetricCode; diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidateProviderMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidateProviderMetric.java index acfb4a4e3e39..9f438ecc1146 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/CandidateProviderMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/CandidateProviderMetric.java @@ -18,63 +18,72 @@ package com.android.server.credentials.metrics; /** * The central candidate provider metric object that mimics our defined metric setup. + * Some types are redundant across these metric collectors, but that has debug use-cases as + * these data-types are available at different moments of the flow (and typically, one can feed + * into the next). + * TODO(b/270403549) - iterate on this in V3+ */ public class CandidateProviderMetric { + private static final String TAG = "CandidateProviderMetric"; private int mCandidateUid = -1; - private long mStartTimeNanoseconds = -1; + + // Raw timestamp in nanoseconds, will be converted to microseconds for logging + + private long mStartQueryTimeNanoseconds = -1; private long mQueryFinishTimeNanoseconds = -1; private int mProviderQueryStatus = -1; - public CandidateProviderMetric(long startTime, long queryFinishTime, int providerQueryStatus, - int candidateUid) { - this.mStartTimeNanoseconds = startTime; - this.mQueryFinishTimeNanoseconds = queryFinishTime; - this.mProviderQueryStatus = providerQueryStatus; - this.mCandidateUid = candidateUid; + public CandidateProviderMetric() { } - public CandidateProviderMetric(){} + /* ---------- Latencies ---------- */ - public void setStartTimeNanoseconds(long startTimeNanoseconds) { - this.mStartTimeNanoseconds = startTimeNanoseconds; + public void setStartQueryTimeNanoseconds(long startQueryTimeNanoseconds) { + this.mStartQueryTimeNanoseconds = startQueryTimeNanoseconds; } public void setQueryFinishTimeNanoseconds(long queryFinishTimeNanoseconds) { this.mQueryFinishTimeNanoseconds = queryFinishTimeNanoseconds; } - public void setProviderQueryStatus(int providerQueryStatus) { - this.mProviderQueryStatus = providerQueryStatus; + public long getStartQueryTimeNanoseconds() { + return this.mStartQueryTimeNanoseconds; } - public void setCandidateUid(int candidateUid) { - this.mCandidateUid = candidateUid; + public long getQueryFinishTimeNanoseconds() { + return this.mQueryFinishTimeNanoseconds; } - public long getStartTimeNanoseconds() { - return this.mStartTimeNanoseconds; + /** + * Returns the latency in microseconds for the query phase. + */ + public int getQueryLatencyMicroseconds() { + return (int) ((this.getQueryFinishTimeNanoseconds() + - this.getStartQueryTimeNanoseconds()) / 1000); } - public long getQueryFinishTimeNanoseconds() { - return this.mQueryFinishTimeNanoseconds; + // TODO (in direct next dependent CL, so this is transient) - add reference timestamp in micro + // seconds for this too. + + /* ------------- Provider Query Status ------------ */ + + public void setProviderQueryStatus(int providerQueryStatus) { + this.mProviderQueryStatus = providerQueryStatus; } public int getProviderQueryStatus() { return this.mProviderQueryStatus; } - public int getCandidateUid() { - return this.mCandidateUid; - } + /* -------------- Candidate Uid ---------------- */ - /** - * Returns the latency in microseconds for the query phase. - */ - public int getQueryLatencyMs() { - return (int) ((this.getQueryFinishTimeNanoseconds() - - this.getStartTimeNanoseconds()) / 1000); + public void setCandidateUid(int candidateUid) { + this.mCandidateUid = candidateUid; } + public int getCandidateUid() { + return this.mCandidateUid; + } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderMetric.java b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderMetric.java index c4d0b3c7254d..03102558d21b 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderMetric.java @@ -16,18 +16,42 @@ package com.android.server.credentials.metrics; +import android.util.Log; + +import com.android.server.credentials.MetricUtilities; + /** * The central chosen provider metric object that mimics our defined metric setup. + * Some types are redundant across these metric collectors, but that has debug use-cases as + * these data-types are available at different moments of the flow (and typically, one can feed + * into the next). + * TODO(b/270403549) - iterate on this in V3+ */ public class ChosenProviderMetric { + // TODO(b/270403549) - applies elsewhere, likely removed or replaced with a count-index (1,2,3) + private static final String TAG = "ChosenProviderMetric"; private int mChosenUid = -1; - private long mStartTimeNanoseconds = -1; - private long mQueryFinishTimeNanoseconds = -1; + + // Latency figures typically fed in from prior CandidateProviderMetric + + private int mPreQueryPhaseLatencyMicroseconds = -1; + private int mQueryPhaseLatencyMicroseconds = -1; + + // Timestamps kept in raw nanoseconds. Expected to be converted to microseconds from using + // reference 'mServiceBeganTimeNanoseconds' during metric log point. + + private long mServiceBeganTimeNanoseconds = -1; + private long mQueryStartTimeNanoseconds = -1; + private long mUiCallStartTimeNanoseconds = -1; + private long mUiCallEndTimeNanoseconds = -1; private long mFinalFinishTimeNanoseconds = -1; private int mChosenProviderStatus = -1; - public ChosenProviderMetric() {} + public ChosenProviderMetric() { + } + + /* ------------------- UID ------------------- */ public int getChosenUid() { return mChosenUid; @@ -37,54 +61,143 @@ public class ChosenProviderMetric { mChosenUid = chosenUid; } - public long getStartTimeNanoseconds() { - return mStartTimeNanoseconds; + /* ---------------- Latencies ------------------ */ + + + /* ----- Direct Latencies ------- */ + + /** + * In order for a chosen provider to be selected, the call must have successfully begun. + * Thus, the {@link InitialPhaseMetric} can directly pass this initial latency figure into + * this chosen provider metric. + * + * @param preQueryPhaseLatencyMicroseconds the millisecond latency for the service start, + * typically passed in through the + * {@link InitialPhaseMetric} + */ + public void setPreQueryPhaseLatencyMicroseconds(int preQueryPhaseLatencyMicroseconds) { + mPreQueryPhaseLatencyMicroseconds = preQueryPhaseLatencyMicroseconds; + } + + /** + * In order for a chosen provider to be selected, a candidate provider must exist. The + * candidate provider can directly pass the final latency figure into this chosen provider + * metric. + * + * @param queryPhaseLatencyMicroseconds the millisecond latency for the query phase, typically + * passed in through the {@link CandidateProviderMetric} + */ + public void setQueryPhaseLatencyMicroseconds(int queryPhaseLatencyMicroseconds) { + mQueryPhaseLatencyMicroseconds = queryPhaseLatencyMicroseconds; } - public void setStartTimeNanoseconds(long startTimeNanoseconds) { - mStartTimeNanoseconds = startTimeNanoseconds; + public int getPreQueryPhaseLatencyMicroseconds() { + return mPreQueryPhaseLatencyMicroseconds; } - public long getQueryFinishTimeNanoseconds() { - return mQueryFinishTimeNanoseconds; + public int getQueryPhaseLatencyMicroseconds() { + return mQueryPhaseLatencyMicroseconds; } - public void setQueryFinishTimeNanoseconds(long queryFinishTimeNanoseconds) { - mQueryFinishTimeNanoseconds = queryFinishTimeNanoseconds; + public int getUiPhaseLatencyMicroseconds() { + return (int) ((this.mUiCallEndTimeNanoseconds + - this.mUiCallStartTimeNanoseconds) / 1000); } - public long getFinalFinishTimeNanoseconds() { - return mFinalFinishTimeNanoseconds; + /** + * Returns the full provider (invocation to response) latency in microseconds. Expects the + * start time to be provided, such as from {@link CandidateProviderMetric}. + */ + public int getEntireProviderLatencyMicroseconds() { + return (int) ((this.mFinalFinishTimeNanoseconds + - this.mQueryStartTimeNanoseconds) / 1000); + } + + /** + * Returns the full (platform invoked to response) latency in microseconds. Expects the + * start time to be provided, such as from {@link InitialPhaseMetric}. + */ + public int getEntireLatencyMicroseconds() { + return (int) ((this.mFinalFinishTimeNanoseconds + - this.mServiceBeganTimeNanoseconds) / 1000); + } + + /* ----- Timestamps for Latency ----- */ + + /** + * In order for a chosen provider to be selected, the call must have successfully begun. + * Thus, the {@link InitialPhaseMetric} can directly pass this initial timestamp into this + * chosen provider metric. + * + * @param serviceBeganTimeNanoseconds the timestamp moment when the platform was called, + * typically passed in through the {@link InitialPhaseMetric} + */ + public void setServiceBeganTimeNanoseconds(long serviceBeganTimeNanoseconds) { + mServiceBeganTimeNanoseconds = serviceBeganTimeNanoseconds; + } + + public void setQueryStartTimeNanoseconds(long queryStartTimeNanoseconds) { + mQueryStartTimeNanoseconds = queryStartTimeNanoseconds; + } + + public void setUiCallStartTimeNanoseconds(long uiCallStartTimeNanoseconds) { + this.mUiCallStartTimeNanoseconds = uiCallStartTimeNanoseconds; + } + + public void setUiCallEndTimeNanoseconds(long uiCallEndTimeNanoseconds) { + this.mUiCallEndTimeNanoseconds = uiCallEndTimeNanoseconds; } public void setFinalFinishTimeNanoseconds(long finalFinishTimeNanoseconds) { mFinalFinishTimeNanoseconds = finalFinishTimeNanoseconds; } - public int getChosenProviderStatus() { - return mChosenProviderStatus; + public long getServiceBeganTimeNanoseconds() { + return mServiceBeganTimeNanoseconds; } - public void setChosenProviderStatus(int chosenProviderStatus) { - mChosenProviderStatus = chosenProviderStatus; + public long getQueryStartTimeNanoseconds() { + return mQueryStartTimeNanoseconds; } - /** - * Returns the full provider (invocation to response) latency in microseconds. - */ - public int getEntireProviderLatencyMs() { - return (int) ((this.getFinalFinishTimeNanoseconds() - - this.getStartTimeNanoseconds()) / 1000); + public long getUiCallStartTimeNanoseconds() { + return mUiCallStartTimeNanoseconds; } - // TODO get post click final phase and re-add the query phase time to metric + public long getUiCallEndTimeNanoseconds() { + return mUiCallEndTimeNanoseconds; + } + + public long getFinalFinishTimeNanoseconds() { + return mFinalFinishTimeNanoseconds; + } + + /* --- Time Stamp Conversion to Microseconds --- */ /** - * Returns the end of query to response phase latency in microseconds. + * We collect raw timestamps in nanoseconds for ease of collection. However, given the scope + * of our logging timeframe, and size considerations of the metric, we require these to give us + * the microsecond timestamps from the start reference point. + * + * @param specificTimestamp the timestamp to consider, must be greater than the reference + * @return the microsecond integer timestamp from service start to query began */ - public int getFinalPhaseLatencyMs() { - return (int) ((this.getFinalFinishTimeNanoseconds() - - this.getQueryFinishTimeNanoseconds()) / 1000); + public int getTimestampFromReferenceStartMicroseconds(long specificTimestamp) { + if (specificTimestamp < this.mServiceBeganTimeNanoseconds) { + Log.i(TAG, "The timestamp is before service started, falling back to default int"); + return MetricUtilities.DEFAULT_INT_32; + } + return (int) ((specificTimestamp + - this.mServiceBeganTimeNanoseconds) / 1000); + } + + /* ----------- Provider Status -------------- */ + + public int getChosenProviderStatus() { + return mChosenProviderStatus; } + public void setChosenProviderStatus(int chosenProviderStatus) { + mChosenProviderStatus = chosenProviderStatus; + } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java new file mode 100644 index 000000000000..5f062b05df67 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.credentials.metrics; + +/** + * This handles metrics collected prior to any remote calls to providers. + * Some types are redundant across these metric collectors, but that has debug use-cases as + * these data-types are available at different moments of the flow (and typically, one can feed + * into the next). + * TODO(b/270403549) - iterate on this in V3+ + */ +public class InitialPhaseMetric { + private static final String TAG = "PreCandidateMetric"; + + // The api being called, default set to unknown + private int mApiName = ApiName.UNKNOWN.getMetricCode(); + // The caller uid of the calling application, default to -1 + private int mCallerUid = -1; + // The session id to unite multiple atom emits, default to -1 + private long mSessionId = -1; + // A sequence id to order united emits, default to -1 + private int mSequenceId = -1; + private int mCountRequestClassType = -1; + + // Raw timestamps in nanoseconds, *the only* one logged as such (i.e. 64 bits) since it is a + // reference point. + private long mCredentialServiceStartedTimeNanoseconds = -1; + + // A reference point to give this object utility to capture latency. Can be directly handed + // over to the next latency object. + private long mCredentialServiceBeginQueryTimeNanoseconds = -1; + + + public InitialPhaseMetric() { + } + + /* ---------- Latencies ---------- */ + + /* -- Direct Latencies -- */ + + public int getServiceStartToQueryLatencyMicroseconds() { + return (int) ((this.mCredentialServiceStartedTimeNanoseconds + - this.mCredentialServiceBeginQueryTimeNanoseconds) / 1000); + } + + /* -- Timestamps -- */ + + public void setCredentialServiceStartedTimeNanoseconds( + long credentialServiceStartedTimeNanoseconds + ) { + this.mCredentialServiceStartedTimeNanoseconds = credentialServiceStartedTimeNanoseconds; + } + + public void setCredentialServiceBeginQueryTimeNanoseconds( + long credentialServiceBeginQueryTimeNanoseconds) { + mCredentialServiceBeginQueryTimeNanoseconds = credentialServiceBeginQueryTimeNanoseconds; + } + + public long getCredentialServiceStartedTimeNanoseconds() { + return mCredentialServiceStartedTimeNanoseconds; + } + + public long getCredentialServiceBeginQueryTimeNanoseconds() { + return mCredentialServiceBeginQueryTimeNanoseconds; + } + + /* ------ ApiName ------ */ + + public void setApiName(int apiName) { + mApiName = apiName; + } + + public int getApiName() { + return mApiName; + } + + /* ------ CallerUid ------ */ + + public void setCallerUid(int callerUid) { + mCallerUid = callerUid; + } + + public int getCallerUid() { + return mCallerUid; + } + + /* ------ SessionId ------ */ + + public void setSessionId(long sessionId) { + mSessionId = sessionId; + } + + public long getSessionId() { + return mSessionId; + } + + /* ------ SequenceId ------ */ + + public void setSequenceId(int sequenceId) { + mSequenceId = sequenceId; + } + + public int getSequenceId() { + return mSequenceId; + } + + /* ------ Count Request Class Types ------ */ + + public void setCountRequestClassType(int countRequestClassType) { + mCountRequestClassType = countRequestClassType; + } + + public int getCountRequestClassType() { + return mCountRequestClassType; + } +} diff --git a/services/credentials/java/com/android/server/credentials/metrics/ProviderStatusForMetrics.java b/services/credentials/java/com/android/server/credentials/metrics/ProviderStatusForMetrics.java new file mode 100644 index 000000000000..08f1afa2f438 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/metrics/ProviderStatusForMetrics.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.credentials.metrics; + +import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_FINAL_FAILURE; +import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_FINAL_SUCCESS; +import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_QUERY_FAILURE; +import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_QUERY_SUCCESS; +import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_UNKNOWN; + +public enum ProviderStatusForMetrics { + + UNKNOWN( + CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_UNKNOWN), + FINAL_FAILURE( + CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_FINAL_FAILURE), + QUERY_FAILURE( + CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_QUERY_FAILURE), + FINAL_SUCCESS( + CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_FINAL_SUCCESS), + QUERY_SUCCESS( + CREDENTIAL_MANAGER_API_CALLED__CANDIDATE_PROVIDER_STATUS__PROVIDER_QUERY_SUCCESS); + + private final int mInnerMetricCode; + + ProviderStatusForMetrics(int innerMetricCode) { + this.mInnerMetricCode = innerMetricCode; + } + + /** + * Gives the West-world version of the metric name. + * + * @return a code corresponding to the west world metric name + */ + public int getMetricCode() { + return this.mInnerMetricCode; + } +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java index 4351bc1c81ce..80100a927e82 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java @@ -24,6 +24,8 @@ import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -52,6 +54,11 @@ public class DevicePolicyCacheImpl extends DevicePolicyCache { @GuardedBy("mLock") private final SparseIntArray mPermissionPolicy = new SparseIntArray(); + @GuardedBy("mLock") + private List<String> mLauncherShortcutOverrides = + new ArrayList<>(); + + /** Maps to {@code ActiveAdmin.mAdminCanGrantSensorsPermissions}. */ private final AtomicBoolean mCanGrantSensorsPermissions = new AtomicBoolean(false); @@ -122,6 +129,22 @@ public class DevicePolicyCacheImpl extends DevicePolicyCache { mCanGrantSensorsPermissions.set(canGrant); } + @Override + public List<String> getLauncherShortcutOverrides() { + synchronized (mLock) { + return new ArrayList<>(mLauncherShortcutOverrides); + } + } + + /** + * Sets a list of packages for which shortcuts should be replaced by their badged version. + */ + public void setLauncherShortcutOverrides(List<String> launcherShortcutOverrides) { + synchronized (mLock) { + mLauncherShortcutOverrides = new ArrayList<>(launcherShortcutOverrides); + } + } + /** Dump content */ public void dump(IndentingPrintWriter pw) { synchronized (mLock) { @@ -131,6 +154,8 @@ public class DevicePolicyCacheImpl extends DevicePolicyCache { pw.println("Password quality: " + mPasswordQuality); pw.println("Permission policy: " + mPermissionPolicy); pw.println("Admin can grant sensors permission: " + mCanGrantSensorsPermissions.get()); + pw.print("Shortcuts overrides: "); + pw.println(mLauncherShortcutOverrides); pw.decreaseIndent(); } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index e9c23a052422..a4e563b21bec 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -3212,8 +3212,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private void sendChangedNotification(int userHandle) { Intent intent = new Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + Bundle options = new BroadcastOptions() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .setDeferUntilActive(true) + .toBundle(); mInjector.binderWithCleanCallingIdentity(() -> - mContext.sendBroadcastAsUser(intent, new UserHandle(userHandle))); + mContext.sendBroadcastAsUser(intent, new UserHandle(userHandle), null, options)); } private void loadSettingsLocked(DevicePolicyData policy, int userHandle) { @@ -3518,16 +3522,30 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { userId == UserHandle.USER_SYSTEM ? UserHandle.USER_ALL : userId); updatePermissionPolicyCache(userId); updateAdminCanGrantSensorsPermissionCache(userId); - final List<PreferentialNetworkServiceConfig> preferentialNetworkServiceConfigs; + boolean isManagedSubscription; + synchronized (getLockObject()) { ActiveAdmin owner = getDeviceOrProfileOwnerAdminLocked(userId); preferentialNetworkServiceConfigs = owner != null ? owner.mPreferentialNetworkServiceConfigs : List.of(PreferentialNetworkServiceConfig.DEFAULT); + + isManagedSubscription = owner != null && owner.mManagedSubscriptionsPolicy != null + && owner.mManagedSubscriptionsPolicy.getPolicyType() + == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS; } updateNetworkPreferenceForUser(userId, preferentialNetworkServiceConfigs); + if (isManagedSubscription) { + String defaultDialerPackageName = getDefaultRoleHolderPackageName( + com.android.internal.R.string.config_defaultDialer); + String defaultSmsPackageName = getDefaultRoleHolderPackageName( + com.android.internal.R.string.config_defaultSms); + updateDialerAndSmsManagedShortcutsOverrideCache(defaultDialerPackageName, + defaultSmsPackageName); + } + startOwnerService(userId, "start-user"); if (isDevicePolicyEngineEnabled()) { mDevicePolicyEngine.handleStartUser(userId); @@ -7610,6 +7628,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (isWorkProfileTelephonyFlagEnabled()) { clearManagedSubscriptionsPolicy(); + clearLauncherShortcutOverrides(); updateTelephonyCrossProfileIntentFilters(parentId, UserHandle.USER_NULL, false); } Slogf.i(LOG_TAG, "Cleaning up device-wide policies done."); @@ -7627,6 +7646,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + private void clearLauncherShortcutOverrides() { + mPolicyCache.setLauncherShortcutOverrides(new ArrayList<>()); + } + private void updateTelephonyCrossProfileIntentFilters(int parentUserId, int profileUserId, boolean enableWorkTelephony) { try { @@ -22751,12 +22774,30 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } else { Slogf.w(LOG_TAG, "Couldn't install sms app, sms app package is null"); } + + updateDialerAndSmsManagedShortcutsOverrideCache(defaultDialerPackageName, + defaultSmsPackageName); } catch (RemoteException re) { // shouldn't happen Slogf.wtf(LOG_TAG, "Failed to install dialer/sms app", re); } } + private void updateDialerAndSmsManagedShortcutsOverrideCache( + String defaultDialerPackageName, String defaultSmsPackageName) { + + List<String> shortcutOverrides = new ArrayList<>(); + + if (defaultDialerPackageName != null) { + shortcutOverrides.add(defaultDialerPackageName); + } + + if (defaultSmsPackageName != null) { + shortcutOverrides.add(defaultSmsPackageName); + } + mPolicyCache.setLauncherShortcutOverrides(shortcutOverrides); + } + private void registerListenerToAssignSubscriptionsToUser(int userId) { synchronized (mSubscriptionsChangedListenerLock) { if (mSubscriptionsChangedListener != null) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java index 3ca158dc9c96..194647fda92c 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java @@ -120,7 +120,9 @@ class Owners { } else { mDeviceStateCache.setDeviceOwnerType(NO_DEVICE_OWNER); } - + for (int userId : usersIds) { + mDeviceStateCache.setHasProfileOwner(userId, hasProfileOwner(userId)); + } } else { mUserManagerInternal.setDeviceManaged(hasDeviceOwner()); for (int userId : usersIds) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 850b5b686efe..edfe95efe7f9 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -2739,6 +2739,14 @@ public final class SystemServer implements Dumpable { } t.traceEnd(); + t.traceBegin("RegisterLogMteState"); + try { + LogMteState.register(context); + } catch (Throwable e) { + reportWtf("RegisterLogMteState", e); + } + t.traceEnd(); + // Emit any pending system_server WTFs synchronized (SystemService.class) { if (sPendingWtfs != null) { 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 99da415380cd..8a5d3a6772a5 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -558,7 +558,7 @@ public final class BroadcastQueueModernImplTest { // To maximize test coverage, dump current state; we're not worried // about the actual output, just that we don't crash - queue.getActive().setDeliveryState(0, BroadcastRecord.DELIVERY_SCHEDULED); + queue.getActive().setDeliveryState(0, BroadcastRecord.DELIVERY_SCHEDULED, "Test-driven"); queue.dumpLocked(SystemClock.uptimeMillis(), new IndentingPrintWriter(new PrintWriter(new ByteArrayOutputStream()))); diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java index cd5ac7bcb1e5..1731590be3c9 100644 --- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java @@ -782,6 +782,34 @@ public class AppOpsUidStateTrackerTest { assertTrue(mIntf.isUidInForeground(UID)); } + @Test + public void testAppWidgetVisibleDoesntChangeUidState() { + procStateBuilder(UID) + .topState() + .update(); + + SparseArray<String> updatedAppWidgetVisibilities = new SparseArray<>(); + updatedAppWidgetVisibilities.put(UID, ""); + + mIntf.updateAppWidgetVisibility(updatedAppWidgetVisibilities, true); + + assertEquals(UID_STATE_TOP, mIntf.getUidState(UID)); + } + + @Test + public void testAppWidgetNotVisibleDoesntChangeUidState() { + SparseArray<String> updatedAppWidgetVisibilities = new SparseArray<>(); + updatedAppWidgetVisibilities.put(UID, ""); + mIntf.updateAppWidgetVisibility(updatedAppWidgetVisibilities, true); + procStateBuilder(UID) + .topState() + .update(); + + mIntf.updateAppWidgetVisibility(updatedAppWidgetVisibilities, false); + + assertEquals(UID_STATE_TOP, mIntf.getUidState(UID)); + } + public void testUidStateChangedCallback(int initialState, int finalState) { int initialUidState = processStateToUidState(initialState); int finalUidState = processStateToUidState(finalState); diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 71492656913b..6861c2f049fb 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -108,6 +108,7 @@ <uses-permission android:name="android.permission.UPDATE_LOCK_TASK_PACKAGES" /> <uses-permission android:name="android.permission.ACCESS_CONTEXT_HUB" /> <uses-permission android:name="android.permission.USE_BIOMETRIC_INTERNAL" /> + <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" /> <queries> <package android:name="com.android.servicestests.apps.suspendtestapp" /> diff --git a/services/tests/servicestests/res/xml/irq_device_map_3.xml b/services/tests/servicestests/res/xml/irq_device_map_3.xml index 498b676dd1dc..1d2a7d37d613 100644 --- a/services/tests/servicestests/res/xml/irq_device_map_3.xml +++ b/services/tests/servicestests/res/xml/irq_device_map_3.xml @@ -21,6 +21,6 @@ <subsystem>Alarm</subsystem> </device> <device name="test.wifi.device"> - <subsystem>undefined</subsystem> + <subsystem>Wifi</subsystem> </device> </irq-device-map>
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java index 3ff802c0125c..6b0e33037af8 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.face.aidl; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -30,11 +32,15 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.content.ComponentName; +import android.hardware.biometrics.common.AuthenticateReason; import android.hardware.biometrics.common.ICancellationSignal; +import android.hardware.biometrics.common.OperationContext; +import android.hardware.biometrics.common.WakeReason; import android.hardware.biometrics.face.ISession; import android.hardware.face.Face; import android.hardware.face.FaceAuthenticateOptions; import android.os.IBinder; +import android.os.PowerManager; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.testing.TestableContext; @@ -69,6 +75,8 @@ public class FaceAuthenticationClientTest { private static final int USER_ID = 12; private static final long OP_ID = 32; + private static final int WAKE_REASON = WakeReason.LIFT; + private static final int AUTH_REASON = AuthenticateReason.Face.ASSISTANT_VISIBLE; @Rule public final TestableContext mContext = new TestableContext( @@ -126,8 +134,13 @@ public class FaceAuthenticationClientTest { InOrder order = inOrder(mHal, mBiometricContext); order.verify(mBiometricContext).updateContext( mOperationContextCaptor.capture(), anyBoolean()); - order.verify(mHal).authenticateWithContext( - eq(OP_ID), same(mOperationContextCaptor.getValue().toAidlContext())); + + final OperationContext aidlContext = mOperationContextCaptor.getValue().toAidlContext(); + order.verify(mHal).authenticateWithContext(eq(OP_ID), same(aidlContext)); + assertThat(aidlContext.wakeReason).isEqualTo(WAKE_REASON); + assertThat(aidlContext.authenticateReason.getFaceAuthenticateReason()) + .isEqualTo(AUTH_REASON); + verify(mHal, never()).authenticate(anyLong()); } @@ -156,8 +169,11 @@ public class FaceAuthenticationClientTest { final AidlSession aidl = new AidlSession(version, mHal, USER_ID, mHalSessionCallback); final FaceAuthenticateOptions options = new FaceAuthenticateOptions.Builder() .setOpPackageName("test-owner") - .setUserId(5) + .setUserId(USER_ID) .setSensorId(9) + .setWakeReason(PowerManager.WAKE_REASON_LIFT) + .setAuthenticateReason( + FaceAuthenticateOptions.AUTHENTICATE_REASON_ASSISTANT_VISIBLE) .build(); return new FaceAuthenticationClient(mContext, () -> aidl, mToken, 2 /* requestId */, mClientMonitorCallbackConverter, OP_ID, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceDetectClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceDetectClientTest.java index c4c550549f73..0abfa7e6546d 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceDetectClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceDetectClientTest.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.face.aidl; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.inOrder; @@ -24,9 +26,13 @@ import static org.mockito.Mockito.same; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.hardware.biometrics.common.AuthenticateReason; +import android.hardware.biometrics.common.OperationContext; +import android.hardware.biometrics.common.WakeReason; import android.hardware.biometrics.face.ISession; import android.hardware.face.FaceAuthenticateOptions; import android.os.IBinder; +import android.os.PowerManager; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.testing.TestableContext; @@ -55,6 +61,8 @@ import org.mockito.junit.MockitoRule; public class FaceDetectClientTest { private static final int USER_ID = 12; + private static final int WAKE_REASON = WakeReason.POWER_BUTTON; + private static final int AUTH_REASON = AuthenticateReason.Face.OCCLUDING_APP_REQUESTED; @Rule public final TestableContext mContext = new TestableContext( @@ -103,8 +111,13 @@ public class FaceDetectClientTest { InOrder order = inOrder(mHal, mBiometricContext); order.verify(mBiometricContext).updateContext( mOperationContextCaptor.capture(), anyBoolean()); - order.verify(mHal).detectInteractionWithContext( - same(mOperationContextCaptor.getValue().toAidlContext())); + + final OperationContext aidlContext = mOperationContextCaptor.getValue().toAidlContext(); + order.verify(mHal).detectInteractionWithContext(same(aidlContext)); + assertThat(aidlContext.wakeReason).isEqualTo(WAKE_REASON); + assertThat(aidlContext.authenticateReason.getFaceAuthenticateReason()) + .isEqualTo(AUTH_REASON); + verify(mHal, never()).detectInteraction(); } @@ -118,6 +131,9 @@ public class FaceDetectClientTest { .setUserId(USER_ID) .setSensorId(5) .setOpPackageName("own-it") + .setWakeReason(PowerManager.WAKE_REASON_POWER_BUTTON) + .setAuthenticateReason( + FaceAuthenticateOptions.AUTHENTICATE_REASON_OCCLUDING_APP_REQUESTED) .build(), mBiometricLogger, mBiometricContext, false /* isStrongBiometric */, null /* sensorPrivacyManager */); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index f0f975ccf5ff..c6645003c7e2 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -36,6 +36,7 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.content.ComponentName; import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.common.AuthenticateReason; import android.hardware.biometrics.common.ICancellationSignal; import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.fingerprint.ISession; @@ -165,8 +166,12 @@ public class FingerprintAuthenticationClientTest { InOrder order = inOrder(mHal, mBiometricContext); order.verify(mBiometricContext).updateContext( mOperationContextCaptor.capture(), anyBoolean()); - order.verify(mHal).authenticateWithContext( - eq(OP_ID), same(mOperationContextCaptor.getValue().toAidlContext())); + + final OperationContext aidlContext = mOperationContextCaptor.getValue().toAidlContext(); + order.verify(mHal).authenticateWithContext(eq(OP_ID), same(aidlContext)); + assertThat(aidlContext.authenticateReason.getFingerprintAuthenticateReason()) + .isEqualTo(AuthenticateReason.Fingerprint.UNKNOWN); + verify(mHal, never()).authenticate(anyLong()); } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java index e741e446da85..c20cc392f5c0 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.any; import static org.mockito.Mockito.inOrder; @@ -24,6 +26,8 @@ import static org.mockito.Mockito.same; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.hardware.biometrics.common.AuthenticateReason; +import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.fingerprint.ISession; import android.hardware.fingerprint.FingerprintAuthenticateOptions; import android.hardware.fingerprint.IUdfpsOverlayController; @@ -108,8 +112,12 @@ public class FingerprintDetectClientTest { InOrder order = inOrder(mHal, mBiometricContext); order.verify(mBiometricContext).updateContext( mOperationContextCaptor.capture(), anyBoolean()); - order.verify(mHal).detectInteractionWithContext( - same(mOperationContextCaptor.getValue().toAidlContext())); + + final OperationContext aidlContext = mOperationContextCaptor.getValue().toAidlContext(); + order.verify(mHal).detectInteractionWithContext(same(aidlContext)); + assertThat(aidlContext.authenticateReason.getFingerprintAuthenticateReason()) + .isEqualTo(AuthenticateReason.Fingerprint.UNKNOWN); + verify(mHal, never()).detectInteraction(); } 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 2967c5c53e8a..339ccd80c351 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 @@ -130,7 +130,6 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -283,7 +282,7 @@ public class VirtualDeviceManagerServiceTest { return blockedActivities; } - private Intent createRestrictedActivityBlockedIntent(List displayCategories, + private Intent createRestrictedActivityBlockedIntent(Set<String> displayCategories, String targetDisplayCategory) { when(mDisplayManagerInternalMock.createVirtualDisplay(any(), any(), any(), any(), eq(NONBLOCKED_APP_PACKAGE_NAME))).thenReturn(DISPLAY_ID_1); @@ -1634,7 +1633,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void nonRestrictedActivityOnRestrictedVirtualDisplay_startBlockedAlertActivity() { - Intent blockedAppIntent = createRestrictedActivityBlockedIntent(List.of("abc"), + Intent blockedAppIntent = createRestrictedActivityBlockedIntent(Set.of("abc"), /* targetDisplayCategory= */ null); verify(mContext).startActivityAsUser(argThat(intent -> intent.filterEquals(blockedAppIntent)), any(), any()); @@ -1642,7 +1641,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void restrictedActivityOnRestrictedVirtualDisplay_doesNotStartBlockedAlertActivity() { - Intent blockedAppIntent = createRestrictedActivityBlockedIntent(List.of("abc"), "abc"); + Intent blockedAppIntent = createRestrictedActivityBlockedIntent(Set.of("abc"), "abc"); verify(mContext, never()).startActivityAsUser(argThat(intent -> intent.filterEquals(blockedAppIntent)), any(), any()); } @@ -1650,14 +1649,14 @@ public class VirtualDeviceManagerServiceTest { @Test public void restrictedActivityOnNonRestrictedVirtualDisplay_startBlockedAlertActivity() { Intent blockedAppIntent = createRestrictedActivityBlockedIntent( - /* displayCategories= */ List.of(), "abc"); + /* displayCategories= */ Set.of(), "abc"); verify(mContext).startActivityAsUser(argThat(intent -> intent.filterEquals(blockedAppIntent)), any(), any()); } @Test public void restrictedActivityNonMatchingRestrictedVirtualDisplay_startBlockedAlertActivity() { - Intent blockedAppIntent = createRestrictedActivityBlockedIntent(List.of("abc"), "def"); + Intent blockedAppIntent = createRestrictedActivityBlockedIntent(Set.of("abc"), "def"); verify(mContext).startActivityAsUser(argThat(intent -> intent.filterEquals(blockedAppIntent)), any(), any()); } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java index d9e4da73c1c3..2bfa44ecb1d6 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java @@ -89,7 +89,7 @@ public class VirtualAudioControllerTest { /* activityBlockedCallback= */ null, /* secureWindowCallback= */ null, /* intentListenerCallback= */ null, - /* displayCategories= */ new ArrayList<>(), + /* displayCategories= */ new ArraySet<>(), /* showTasksInHostDeviceRecents= */ true); } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 1f25da7a3cef..aaabb286589c 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -162,9 +162,9 @@ import com.android.server.pm.UserRestrictionsUtils; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; -import org.junit.Ignore; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; import org.mockito.internal.util.collections.Sets; @@ -514,7 +514,9 @@ public class DevicePolicyManagerTest extends DpmTestBase { verify(mContext.spiedContext).sendBroadcastAsUser( MockUtils.checkIntentAction( DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), - MockUtils.checkUserHandle(CALLER_USER_HANDLE)); + MockUtils.checkUserHandle(CALLER_USER_HANDLE), + eq(null), + any(Bundle.class)); verify(mContext.spiedContext).sendBroadcastAsUser( MockUtils.checkIntentAction( DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED), @@ -793,7 +795,9 @@ public class DevicePolicyManagerTest extends DpmTestBase { verify(mContext.spiedContext, times(1)).sendBroadcastAsUser( MockUtils.checkIntentAction( DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), - MockUtils.checkUserHandle(CALLER_USER_HANDLE)); + MockUtils.checkUserHandle(CALLER_USER_HANDLE), + eq(null), + any(Bundle.class)); // Remove. No permissions, but same user, so it'll work. mContext.callerPermissions.clear(); @@ -820,7 +824,9 @@ public class DevicePolicyManagerTest extends DpmTestBase { verify(mContext.spiedContext, times(2)).sendBroadcastAsUser( MockUtils.checkIntentAction( DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), - MockUtils.checkUserHandle(CALLER_USER_HANDLE)); + MockUtils.checkUserHandle(CALLER_USER_HANDLE), + eq(null), + any(Bundle.class)); // TODO Check other internal calls. } @@ -846,7 +852,9 @@ public class DevicePolicyManagerTest extends DpmTestBase { verify(mContext.spiedContext, times(2)).sendBroadcastAsUser( MockUtils.checkIntentAction( DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), - MockUtils.checkUserHandle(CALLER_USER_HANDLE)); + MockUtils.checkUserHandle(CALLER_USER_HANDLE), + eq(null), + any(Bundle.class)); // Remove. No permissions, but same user, so it'll work. mContext.callerPermissions.clear(); @@ -874,7 +882,9 @@ public class DevicePolicyManagerTest extends DpmTestBase { verify(mContext.spiedContext, times(3)).sendBroadcastAsUser( MockUtils.checkIntentAction( DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), - MockUtils.checkUserHandle(CALLER_USER_HANDLE)); + MockUtils.checkUserHandle(CALLER_USER_HANDLE), + eq(null), + any(Bundle.class)); } /** @@ -2425,7 +2435,9 @@ public class DevicePolicyManagerTest extends DpmTestBase { verify(mContext.spiedContext, times(2)).sendBroadcastAsUser( MockUtils.checkIntentAction( DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), - MockUtils.checkUserHandle(CALLER_USER_HANDLE)); + MockUtils.checkUserHandle(CALLER_USER_HANDLE), + eq(null), + any(Bundle.class)); verify(mContext.spiedContext).sendBroadcastAsUser( MockUtils.checkIntentAction( DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED), @@ -5886,7 +5898,9 @@ public class DevicePolicyManagerTest extends DpmTestBase { verify(mContext.spiedContext, times(1)).sendBroadcastAsUser( MockUtils.checkIntentAction( DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), - MockUtils.checkUserHandle(userHandle)); + MockUtils.checkUserHandle(userHandle), + eq(null), + any(Bundle.class)); final Intent intent = new Intent(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED); intent.setComponent(admin1); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/OwnersTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/OwnersTest.java index 85a2446cc316..375b52d2d5e4 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/OwnersTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/OwnersTest.java @@ -21,6 +21,7 @@ import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED; import static android.app.admin.SystemUpdatePolicy.TYPE_INSTALL_WINDOWED; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import android.content.ComponentName; import android.os.IpcDataCache; @@ -43,6 +44,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class OwnersTest extends DpmTestBase { + private static final int TEST_PO_USER = 10; private static final String TESTDPC_PACKAGE = "com.afwsamples.testdpc"; private final DeviceStateCacheImpl mDeviceStateCache = new DeviceStateCacheImpl(); @@ -55,11 +57,11 @@ public class OwnersTest extends DpmTestBase { @Test public void loadProfileOwner() throws Exception { - getServices().addUsers(10); + getServices().addUsers(TEST_PO_USER); final Owners owners = makeOwners(); - DpmTestUtils.writeToFile(owners.getProfileOwnerFile(10), + DpmTestUtils.writeToFile(owners.getProfileOwnerFile(TEST_PO_USER), DpmTestUtils.readAsset(mRealTestContext, "OwnersTest/profile_owner_1.xml")); owners.load(); @@ -71,6 +73,9 @@ public class OwnersTest extends DpmTestBase { assertThat(owners.getProfileOwnerComponent(10)) .isEqualTo(new ComponentName(TESTDPC_PACKAGE, "com.afwsamples.testdpc.DeviceAdminReceiver")); + + assertWithMessage("Profile owner data in DeviceStateCache wasn't populated") + .that(mDeviceStateCache.isUserOrganizationManaged(TEST_PO_USER)).isTrue(); } @Test @@ -90,6 +95,10 @@ public class OwnersTest extends DpmTestBase { "com.afwsamples.testdpc.DeviceAdminReceiver")); assertThat(owners.getSystemUpdatePolicy().getPolicyType()).isEqualTo(TYPE_INSTALL_WINDOWED); + + assertWithMessage("Device owner data in DeviceStateCache wasn't populated") + .that(mDeviceStateCache.isUserOrganizationManaged(owners.getDeviceOwnerUserId())) + .isTrue(); } @Test 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 7971fd71ea09..94d30bb4440b 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -24,6 +24,8 @@ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_D import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -32,8 +34,10 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -61,10 +65,13 @@ import android.hardware.display.HdrConversionMode; import android.hardware.display.IDisplayManagerCallback; import android.hardware.display.IVirtualDisplayCallback; import android.hardware.display.VirtualDisplayConfig; +import android.media.projection.IMediaProjectionManager; +import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.MessageQueue; import android.os.Process; +import android.view.ContentRecordingSession; import android.view.Display; import android.view.DisplayCutout; import android.view.DisplayEventReceiver; @@ -158,25 +165,40 @@ public class DisplayManagerServiceTest { } }; - class BasicInjector extends DisplayManagerService.Injector { - @Override - VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot, Context context, - Handler handler, DisplayAdapter.Listener displayAdapterListener) { - return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener, - (String name, boolean secure, float refreshRate) -> mMockDisplayToken); - } - - @Override - LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context, - Handler handler, DisplayAdapter.Listener displayAdapterListener) { - return new LocalDisplayAdapter(syncRoot, context, handler, - displayAdapterListener, new LocalDisplayAdapter.Injector() { - @Override - public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() { - return mSurfaceControlProxy; - } - }); - } + class BasicInjector extends DisplayManagerService.Injector { + @Override + IMediaProjectionManager getProjectionService() { + return mMockProjectionService; + } + + @Override + VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot, Context context, + Handler handler, DisplayAdapter.Listener displayAdapterListener) { + return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener, + new VirtualDisplayAdapter.SurfaceControlDisplayFactory() { + @Override + public IBinder createDisplay(String name, boolean secure, + float requestedRefreshRate) { + return mMockDisplayToken; + } + + @Override + public void destroyDisplay(IBinder displayToken) { + } + }); + } + + @Override + LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context, + Handler handler, DisplayAdapter.Listener displayAdapterListener) { + return new LocalDisplayAdapter(syncRoot, context, handler, + displayAdapterListener, new LocalDisplayAdapter.Injector() { + @Override + public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() { + return mSurfaceControlProxy; + } + }); + } @Override int setHdrConversionMode(int conversionMode, int preferredHdrOutputType, @@ -198,6 +220,7 @@ public class DisplayManagerServiceTest { private final DisplayManagerService.Injector mBasicInjector = new BasicInjector(); + @Mock IMediaProjectionManager mMockProjectionService; @Mock IVirtualDeviceManager mIVirtualDeviceManager; @Mock InputManagerInternal mMockInputManagerInternal; @Mock VirtualDeviceManagerInternal mMockVirtualDeviceManagerInternal; @@ -285,6 +308,7 @@ public class DisplayManagerServiceTest { builder.setFlags(flags); int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); @@ -410,6 +434,7 @@ public class DisplayManagerServiceTest { builder.setUniqueId(uniqueId); int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); @@ -446,6 +471,7 @@ public class DisplayManagerServiceTest { builder.setUniqueId(uniqueId); int displayId = bs.createVirtualDisplay(builder.build(), /* callback= */ mMockAppToken, /* projection= */ null, PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); @@ -479,6 +505,7 @@ public class DisplayManagerServiceTest { builder.setUniqueId(uniqueId); int displayId = bs.createVirtualDisplay(builder.build(), /* callback= */ mMockAppToken, /* projection= */ null, PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); @@ -720,6 +747,7 @@ public class DisplayManagerServiceTest { builder.setUniqueId(uniqueId); final int firstDisplayId = binderService.createVirtualDisplay(builder.build(), mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); // The second virtual display requests to mirror the first virtual display. final String uniqueId2 = "uniqueId --- displayIdToMirrorTest #2"; @@ -731,6 +759,7 @@ public class DisplayManagerServiceTest { final int secondDisplayId = binderService.createVirtualDisplay(builder2.build(), mMockAppToken2 /* callback */, null /* projection */, PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); // flush the handler @@ -768,6 +797,7 @@ public class DisplayManagerServiceTest { virtualDevice /* virtualDeviceToken */, mock(DisplayWindowPolicyController.class), PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId; // Create a second virtual display. This should be added to the previously created display @@ -783,6 +813,7 @@ public class DisplayManagerServiceTest { virtualDevice /* virtualDeviceToken */, mock(DisplayWindowPolicyController.class), PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId; assertEquals( @@ -820,6 +851,7 @@ public class DisplayManagerServiceTest { virtualDevice /* virtualDeviceToken */, mock(DisplayWindowPolicyController.class), PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId; // Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP, @@ -838,6 +870,7 @@ public class DisplayManagerServiceTest { virtualDevice /* virtualDeviceToken */, mock(DisplayWindowPolicyController.class), PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId; assertNotEquals( @@ -881,6 +914,7 @@ public class DisplayManagerServiceTest { virtualDevice /* virtualDeviceToken */, mock(DisplayWindowPolicyController.class), PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); // Check that FLAG_ALWAYS_UNLOCKED is set. assertNotEquals( @@ -906,6 +940,7 @@ public class DisplayManagerServiceTest { virtualDevice /* virtualDeviceToken */, mock(DisplayWindowPolicyController.class), PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); // Check that FLAG_ALWAYS_UNLOCKED is set. assertNotEquals( @@ -929,6 +964,7 @@ public class DisplayManagerServiceTest { null /* virtualDeviceToken */, mock(DisplayWindowPolicyController.class), PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); // Check that FLAG_ALWAYS_UNLOCKED is not set. assertEquals( @@ -960,6 +996,7 @@ public class DisplayManagerServiceTest { .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY); final int firstDisplayId = binderService.createVirtualDisplay(builder.build(), mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); // The second virtual display requests to mirror the first virtual display. final String uniqueId2 = "uniqueId --- displayIdToMirrorTest #2"; @@ -967,10 +1004,11 @@ public class DisplayManagerServiceTest { final VirtualDisplayConfig.Builder builder2 = new VirtualDisplayConfig.Builder( VIRTUAL_DISPLAY_NAME, width, height, dpi) .setUniqueId(uniqueId2) - .setWindowManagerMirroring(true); + .setWindowManagerMirroringEnabled(true); final int secondDisplayId = binderService.createVirtualDisplay(builder2.build(), mMockAppToken2 /* callback */, null /* projection */, PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); // flush the handler @@ -985,6 +1023,54 @@ public class DisplayManagerServiceTest { Display.INVALID_DISPLAY); } + @Test + public void testCreateVirtualDisplay_setContentRecordingSessionSuccess() throws Exception { + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + when(mMockWindowManagerInternal + .setContentRecordingSession(any(ContentRecordingSession.class))) + .thenReturn(true); + + final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder( + VIRTUAL_DISPLAY_NAME, 600, 800, 320); + builder.setUniqueId("uniqueId --- setContentRecordingSession true"); + builder.setContentRecordingSession( + ContentRecordingSession.createDisplaySession(new Binder(""))); + + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + registerDefaultDisplays(displayManager); + displayManager.windowManagerAndInputReady(); + + DisplayManagerService.BinderService binderService = displayManager.new BinderService(); + final int displayId = binderService.createVirtualDisplay(builder.build(), + mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME); + + assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY); + } + + @Test + public void testCreateVirtualDisplay_setContentRecordingSessionFail() throws Exception { + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + when(mMockWindowManagerInternal + .setContentRecordingSession(any(ContentRecordingSession.class))) + .thenReturn(false); + + final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder( + VIRTUAL_DISPLAY_NAME, 600, 800, 320); + builder.setUniqueId("uniqueId --- setContentRecordingSession false"); + builder.setContentRecordingSession( + ContentRecordingSession.createDisplaySession(new Binder(""))); + + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + registerDefaultDisplays(displayManager); + displayManager.windowManagerAndInputReady(); + + DisplayManagerService.BinderService binderService = displayManager.new BinderService(); + final int displayId = binderService.createVirtualDisplay(builder.build(), + mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME); + + assertThat(displayId).isEqualTo(Display.INVALID_DISPLAY); + } + /** * Tests that the virtual display is created with * {@link VirtualDisplayConfig.Builder#setSurface(Surface)} @@ -1011,6 +1097,7 @@ public class DisplayManagerServiceTest { builder.setUniqueId(uniqueId); final int displayId = binderService.createVirtualDisplay(builder.build(), mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); @@ -1043,6 +1130,7 @@ public class DisplayManagerServiceTest { int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId); @@ -1093,7 +1181,6 @@ public class DisplayManagerServiceTest { registerDefaultDisplays(displayManager); - DisplayManagerService.BinderService bs = displayManager.new BinderService(); when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)).thenReturn( @@ -1111,6 +1198,7 @@ public class DisplayManagerServiceTest { int displayId = localService.createVirtualDisplay(builder.build(), mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */, mock(DisplayWindowPolicyController.class), PACKAGE_NAME); + verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any()); displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class)); displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId); diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt index b2bfd2bf720c..b660926f1394 100644 --- a/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt @@ -256,12 +256,16 @@ class KeyboardLayoutManagerTests { @Test fun testNewUi_getKeyboardLayoutsForInputDevice() { NewSettingsApiFlag(true).use { - val keyboardLayouts = - keyboardLayoutManager.getKeyboardLayoutsForInputDevice(keyboardDevice.identifier) - assertEquals( - "New UI: getKeyboardLayoutsForInputDevice API should always return empty array", - 0, - keyboardLayouts.size + val keyboardLayouts = keyboardLayoutManager.keyboardLayouts + assertNotEquals( + "New UI: getKeyboardLayoutsForInputDevice API should not return empty array", + 0, + keyboardLayouts.size + ) + assertTrue( + "New UI: getKeyboardLayoutsForInputDevice API should provide English(US) " + + "layout", + hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) ) } } diff --git a/services/tests/servicestests/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java b/services/tests/servicestests/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java new file mode 100644 index 000000000000..1e73a45a0c22 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java @@ -0,0 +1,247 @@ +/* + * 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.media; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.media.AudioManager; +import android.media.AudioRoutesInfo; +import android.media.IAudioRoutesObserver; +import android.media.MediaRoute2Info; +import android.os.RemoteException; + +import com.android.internal.R; +import com.android.server.audio.AudioService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(JUnit4.class) +public class AudioPoliciesDeviceRouteControllerTest { + + private static final String ROUTE_NAME_DEFAULT = "default"; + private static final String ROUTE_NAME_DOCK = "dock"; + private static final String ROUTE_NAME_HEADPHONES = "headphones"; + + private static final int VOLUME_SAMPLE_1 = 25; + + @Mock + private Context mContext; + @Mock + private Resources mResources; + @Mock + private AudioManager mAudioManager; + @Mock + private AudioService mAudioService; + @Mock + private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; + + @Captor + private ArgumentCaptor<IAudioRoutesObserver.Stub> mAudioRoutesObserverCaptor; + + private AudioPoliciesDeviceRouteController mController; + + private IAudioRoutesObserver.Stub mAudioRoutesObserver; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getText(anyInt())).thenReturn(ROUTE_NAME_DEFAULT); + + // Setting built-in speaker as default speaker. + AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo(); + audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_SPEAKER; + when(mAudioService.startWatchingRoutes(mAudioRoutesObserverCaptor.capture())) + .thenReturn(audioRoutesInfo); + + mController = new AudioPoliciesDeviceRouteController( + mContext, mAudioManager, mAudioService, mOnDeviceRouteChangedListener); + + mAudioRoutesObserver = mAudioRoutesObserverCaptor.getValue(); + } + + @Test + public void getDeviceRoute_noSelectedRoutes_returnsDefaultDevice() { + MediaRoute2Info route2Info = mController.getDeviceRoute(); + + assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DEFAULT); + assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + } + + @Test + public void getDeviceRoute_audioRouteHasChanged_returnsRouteFromAudioService() { + when(mResources.getText(R.string.default_audio_route_name_headphones)) + .thenReturn(ROUTE_NAME_HEADPHONES); + + AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo(); + audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES; + callAudioRoutesObserver(audioRoutesInfo); + + MediaRoute2Info route2Info = mController.getDeviceRoute(); + assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES); + assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES); + } + + @Test + public void getDeviceRoute_selectDevice_returnsSelectedRoute() { + when(mResources.getText(R.string.default_audio_route_name_dock_speakers)) + .thenReturn(ROUTE_NAME_DOCK); + + mController.selectRoute(MediaRoute2Info.TYPE_DOCK); + + MediaRoute2Info route2Info = mController.getDeviceRoute(); + assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DOCK); + assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK); + } + + @Test + public void getDeviceRoute_hasSelectedAndAudioServiceRoutes_returnsSelectedRoute() { + when(mResources.getText(R.string.default_audio_route_name_headphones)) + .thenReturn(ROUTE_NAME_HEADPHONES); + when(mResources.getText(R.string.default_audio_route_name_dock_speakers)) + .thenReturn(ROUTE_NAME_DOCK); + + AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo(); + audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES; + callAudioRoutesObserver(audioRoutesInfo); + + mController.selectRoute(MediaRoute2Info.TYPE_DOCK); + + MediaRoute2Info route2Info = mController.getDeviceRoute(); + assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DOCK); + assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK); + } + + @Test + public void getDeviceRoute_unselectRoute_returnsAudioServiceRoute() { + when(mResources.getText(R.string.default_audio_route_name_headphones)) + .thenReturn(ROUTE_NAME_HEADPHONES); + when(mResources.getText(R.string.default_audio_route_name_dock_speakers)) + .thenReturn(ROUTE_NAME_DOCK); + + mController.selectRoute(MediaRoute2Info.TYPE_DOCK); + + AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo(); + audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES; + callAudioRoutesObserver(audioRoutesInfo); + + mController.selectRoute(null); + + MediaRoute2Info route2Info = mController.getDeviceRoute(); + assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES); + assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES); + } + + @Test + public void getDeviceRoute_selectRouteFails_returnsAudioServiceRoute() { + when(mResources.getText(R.string.default_audio_route_name_headphones)) + .thenReturn(ROUTE_NAME_HEADPHONES); + + AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo(); + audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES; + callAudioRoutesObserver(audioRoutesInfo); + + mController.selectRoute(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); + + MediaRoute2Info route2Info = mController.getDeviceRoute(); + assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES); + assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES); + } + + @Test + public void selectRoute_selectWiredRoute_returnsTrue() { + assertThat(mController.selectRoute(MediaRoute2Info.TYPE_HDMI)).isTrue(); + } + + @Test + public void selectRoute_selectBluetoothRoute_returnsFalse() { + assertThat(mController.selectRoute(MediaRoute2Info.TYPE_BLUETOOTH_A2DP)).isFalse(); + } + + @Test + public void selectRoute_unselectRoute_returnsTrue() { + assertThat(mController.selectRoute(null)).isTrue(); + } + + @Test + public void updateVolume_noSelectedRoute_deviceRouteVolumeChanged() { + when(mResources.getText(R.string.default_audio_route_name_headphones)) + .thenReturn(ROUTE_NAME_HEADPHONES); + + AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo(); + audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES; + callAudioRoutesObserver(audioRoutesInfo); + + mController.updateVolume(VOLUME_SAMPLE_1); + + MediaRoute2Info route2Info = mController.getDeviceRoute(); + assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES); + assertThat(route2Info.getVolume()).isEqualTo(VOLUME_SAMPLE_1); + } + + @Test + public void updateVolume_connectSelectedRouteLater_selectedRouteVolumeChanged() { + when(mResources.getText(R.string.default_audio_route_name_headphones)) + .thenReturn(ROUTE_NAME_HEADPHONES); + when(mResources.getText(R.string.default_audio_route_name_dock_speakers)) + .thenReturn(ROUTE_NAME_DOCK); + + AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo(); + audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES; + callAudioRoutesObserver(audioRoutesInfo); + + mController.updateVolume(VOLUME_SAMPLE_1); + + mController.selectRoute(MediaRoute2Info.TYPE_DOCK); + + MediaRoute2Info route2Info = mController.getDeviceRoute(); + assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK); + assertThat(route2Info.getVolume()).isEqualTo(VOLUME_SAMPLE_1); + } + + /** + * Simulates {@link IAudioRoutesObserver.Stub#dispatchAudioRoutesChanged(AudioRoutesInfo)} + * from {@link AudioService}. This happens when there is a wired route change, + * like a wired headset being connected. + * + * @param audioRoutesInfo updated state of connected wired device + */ + private void callAudioRoutesObserver(AudioRoutesInfo audioRoutesInfo) { + try { + // this is a captured observer implementation + // from WiredRoutesController's AudioService#startWatchingRoutes call + mAudioRoutesObserver.dispatchAudioRoutesChanged(audioRoutesInfo); + } catch (RemoteException exception) { + // Should not happen since the object is mocked. + assertWithMessage("An unexpected RemoteException happened.").fail(); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java b/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java index 24ed42cab63a..24e48517f280 100644 --- a/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java @@ -50,7 +50,7 @@ import java.util.Arrays; import java.util.Collection; @RunWith(Enclosed.class) -public class DeviceRouteControllerTest { +public class LegacyDeviceRouteControllerTest { private static final String DEFAULT_ROUTE_NAME = "default_route"; private static final String DEFAULT_HEADPHONES_NAME = "headphone"; @@ -97,7 +97,7 @@ public class DeviceRouteControllerTest { // Default route should be initialized even when AudioService returns null. when(mAudioService.startWatchingRoutes(any())).thenReturn(null); - DeviceRouteController deviceRouteController = new DeviceRouteController( + LegacyDeviceRouteController deviceRouteController = new LegacyDeviceRouteController( mContext, mAudioManager, mAudioService, @@ -122,7 +122,7 @@ public class DeviceRouteControllerTest { AudioRoutesInfo fakeBluetoothAudioRoute = createFakeBluetoothAudioRoute(); when(mAudioService.startWatchingRoutes(any())).thenReturn(fakeBluetoothAudioRoute); - DeviceRouteController deviceRouteController = new DeviceRouteController( + LegacyDeviceRouteController deviceRouteController = new LegacyDeviceRouteController( mContext, mAudioManager, mAudioService, @@ -236,7 +236,7 @@ public class DeviceRouteControllerTest { when(mResources.getText(mExpectedRouteNameResource)) .thenReturn(mExpectedRouteNameValue); - DeviceRouteController deviceRouteController = new DeviceRouteController( + LegacyDeviceRouteController deviceRouteController = new LegacyDeviceRouteController( mContext, mAudioManager, mAudioService, @@ -269,7 +269,7 @@ public class DeviceRouteControllerTest { @Captor private ArgumentCaptor<IAudioRoutesObserver.Stub> mAudioRoutesObserverCaptor; - private DeviceRouteController mDeviceRouteController; + private LegacyDeviceRouteController mDeviceRouteController; private IAudioRoutesObserver.Stub mAudioRoutesObserver; @Before @@ -287,7 +287,7 @@ public class DeviceRouteControllerTest { when(mAudioService.startWatchingRoutes(mAudioRoutesObserverCaptor.capture())) .thenReturn(audioRoutesInfo); - mDeviceRouteController = new DeviceRouteController( + mDeviceRouteController = new LegacyDeviceRouteController( mContext, mAudioManager, mAudioService, diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsImplTest.java index c6a7fbcfb454..ee4b839dda5a 100644 --- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsImplTest.java +++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsImplTest.java @@ -572,41 +572,14 @@ public class BatteryStatsImplTest { mBatteryStatsImpl.noteBluetoothScanStoppedFromSourceLocked(ws, true, 9000, 9000); mBatteryStatsImpl.noteBluetoothScanResultsFromSourceLocked(ws, 42, 9000, 9000); - - - final Parcel uidTrafficParcel1 = Parcel.obtain(); - final Parcel uidTrafficParcel2 = Parcel.obtain(); - - uidTrafficParcel1.writeInt(10042); - uidTrafficParcel1.writeLong(3000); - uidTrafficParcel1.writeLong(4000); - uidTrafficParcel1.setDataPosition(0); - uidTrafficParcel2.writeInt(10043); - uidTrafficParcel2.writeLong(5000); - uidTrafficParcel2.writeLong(8000); - uidTrafficParcel2.setDataPosition(0); - - List<UidTraffic> uidTrafficList = ImmutableList.of( - UidTraffic.CREATOR.createFromParcel(uidTrafficParcel1), - UidTraffic.CREATOR.createFromParcel(uidTrafficParcel2)); - - final Parcel btActivityEnergyInfoParcel = Parcel.obtain(); - btActivityEnergyInfoParcel.writeLong(1000); - btActivityEnergyInfoParcel.writeInt( - BluetoothActivityEnergyInfo.BT_STACK_STATE_STATE_ACTIVE); - btActivityEnergyInfoParcel.writeLong(9000); - btActivityEnergyInfoParcel.writeLong(8000); - btActivityEnergyInfoParcel.writeLong(12000); - btActivityEnergyInfoParcel.writeLong(0); - btActivityEnergyInfoParcel.writeTypedList(uidTrafficList); - btActivityEnergyInfoParcel.setDataPosition(0); - - BluetoothActivityEnergyInfo info = BluetoothActivityEnergyInfo.CREATOR - .createFromParcel(btActivityEnergyInfoParcel); - - uidTrafficParcel1.recycle(); - uidTrafficParcel2.recycle(); - btActivityEnergyInfoParcel.recycle(); + BluetoothActivityEnergyInfo info = createBluetoothActivityEnergyInfo( + /* timestamp= */ 1000, + /* controllerTxTimeMs= */ 9000, + /* controllerRxTimeMs= */ 8000, + /* controllerIdleTimeMs= */ 12000, + /* controllerEnergyUsed= */ 0, + createUidTraffic(/* appUid= */ 10042, /* rxBytes= */ 3000, /* txBytes= */ 4000), + createUidTraffic(/* appUid= */ 10043, /* rxBytes= */ 5000, /* txBytes= */ 8000)); mBatteryStatsImpl.updateBluetoothStateLocked(info, -1, 1000, 1000); @@ -622,4 +595,105 @@ public class BatteryStatsImplTest { assertThat(uidStats.rxTimeMs).isEqualTo(7375); // Some scan time is treated as RX assertThat(uidStats.txTimeMs).isEqualTo(7666); // Some scan time is treated as TX } + + /** A regression test for b/266128651 */ + @Test + public void testGetNetworkActivityBytes_multipleUpdates() { + when(mPowerProfile.getAveragePower( + PowerProfile.POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE)).thenReturn(3.0); + mBatteryStatsImpl.setOnBatteryInternal(true); + mBatteryStatsImpl.updateTimeBasesLocked(true, Display.STATE_OFF, 0, 0); + + BluetoothActivityEnergyInfo info1 = createBluetoothActivityEnergyInfo( + /* timestamp= */ 10000, + /* controllerTxTimeMs= */ 9000, + /* controllerRxTimeMs= */ 8000, + /* controllerIdleTimeMs= */ 2000, + /* controllerEnergyUsed= */ 0, + createUidTraffic(/* appUid= */ 10042, /* rxBytes= */ 3000, /* txBytes= */ 4000), + createUidTraffic(/* appUid= */ 10043, /* rxBytes= */ 5000, /* txBytes= */ 8000)); + + mBatteryStatsImpl.updateBluetoothStateLocked(info1, -1, 1000, 1000); + + long totalRx1 = mBatteryStatsImpl.getNetworkActivityBytes( + BatteryStats.NETWORK_BT_RX_DATA, BatteryStats.STATS_SINCE_CHARGED); + long totalTx1 = mBatteryStatsImpl.getNetworkActivityBytes( + BatteryStats.NETWORK_BT_TX_DATA, BatteryStats.STATS_SINCE_CHARGED); + + assertThat(totalRx1).isEqualTo(8000); // 3000 + 5000 + assertThat(totalTx1).isEqualTo(12000); // 4000 + 8000 + + BluetoothActivityEnergyInfo info2 = createBluetoothActivityEnergyInfo( + /* timestamp= */ 20000, + /* controllerTxTimeMs= */ 19000, + /* controllerRxTimeMs= */ 18000, + /* controllerIdleTimeMs= */ 3000, + /* controllerEnergyUsed= */ 0, + createUidTraffic(/* appUid= */ 10043, /* rxBytes= */ 6000, /* txBytes= */ 9500), + createUidTraffic(/* appUid= */ 10044, /* rxBytes= */ 7000, /* txBytes= */ 9000)); + + mBatteryStatsImpl.updateBluetoothStateLocked(info2, -1, 2000, 2000); + + long totalRx2 = mBatteryStatsImpl.getNetworkActivityBytes( + BatteryStats.NETWORK_BT_RX_DATA, BatteryStats.STATS_SINCE_CHARGED); + long totalTx2 = mBatteryStatsImpl.getNetworkActivityBytes( + BatteryStats.NETWORK_BT_TX_DATA, BatteryStats.STATS_SINCE_CHARGED); + + assertThat(totalRx2).isEqualTo(16000); // 3000 + 6000 (updated) + 7000 (new) + assertThat(totalTx2).isEqualTo(22500); // 4000 + 9500 (updated) + 9000 (new) + + BluetoothActivityEnergyInfo info3 = createBluetoothActivityEnergyInfo( + /* timestamp= */ 30000, + /* controllerTxTimeMs= */ 20000, + /* controllerRxTimeMs= */ 20000, + /* controllerIdleTimeMs= */ 4000, + /* controllerEnergyUsed= */ 0, + createUidTraffic(/* appUid= */ 10043, /* rxBytes= */ 7000, /* txBytes= */ 9900), + createUidTraffic(/* appUid= */ 10044, /* rxBytes= */ 8000, /* txBytes= */ 10000)); + + mBatteryStatsImpl.updateBluetoothStateLocked(info3, -1, 2000, 2000); + + long totalRx3 = mBatteryStatsImpl.getNetworkActivityBytes( + BatteryStats.NETWORK_BT_RX_DATA, BatteryStats.STATS_SINCE_CHARGED); + long totalTx3 = mBatteryStatsImpl.getNetworkActivityBytes( + BatteryStats.NETWORK_BT_TX_DATA, BatteryStats.STATS_SINCE_CHARGED); + + assertThat(totalRx3).isEqualTo(18000); // 3000 + 7000 (updated) + 8000 (updated) + assertThat(totalTx3).isEqualTo(23900); // 4000 + 9900 (updated) + 10000 (updated) + } + + private UidTraffic createUidTraffic(int appUid, long rxBytes, long txBytes) { + final Parcel parcel = Parcel.obtain(); + parcel.writeInt(appUid); // mAppUid + parcel.writeLong(rxBytes); // mRxBytes + parcel.writeLong(txBytes); // mTxBytes + parcel.setDataPosition(0); + UidTraffic uidTraffic = UidTraffic.CREATOR.createFromParcel(parcel); + parcel.recycle(); + return uidTraffic; + } + + private BluetoothActivityEnergyInfo createBluetoothActivityEnergyInfo( + long timestamp, + long controllerTxTimeMs, + long controllerRxTimeMs, + long controllerIdleTimeMs, + long controllerEnergyUsed, + UidTraffic... uidTraffic) { + Parcel parcel = Parcel.obtain(); + parcel.writeLong(timestamp); // mTimestamp + parcel.writeInt( + BluetoothActivityEnergyInfo.BT_STACK_STATE_STATE_ACTIVE); // mBluetoothStackState + parcel.writeLong(controllerTxTimeMs); // mControllerTxTimeMs; + parcel.writeLong(controllerRxTimeMs); // mControllerRxTimeMs; + parcel.writeLong(controllerIdleTimeMs); // mControllerIdleTimeMs; + parcel.writeLong(controllerEnergyUsed); // mControllerEnergyUsed; + parcel.writeTypedList(ImmutableList.copyOf(uidTraffic)); // mUidTraffic + parcel.setDataPosition(0); + + BluetoothActivityEnergyInfo info = + BluetoothActivityEnergyInfo.CREATOR.createFromParcel(parcel); + parcel.recycle(); + return info; + } } diff --git a/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java b/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java index 34e45c2096ea..397d7b5f2a3e 100644 --- a/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java +++ b/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java @@ -18,9 +18,9 @@ package com.android.server.power.stats; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_UNKNOWN; +import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_WIFI; import static com.android.server.power.stats.CpuWakeupStats.WAKEUP_REASON_HALF_WINDOW_MS; -import static com.android.server.power.stats.CpuWakeupStats.WAKEUP_RETENTION_MS; import static com.google.common.truth.Truth.assertThat; @@ -45,6 +45,7 @@ import java.util.concurrent.ThreadLocalRandom; @RunWith(AndroidJUnit4.class) public class CpuWakeupStatsTest { private static final String KERNEL_REASON_ALARM_IRQ = "120 test.alarm.device"; + private static final String KERNEL_REASON_WIFI_IRQ = "120 test.wifi.device"; private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device"; private static final String KERNEL_REASON_UNKNOWN = "free-form-reason test.alarm.device"; private static final String KERNEL_REASON_UNSUPPORTED = "-1 test.alarm.device"; @@ -64,28 +65,29 @@ public class CpuWakeupStatsTest { public void removesOldWakeups() { // The xml resource doesn't matter for this test. final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_1, mHandler); + final long retention = obj.mConfig.WAKEUP_STATS_RETENTION_MS; final Set<Long> timestamps = new HashSet<>(); final long firstWakeup = 453192; - obj.noteWakeupTimeAndReason(firstWakeup, 32, "unused"); + obj.noteWakeupTimeAndReason(firstWakeup, 32, KERNEL_REASON_UNKNOWN_IRQ); timestamps.add(firstWakeup); for (int i = 1; i < 1000; i++) { - final long delta = mRandom.nextLong(WAKEUP_RETENTION_MS); + final long delta = mRandom.nextLong(retention); if (timestamps.add(firstWakeup + delta)) { - obj.noteWakeupTimeAndReason(firstWakeup + delta, i, "unused"); + obj.noteWakeupTimeAndReason(firstWakeup + delta, i, KERNEL_REASON_UNKNOWN_IRQ); } } assertThat(obj.mWakeupEvents.size()).isEqualTo(timestamps.size()); - obj.noteWakeupTimeAndReason(firstWakeup + WAKEUP_RETENTION_MS + 1242, 231, "unused"); + obj.noteWakeupTimeAndReason(firstWakeup + retention + 1242, 231, + KERNEL_REASON_UNKNOWN_IRQ); assertThat(obj.mWakeupEvents.size()).isEqualTo(timestamps.size()); for (int i = 0; i < 100; i++) { - final long now = mRandom.nextLong(WAKEUP_RETENTION_MS + 1, 100 * WAKEUP_RETENTION_MS); - obj.noteWakeupTimeAndReason(now, i, "unused"); - assertThat(obj.mWakeupEvents.closestIndexOnOrBefore(now - WAKEUP_RETENTION_MS)) - .isLessThan(0); + final long now = mRandom.nextLong(retention + 1, 100 * retention); + obj.noteWakeupTimeAndReason(now, i, KERNEL_REASON_UNKNOWN_IRQ); + assertThat(obj.mWakeupEvents.closestIndexOnOrBefore(now - retention)).isLessThan(0); } } @@ -111,17 +113,45 @@ public class CpuWakeupStatsTest { assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_1)).isEqualTo(false); assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_2)).isEqualTo(false); assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_3)).isEqualTo(true); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_4)).isEqualTo(false); assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_5)).isEqualTo(true); } @Test - public void alarmIrqAttributionCombined() { + public void wifiIrqAttributionSolo() { + final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); + final long wakeupTime = 12423121; + + obj.noteWakeupTimeAndReason(wakeupTime, 1, KERNEL_REASON_WIFI_IRQ); + + // Outside the window, so should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, + wakeupTime - WAKEUP_REASON_HALF_WINDOW_MS - 1, TEST_UID_1); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, + wakeupTime + WAKEUP_REASON_HALF_WINDOW_MS + 1, TEST_UID_2); + // Should be attributed + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime + 3, TEST_UID_4, TEST_UID_5); + + final SparseArray<SparseBooleanArray> attribution = obj.mWakeupAttribution.get(wakeupTime); + assertThat(attribution).isNotNull(); + assertThat(attribution.size()).isEqualTo(1); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_WIFI)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_1)).isEqualTo(false); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_2)).isEqualTo(false); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_3)).isEqualTo(false); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_4)).isEqualTo(true); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_5)).isEqualTo(true); + } + + @Test + public void alarmAndWifiIrqAttribution() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long wakeupTime = 92123210; obj.noteWakeupTimeAndReason(wakeupTime, 4, - KERNEL_REASON_UNKNOWN_IRQ + ":" + KERNEL_REASON_ALARM_IRQ); + KERNEL_REASON_WIFI_IRQ + ":" + KERNEL_REASON_ALARM_IRQ); + // Alarm activity // Outside the window, so should be ignored. obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - WAKEUP_REASON_HALF_WINDOW_MS - 1, TEST_UID_1); @@ -132,16 +162,34 @@ public class CpuWakeupStatsTest { obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4, TEST_UID_5); + // Wifi activity + // Outside the window, so should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, + wakeupTime - WAKEUP_REASON_HALF_WINDOW_MS - 1, TEST_UID_4); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, + wakeupTime + WAKEUP_REASON_HALF_WINDOW_MS + 1, TEST_UID_3); + // Should be attributed + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime + 2, TEST_UID_1); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime - 1, TEST_UID_2, + TEST_UID_5); + final SparseArray<SparseBooleanArray> attribution = obj.mWakeupAttribution.get(wakeupTime); assertThat(attribution).isNotNull(); assertThat(attribution.size()).isEqualTo(2); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isTrue(); assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_1)).isEqualTo(false); assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_2)).isEqualTo(false); assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_3)).isEqualTo(true); assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_4)).isEqualTo(true); assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_ALARM).get(TEST_UID_5)).isEqualTo(true); - assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue(); + + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_WIFI)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_1)).isEqualTo(true); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_2)).isEqualTo(true); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_3)).isEqualTo(false); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_4)).isEqualTo(false); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_5)).isEqualTo(true); } @Test @@ -151,9 +199,11 @@ public class CpuWakeupStatsTest { obj.noteWakeupTimeAndReason(wakeupTime, 24, KERNEL_REASON_UNKNOWN_IRQ); + assertThat(obj.mWakeupEvents.size()).isEqualTo(1); + // Unrelated subsystems, should not be attributed obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); - obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4, + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime - 3, TEST_UID_4, TEST_UID_5); final SparseArray<SparseBooleanArray> attribution = obj.mWakeupAttribution.get(wakeupTime); @@ -165,42 +215,48 @@ public class CpuWakeupStatsTest { } @Test - public void unknownAttribution() { + public void unknownWakeupIgnored() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long wakeupTime = 72123210; obj.noteWakeupTimeAndReason(wakeupTime, 34, KERNEL_REASON_UNKNOWN); - // Should be ignored as this type of wakeup is unsupported. + // Should be ignored as this type of wakeup is not known. + assertThat(obj.mWakeupEvents.size()).isEqualTo(0); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4); - // There should be nothing in the attribution map. + // Any nearby activity should not end up in the attribution map. assertThat(obj.mWakeupAttribution.size()).isEqualTo(0); } @Test - public void unsupportedAttribution() { + public void unsupportedWakeupIgnored() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); long wakeupTime = 970934; obj.noteWakeupTimeAndReason(wakeupTime, 34, KERNEL_REASON_UNSUPPORTED); // Should be ignored as this type of wakeup is unsupported. + assertThat(obj.mWakeupEvents.size()).isEqualTo(0); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4); - // There should be nothing in the attribution map. + // Any nearby activity should not end up in the attribution map. assertThat(obj.mWakeupAttribution.size()).isEqualTo(0); wakeupTime = 883124; obj.noteWakeupTimeAndReason(wakeupTime, 3, KERNEL_REASON_ABORT); // Should be ignored as this type of wakeup is unsupported. - obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 2, TEST_UID_1, TEST_UID_4); - obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 5, TEST_UID_3); + assertThat(obj.mWakeupEvents.size()).isEqualTo(0); + + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime + 2, TEST_UID_1, TEST_UID_4); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime - 5, TEST_UID_3); - // There should be nothing in the attribution map. + // Any nearby activity should not end up in the attribution map. assertThat(obj.mWakeupAttribution.size()).isEqualTo(0); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index 6f37e601abce..ce076217f37b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -24,6 +24,8 @@ import static android.service.notification.NotificationListenerService.META_DATA import static com.android.server.notification.ManagedServices.APPROVAL_BY_COMPONENT; import static com.android.server.notification.ManagedServices.APPROVAL_BY_PACKAGE; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; @@ -1201,28 +1203,11 @@ public class ManagedServicesTest extends UiServiceTestCase { mIpm, approvalLevel); loadXml(service); - List<String> allowedPackagesForUser0 = new ArrayList<>(); - allowedPackagesForUser0.add("this.is.a.package.name"); - allowedPackagesForUser0.add("another.package"); - allowedPackagesForUser0.add("secondary"); - - List<String> actual = service.getAllowedPackages(0); - assertEquals(3, actual.size()); - for (String pkg : allowedPackagesForUser0) { - assertTrue(actual.contains(pkg)); - } - - List<String> allowedPackagesForUser10 = new ArrayList<>(); - allowedPackagesForUser10.add("this.is.another.package"); - allowedPackagesForUser10.add("package"); - allowedPackagesForUser10.add("this.is.another.package"); - allowedPackagesForUser10.add("component"); - - actual = service.getAllowedPackages(10); - assertEquals(4, actual.size()); - for (String pkg : allowedPackagesForUser10) { - assertTrue(actual.contains(pkg)); - } + assertThat(service.getAllowedPackages(0)).containsExactly("this.is.a.package.name", + "another.package", "secondary"); + assertThat(service.getAllowedPackages(10)).containsExactly("this.is.another.package", + "package", "this.is.another.package", "component"); + assertThat(service.getAllowedPackages(999)).isEmpty(); } } @@ -1263,31 +1248,6 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test - public void testGetAllowedPackages() throws Exception { - ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, - mIpm, APPROVAL_BY_COMPONENT); - loadXml(service); - service.mApprovalLevel = APPROVAL_BY_PACKAGE; - loadXml(service); - - List<String> allowedPackages = new ArrayList<>(); - allowedPackages.add("this.is.a.package.name"); - allowedPackages.add("another.package"); - allowedPackages.add("secondary"); - allowedPackages.add("this.is.another.package"); - allowedPackages.add("package"); - allowedPackages.add("component"); - allowedPackages.add("bananas!"); - allowedPackages.add("non.user.set.package"); - - Set<String> actual = service.getAllowedPackages(); - assertEquals(allowedPackages.size(), actual.size()); - for (String pkg : allowedPackages) { - assertTrue(actual.contains(pkg)); - } - } - - @Test public void testOnUserRemoved() throws Exception { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index f08d0f5f71a4..354420f46c2f 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -29,6 +29,7 @@ import static android.app.Notification.FLAG_FOREGROUND_SERVICE; import static android.app.Notification.FLAG_NO_CLEAR; import static android.app.Notification.FLAG_ONGOING_EVENT; import static android.app.NotificationChannel.USER_LOCKED_ALLOW_BUBBLE; +import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED; import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL; import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; @@ -50,7 +51,6 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; - import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_MUTABLE; import static android.app.PendingIntent.FLAG_ONE_SHOT; @@ -237,6 +237,7 @@ import com.android.server.utils.quota.MultiRateLimiter; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; +import com.google.android.collect.Lists; import com.google.common.collect.ImmutableList; import org.junit.After; @@ -245,10 +246,13 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.ArgumentMatchers; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.io.BufferedInputStream; @@ -440,6 +444,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mContext.addMockSystemService(Context.ALARM_SERVICE, mAlarmManager); mContext.addMockSystemService(NotificationManager.class, mMockNm); + doNothing().when(mContext).sendBroadcastAsUser(any(), any()); doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any()); setDpmAppOppsExemptFromDismissal(false); @@ -7828,6 +7833,75 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void onZenModeChanged_sendsBroadcasts() throws Exception { + when(mAmi.getCurrentUserId()).thenReturn(100); + when(mUmInternal.getProfileIds(eq(100), anyBoolean())).thenReturn(new int[]{100, 101, 102}); + when(mConditionProviders.getAllowedPackages(anyInt())).then(new Answer<List<String>>() { + @Override + public List<String> answer(InvocationOnMock invocation) { + int userId = invocation.getArgument(0); + switch (userId) { + case 100: + return Lists.newArrayList("a", "b", "c"); + case 101: + return Lists.newArrayList(); + case 102: + return Lists.newArrayList("b"); + default: + throw new IllegalArgumentException( + "Why would you ask for packages of userId " + userId + "?"); + } + } + }); + + mService.getBinderService().setZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS, null, + "testing!"); + waitForIdle(); + + InOrder inOrder = inOrder(mContext); + // Verify broadcasts for registered receivers + inOrder.verify(mContext).sendBroadcastAsUser(eqIntent( + new Intent(ACTION_INTERRUPTION_FILTER_CHANGED).setFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY)), eq(UserHandle.of(100)), eq(null)); + inOrder.verify(mContext).sendBroadcastAsUser(eqIntent( + new Intent(ACTION_INTERRUPTION_FILTER_CHANGED).setFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY)), eq(UserHandle.of(101)), eq(null)); + inOrder.verify(mContext).sendBroadcastAsUser(eqIntent( + new Intent(ACTION_INTERRUPTION_FILTER_CHANGED).setFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY)), eq(UserHandle.of(102)), eq(null)); + + // Verify broadcast for packages that manage DND. + inOrder.verify(mContext).sendBroadcastAsUser(eqIntent(new Intent( + ACTION_INTERRUPTION_FILTER_CHANGED).setPackage("a").setFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)), eq(UserHandle.of(100))); + inOrder.verify(mContext).sendBroadcastAsUser(eqIntent(new Intent( + ACTION_INTERRUPTION_FILTER_CHANGED).setPackage("b").setFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)), eq(UserHandle.of(100))); + inOrder.verify(mContext).sendBroadcastAsUser(eqIntent(new Intent( + ACTION_INTERRUPTION_FILTER_CHANGED).setPackage("c").setFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)), eq(UserHandle.of(100))); + inOrder.verify(mContext).sendBroadcastAsUser(eqIntent(new Intent( + ACTION_INTERRUPTION_FILTER_CHANGED).setPackage("b").setFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)), eq(UserHandle.of(102))); + } + + private static Intent eqIntent(Intent wanted) { + return ArgumentMatchers.argThat( + new ArgumentMatcher<Intent>() { + @Override + public boolean matches(Intent argument) { + return wanted.filterEquals(argument) + && wanted.getFlags() == argument.getFlags(); + } + + @Override + public String toString() { + return wanted.toString(); + } + }); + } + + @Test public void testAreNotificationsEnabledForPackage() throws Exception { mBinderService.areNotificationsEnabledForPackage(mContext.getPackageName(), mUid); 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 6661e6a0a60d..49f215a83c2b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2897,7 +2897,7 @@ public class ActivityRecordTests extends WindowTestsBase { // Make the top one invisible, and try transferring the starting window from the top to the // bottom one. - activityTop.setVisibility(false, false); + activityTop.setVisibility(false); activityBottom.transferStartingWindowFromHiddenAboveTokenIfNeeded(); waitUntilHandlersIdle(); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java index 4d71b30d71e2..6d1312458ce2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java @@ -165,7 +165,7 @@ public class AppChangeTransitionTests extends WindowTestsBase { assertTrue(mTask.isInChangeTransition()); // Changing visibility should cancel the change transition and become closing - mActivity.setVisibility(false, false); + mActivity.setVisibility(false); assertEquals(0, mDisplayContent.mChangingContainers.size()); assertFalse(mTask.isInChangeTransition()); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java index 6b814e678419..59cc4f5b0948 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java @@ -295,8 +295,8 @@ public class AppTransitionTests extends WindowTestsBase { dc2.prepareAppTransition(TRANSIT_CLOSE); // One activity window is visible for resuming & the other activity window is invisible // for finishing in different display. - activity1.setVisibility(true, false); - activity2.setVisibility(false, false); + activity1.setVisibility(true); + activity2.setVisibility(false); // Make sure each display is in animating stage. assertTrue(dc1.mOpeningApps.size() > 0); @@ -365,7 +365,7 @@ public class AppTransitionTests extends WindowTestsBase { dc.prepareAppTransition(TRANSIT_CLOSE); assertTrue(dc.mAppTransition.containsTransitRequest(TRANSIT_CLOSE)); dc.mAppTransition.overridePendingAppTransitionRemote(adapter); - exitingActivity.setVisibility(false, false); + exitingActivity.setVisibility(false); assertTrue(dc.mClosingApps.size() > 0); // Make sure window is in animating stage before freeze, and cancel after freeze. diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index aaeae239024d..3379bebe6698 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1620,7 +1620,6 @@ public class DisplayContentTests extends WindowTestsBase { // If the rotated activity requests to show IME, the IME window should use the // transformation from activity to lay out in the same orientation. - mDisplayContent.setImeLayeringTarget(mAppWindow); LocalServices.getService(WindowManagerInternal.class).onToggleImeRequested(true /* show */, app.token, app.token, mDisplayContent.mDisplayId); assertTrue(asyncRotationController.isTargetToken(mImeWindow.mToken)); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index df3af7d2f4fa..49d8da1b2880 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -351,8 +351,8 @@ public class TaskFragmentTest extends WindowTestsBase { final TaskFragment taskFragment1 = createTaskFragmentWithEmbeddedActivity(task, mOrganizer); final ActivityRecord activity0 = taskFragment0.getTopMostActivity(); final ActivityRecord activity1 = taskFragment1.getTopMostActivity(); - activity0.setVisibility(true /* visible */, false /* deferHidingClient */); - activity1.setVisibility(true /* visible */, false /* deferHidingClient */); + activity0.setVisibility(true); + activity1.setVisibility(true); spyOn(mAtm.mTaskFragmentOrganizerController); // Move activity to pinned. diff --git a/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java index 2fccb88ad8de..75a8dd822914 100644 --- a/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java @@ -92,11 +92,11 @@ public class UnknownAppVisibilityControllerTest extends WindowTestsBase { @Test public void testRemoveFinishingInvisibleActivityFromUnknown() { - final ActivityRecord activity = createNonAttachedActivityRecord(mDisplayContent); + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); mDisplayContent.mUnknownAppVisibilityController.notifyLaunched(activity); activity.finishing = true; activity.setVisibleRequested(true); - activity.setVisibility(false, false); + activity.setVisibility(false); assertTrue(mDisplayContent.mUnknownAppVisibilityController.allResolved()); } 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 3aee2cd5fac1..33067df0c8f3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -914,15 +914,15 @@ public class WindowStateTests extends WindowTestsBase { final WindowState app = createWindow(null, TYPE_APPLICATION, "app", uid); app.mActivityRecord.setVisible(false); - app.mActivityRecord.setVisibility(false /* visible */, false /* deferHidingClient */); + app.mActivityRecord.setVisibility(false); assertFalse(mAtm.hasActiveVisibleWindow(uid)); - app.mActivityRecord.setVisibility(true /* visible */, false /* deferHidingClient */); + app.mActivityRecord.setVisibility(true); assertTrue(mAtm.hasActiveVisibleWindow(uid)); // Make the activity invisible and add a visible toast. The uid should have no active // visible window because toast can be misused by legacy app to bypass background check. - app.mActivityRecord.setVisibility(false /* visible */, false /* deferHidingClient */); + app.mActivityRecord.setVisibility(false); final WindowState overlay = createWindow(null, TYPE_APPLICATION_OVERLAY, "overlay", uid); final WindowState toast = createWindow(null, TYPE_TOAST, app.mToken, "toast", uid); toast.onSurfaceShownChanged(true); @@ -1157,12 +1157,12 @@ public class WindowStateTests extends WindowTestsBase { public void testRequestedVisibility() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); app.mActivityRecord.setVisible(false); - app.mActivityRecord.setVisibility(false /* visible */, false /* deferHidingClient */); + app.mActivityRecord.setVisibility(false); assertFalse(app.isVisibleRequested()); // It doesn't have a surface yet, but should still be visible requested. app.setHasSurface(false); - app.mActivityRecord.setVisibility(true /* visible */, false /* deferHidingClient */); + app.mActivityRecord.setVisibility(true); assertFalse(app.isVisible()); assertTrue(app.isVisibleRequested()); diff --git a/services/usb/java/com/android/server/usb/hal/gadget/UsbGadgetHidl.java b/services/usb/java/com/android/server/usb/hal/gadget/UsbGadgetHidl.java index 2c5fcb86b0c5..68067d27a0a5 100644 --- a/services/usb/java/com/android/server/usb/hal/gadget/UsbGadgetHidl.java +++ b/services/usb/java/com/android/server/usb/hal/gadget/UsbGadgetHidl.java @@ -191,9 +191,9 @@ public final class UsbGadgetHidl implements UsbGadgetHal { public void reset(long transactionId) { try { synchronized(mGadgetProxyLock) { - if (android.hardware.usb.gadget.V1_2.IUsbGadget.castFrom(mGadgetProxy) != null) { - android.hardware.usb.gadget.V1_2.IUsbGadget gadgetProxy = - android.hardware.usb.gadget.V1_2.IUsbGadget.castFrom(mGadgetProxy); + if (android.hardware.usb.gadget.V1_1.IUsbGadget.castFrom(mGadgetProxy) != null) { + android.hardware.usb.gadget.V1_1.IUsbGadget gadgetProxy = + android.hardware.usb.gadget.V1_1.IUsbGadget.castFrom(mGadgetProxy); gadgetProxy.reset(); } } diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 918ae7901dbe..bf12b9cce302 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -7608,6 +7608,55 @@ public class CarrierConfigManager { public static final String KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL = KEY_PREFIX + "emergency_requires_volte_enabled_bool"; + /** + * This values indicates that the cross SIM redialing timer shall be disabled. + * + * @see #KEY_CROSS_STACK_REDIAL_TIMER_SEC_INT + * @see #KEY_QUICK_CROSS_STACK_REDIAL_TIMER_SEC_INT + * @hide + */ + public static final int REDIAL_TIMER_DISABLED = 0; + + /** + * A timer to guard the max attempting time on current SIM slot so that modem will not + * stuck in current SIM slot for long time. On timer expiry, if emergency call on the + * other SIM slot is preferable, telephony shall cancel the emergency call and place the + * call on the other SIM slot. If this value is set to {@link #REDIAL_TIMER_DISABLED}, then + * the timer will never be started and domain selection continues on the current SIM slot. + * This value should be greater than the value of {@link #KEY_EMERGENCY_SCAN_TIMER_SEC_INT}. + * + * The default value for the timer is 120 seconds. + * @hide + */ + public static final String KEY_CROSS_STACK_REDIAL_TIMER_SEC_INT = + KEY_PREFIX + "cross_stack_redial_timer_sec_int"; + + /** + * If emergency calls are only allowed with normal-registered service and UE should get + * normal service in a short time with acquired band information, telephony + * expects dialing emergency call will be completed in a short time. + * If dialing is not completed with in a certain timeout, telephony shall place on + * another SIM slot. If this value is set to {@link #REDIAL_TIMER_DISABLED}, then the timer + * will never be started and domain selection continues on the current SIM slot. + * The timer shall be started for the first trial of each subscription and shall be ignored + * in the roaming networks and non-domestic networks. + * + * The default value for the timer is {@link #REDIAL_TIMER_DISABLED}. + * @hide + */ + public static final String KEY_QUICK_CROSS_STACK_REDIAL_TIMER_SEC_INT = + KEY_PREFIX + "quick_cross_stack_redial_timer_sec_int"; + + /** + * Indicates whether the quick cross stack redial timer will be triggered only when + * the device is registered to the network. + * + * The default value is {@code true}. + * @hide + */ + public static final String KEY_START_QUICK_CROSS_STACK_REDIAL_TIMER_WHEN_REGISTERED_BOOL = + KEY_PREFIX + "start_quick_cross_stack_redial_timer_when_registered_bool"; + private static PersistableBundle getDefaults() { PersistableBundle defaults = new PersistableBundle(); defaults.putBoolean(KEY_RETRY_EMERGENCY_ON_IMS_PDN_BOOL, false); @@ -7674,6 +7723,10 @@ public class CarrierConfigManager { defaults.putBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL, false); defaults.putStringArray(KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY, new String[0]); + defaults.putInt(KEY_CROSS_STACK_REDIAL_TIMER_SEC_INT, 120); + defaults.putInt(KEY_QUICK_CROSS_STACK_REDIAL_TIMER_SEC_INT, REDIAL_TIMER_DISABLED); + defaults.putBoolean(KEY_START_QUICK_CROSS_STACK_REDIAL_TIMER_WHEN_REGISTERED_BOOL, + true); return defaults; } diff --git a/telephony/java/android/telephony/PreciseDisconnectCause.java b/telephony/java/android/telephony/PreciseDisconnectCause.java index 3b4cf75e7919..1cfd22cc4d65 100644 --- a/telephony/java/android/telephony/PreciseDisconnectCause.java +++ b/telephony/java/android/telephony/PreciseDisconnectCause.java @@ -235,6 +235,23 @@ public final class PreciseDisconnectCause { /** Call failed/dropped because of a network detach. */ public static final int NETWORK_DETACH = 261; + /** + * Dialing emergency calls is currently unavailable. + * The call should be redialed on the other subscription silently. + * If there is no other subscription available, the call may be redialed + * on this subscription again. + * @hide + */ + public static final int EMERGENCY_TEMP_FAILURE = 325; + /** + * Dialing emergency calls is currently unavailable. + * The call should be redialed on the other subscription silently. + * Even if there is no other subscription available, the call should not + * be redialed on this subscription again. + * @hide + */ + public static final int EMERGENCY_PERM_FAILURE = 326; + /** Mobile station (MS) is locked until next power cycle. */ public static final int CDMA_LOCKED_UNTIL_POWER_CYCLE = 1000; /** Drop call. */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt index 9dc4bf034e66..9c3460c124ba 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt @@ -317,7 +317,8 @@ fun FlickerTest.replacesLayer( assertion.then().isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true) } if (ignoreSplashscreen) { - assertion.then().isSplashScreenVisibleFor(newLayer, isOptional = true) + assertion.then().isSplashScreenVisibleFor( + ComponentNameMatcher(newLayer.packageName, className = ""), isOptional = true) } assertion.then().isVisible(newLayer) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt index 19ecf6ab8799..05abf9fd1a8e 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt @@ -53,7 +53,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class OpenAppColdFromIcon(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) { +open class OpenAppColdFromIcon(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) { /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIconCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIconCfArm.kt new file mode 100644 index 000000000000..46899f373fcf --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIconCfArm.kt @@ -0,0 +1,51 @@ +/* + * 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.launch + +import android.tools.common.NavBar +import android.tools.device.flicker.annotation.FlickerServiceCompatible +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** Some assertions will fail because of b/264415996 */ +@FlickerServiceCompatible +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class OpenAppColdFromIconCfArm(flicker: FlickerTest) : OpenAppColdFromIcon(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> { + // TAPL fails on landscape mode b/240916028 + return FlickerTestFactory.nonRotationTests( + supportedNavigationModes = listOf(NavBar.MODE_3BUTTON) + ) + } + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraOnDoubleClickPowerButton.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraOnDoubleClickPowerButton.kt index 6005a81aac9e..786bb32096ad 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraOnDoubleClickPowerButton.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraOnDoubleClickPowerButton.kt @@ -16,9 +16,10 @@ package com.android.server.wm.flicker.launch +import android.os.SystemClock import android.platform.test.annotations.Postsubmit import android.tools.device.apphelpers.CameraAppHelper -import android.tools.device.flicker.annotation.FlickerServiceCompatible +import android.tools.device.apphelpers.StandardAppHelper import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTest @@ -54,13 +55,14 @@ import org.junit.runners.Parameterized * ``` */ @RequiresDevice -@FlickerServiceCompatible @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class OpenCameraOnDoubleClickPowerButton(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) { private val cameraApp = CameraAppHelper(instrumentation) + override val testApp: StandardAppHelper + get() = cameraApp override val transition: FlickerBuilder.() -> Unit get() = { @@ -70,6 +72,7 @@ class OpenCameraOnDoubleClickPowerButton(flicker: FlickerTest) : } transitions { device.pressKeyCode(KeyEvent.KEYCODE_POWER) + SystemClock.sleep(100) device.pressKeyCode(KeyEvent.KEYCODE_POWER) wmHelper.StateSyncBuilder().withWindowSurfaceAppeared(cameraApp).waitForAndVerify() } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt index da985232a1e3..b848e63c9c87 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt @@ -93,8 +93,7 @@ class OverrideTaskTransitionTest(val flicker: FlickerTest) { .then() // Animation starts, but the app may not be drawn yet which means the Splash // may be visible. - .isInvisible(testApp, isOptional = true) - .isVisible(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true) + .isSplashScreenVisibleFor(testApp, isOptional = true) .then() // App shows up with the custom animation starting at alpha=1. .isVisible(testApp) diff --git a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt index 8b1b06fe40e8..24a567130ff0 100644 --- a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt +++ b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt @@ -124,14 +124,12 @@ class MotionPredictorTest { predictor.record(moveEvent) val predicted = predictor.predict(Duration.ofMillis(8).toNanos()) - assertEquals(1, predicted.size) - val event = predicted[0] - assertNotNull(event) + assertNotNull(predicted) // Prediction will happen for t=12 (since it is the next input interval after the requested // time, 8, plus the model offset, 1). - assertEquals(12, event.eventTime) - assertEquals(30f, event.x, /*delta=*/5f) - assertEquals(60f, event.y, /*delta=*/15f) + assertEquals(12, predicted!!.eventTime) + assertEquals(30f, predicted.x, /*delta=*/5f) + assertEquals(60f, predicted.y, /*delta=*/15f) } } diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/NotificationTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/NotificationTest.java index 573b3b695a90..d2708ad47712 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/NotificationTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/NotificationTest.java @@ -42,6 +42,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.uiautomator.By; import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; import androidx.test.uiautomator.Until; import org.junit.After; @@ -74,7 +75,7 @@ public final class NotificationTest { // This is for AOSP System UI for phones. When testing customized System UI, please modify here. private static final BySelector REPLY_SEND_BUTTON_SELECTOR = - By.res("com.android.systemui", "remote_input_send"); + By.res("com.android.systemui", "remote_input_send").enabled(true); @Rule public UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule(); @@ -119,7 +120,15 @@ public final class NotificationTest { mUiDevice.pressKeyCode(KeyEvent.KEYCODE_A); mUiDevice.pressKeyCode(KeyEvent.KEYCODE_B); mUiDevice.pressKeyCode(KeyEvent.KEYCODE_C); - mUiDevice.wait(Until.findObject(REPLY_SEND_BUTTON_SELECTOR.enabled(true)), TIMEOUT).click(); + UiObject2 sendButton = mUiDevice.wait( + Until.findObject(REPLY_SEND_BUTTON_SELECTOR), TIMEOUT); + if (sendButton == null) { + // If the screen is too small, sendButton may be hidden by IME. + // Dismiss IME and try again. + mUiDevice.pressBack(); + sendButton = mUiDevice.wait(Until.findObject(REPLY_SEND_BUTTON_SELECTOR), TIMEOUT); + } + sendButton.click(); // Verify that IME is gone. assertThat(mUiDevice.wait(Until.gone(By.pkg(getImePackage(mContext))), TIMEOUT)).isTrue(); } diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java index 2cdb94526839..7deb8c73d1fc 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java @@ -86,7 +86,7 @@ public class ProtoLogImplTest { mFile = testContext.getFileStreamPath("tracing_test.dat"); //noinspection ResultOfMethodCallIgnored mFile.delete(); - mProtoLog = new ProtoLogImpl(mFile, 1024 * 1024, mReader); + mProtoLog = new ProtoLogImpl(mFile, 1024 * 1024, mReader, 1024); } @After diff --git a/tests/MotionPrediction/src/test/motionprediction/DrawingView.kt b/tests/MotionPrediction/src/test/motionprediction/DrawingView.kt index f529bf77f32a..229d0c8da6e9 100644 --- a/tests/MotionPrediction/src/test/motionprediction/DrawingView.kt +++ b/tests/MotionPrediction/src/test/motionprediction/DrawingView.kt @@ -97,8 +97,8 @@ class DrawingView(context: Context, attrs: AttributeSet) : View(context, attrs) } // Draw predictions. Convert to nanos and hardcode to +20ms into the future - val predictionList = predictor.predict(eventTime * 1000000 + 20000000) - for (prediction in predictionList) { + val prediction = predictor.predict(eventTime * 1000000 + 20000000) + if (prediction != null) { val realEvents = events.get(prediction.deviceId)!! drawLine(canvas, realEvents[realEvents.size - 1], prediction, predictionPaint) } diff --git a/tools/codegen/Android.bp b/tools/codegen/Android.bp index e53ba3e18a86..a1df878df12e 100644 --- a/tools/codegen/Android.bp +++ b/tools/codegen/Android.bp @@ -9,7 +9,7 @@ package { java_binary_host { name: "codegen_cli", - manifest: "manifest.txt", + main_class: "com.android.codegen.MainKt", srcs: [ "src/**/*.kt", ], diff --git a/tools/codegen/BUILD.bazel b/tools/codegen/BUILD.bazel deleted file mode 100644 index c14046d674dc..000000000000 --- a/tools/codegen/BUILD.bazel +++ /dev/null @@ -1,21 +0,0 @@ -# TODO(b/245731902): auto-generate these with bp2build. -load("@rules_kotlin//kotlin:jvm_library.bzl", "kt_jvm_library") - -java_binary( - name = "codegen_cli", - main_class = "com.android.codegen.MainKt", - runtime_deps = [ - ":codegen_cli_kt_lib", - ], -) - -kt_jvm_library( - name = "codegen_cli_kt_lib", - srcs = glob(["src/**/*.kt"]), - deps = ["//external/javaparser"], -) - -kt_jvm_library( - name = "codegen-version-info", - srcs = glob(["src/**/SharedConstants.kt"]), -) diff --git a/tools/codegen/manifest.txt b/tools/codegen/manifest.txt deleted file mode 100644 index 6e1018ba6b55..000000000000 --- a/tools/codegen/manifest.txt +++ /dev/null @@ -1 +0,0 @@ -Main-class: com.android.codegen.MainKt |