diff options
688 files changed, 18181 insertions, 6718 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..73dbd286229d 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"], } @@ -261,3 +263,15 @@ genrule { out: ["combined-removed-dex.txt"], cmd: "$(location gen_combined_removed_dex.sh) $(location metalava) $(genDir) $(in) > $(out)", } + +java_genrule { + name: "api_fingerprint", + srcs: [ + ":frameworks-base-api-current.txt", + ":frameworks-base-api-system-current.txt", + ":frameworks-base-api-module-lib-current.txt", + ":frameworks-base-api-system-server-current.txt", + ], + out: ["api_fingerprint.txt"], + cmd: "cat $(in) | md5sum | cut -d' ' -f1 > $(out)", +} 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 6d89717f47d1..37480520529b 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); } @@ -11158,6 +11157,7 @@ package android.content { method public final String getDataScheme(int); method public final android.os.PatternMatcher getDataSchemeSpecificPart(int); method public final String getDataType(int); + method @NonNull public final android.os.PersistableBundle getExtras(); method public final int getPriority(); method public final boolean hasAction(String); method public final boolean hasCategory(String); @@ -11176,6 +11176,7 @@ package android.content { method public void readFromXml(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; method public final java.util.Iterator<android.os.PatternMatcher> schemeSpecificPartsIterator(); method public final java.util.Iterator<java.lang.String> schemesIterator(); + method public final void setExtras(@NonNull android.os.PersistableBundle); method public final void setPriority(int); method public final java.util.Iterator<java.lang.String> typesIterator(); method public final void writeToParcel(android.os.Parcel, int); @@ -15485,14 +15486,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 +19594,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 +19609,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); @@ -20388,7 +20389,7 @@ package android.location { public final class GnssCapabilities implements android.os.Parcelable { method public int describeContents(); method @NonNull public java.util.List<android.location.GnssSignalType> getGnssSignalTypes(); - method public boolean hasAccumulatedDeltaRange(); + method public int hasAccumulatedDeltaRange(); method public boolean hasAntennaInfo(); method public boolean hasGeofencing(); method @Deprecated public boolean hasGnssAntennaInfo(); @@ -20414,8 +20415,10 @@ package android.location { method public boolean hasSatellitePvt(); method public boolean hasScheduling(); method public boolean hasSingleShotFix(); - method public boolean isAccumulatedDeltaRangeCapabilityKnown(); method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int CAPABILITY_SUPPORTED = 1; // 0x1 + field public static final int CAPABILITY_UNKNOWN = 0; // 0x0 + field public static final int CAPABILITY_UNSUPPORTED = 2; // 0x2 field @NonNull public static final android.os.Parcelable.Creator<android.location.GnssCapabilities> CREATOR; } @@ -20423,9 +20426,8 @@ package android.location { ctor public GnssCapabilities.Builder(); ctor public GnssCapabilities.Builder(@NonNull android.location.GnssCapabilities); method @NonNull public android.location.GnssCapabilities build(); - method @NonNull public android.location.GnssCapabilities.Builder clearIsAccumulatedDeltaRangeCapabilityKnown(); method @NonNull public android.location.GnssCapabilities.Builder setGnssSignalTypes(@NonNull java.util.List<android.location.GnssSignalType>); - method @NonNull public android.location.GnssCapabilities.Builder setHasAccumulatedDeltaRange(boolean); + method @NonNull public android.location.GnssCapabilities.Builder setHasAccumulatedDeltaRange(int); method @NonNull public android.location.GnssCapabilities.Builder setHasAntennaInfo(boolean); method @NonNull public android.location.GnssCapabilities.Builder setHasGeofencing(boolean); method @NonNull public android.location.GnssCapabilities.Builder setHasLowPowerMode(boolean); @@ -27308,6 +27310,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 @@ -27436,6 +27439,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); } @@ -36889,6 +36893,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; @@ -39765,6 +39770,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); @@ -40548,7 +40554,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 { @@ -40597,7 +40603,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 { @@ -40639,9 +40645,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); @@ -51442,7 +51450,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..5dbed3444f73 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); @@ -3579,9 +3581,7 @@ package android.content { } public class IntentFilter implements android.os.Parcelable { - method @NonNull public final android.os.PersistableBundle getExtras(); method public final int getOrder(); - method public final void setExtras(@NonNull android.os.PersistableBundle); method public final void setOrder(int); } @@ -13166,7 +13166,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 +13238,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 +13260,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..03bdb15b1ddf 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 { @@ -1110,7 +1145,6 @@ package android.credentials.ui { public final class Entry implements android.os.Parcelable { ctor public Entry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice); - ctor public Entry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice, @NonNull android.app.PendingIntent, @NonNull android.content.Intent); ctor public Entry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice, @NonNull android.content.Intent); method public int describeContents(); method @Nullable public android.content.Intent getFrameworkExtrasIntent(); @@ -1921,6 +1955,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 +2667,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 +2682,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 +2862,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 +3341,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 +3471,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 +3976,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/IntentFilter.java b/core/java/android/content/IntentFilter.java index afc2285c62c3..5928a505d06b 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -2204,9 +2204,7 @@ public class IntentFilter implements Parcelable { * <p> Subsequent calls to this method will override any previously set extras. * * @param extras The intent extras to match against. - * @hide */ - @SystemApi public final void setExtras(@NonNull PersistableBundle extras) { mExtras = extras; } @@ -2216,11 +2214,8 @@ public class IntentFilter implements Parcelable { * * @return the extras that were previously set using {@link #setExtras(PersistableBundle)} or * an empty {@link PersistableBundle} object if no extras were set. - * @hide */ - @SystemApi - @NonNull - public final PersistableBundle getExtras() { + public final @NonNull PersistableBundle getExtras() { return mExtras == null ? new PersistableBundle() : mExtras; } 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/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java index 12665bad53a9..55f2a3eb490b 100644 --- a/core/java/android/credentials/ui/Entry.java +++ b/core/java/android/credentials/ui/Entry.java @@ -70,17 +70,6 @@ public final class Entry implements Parcelable { /** Constructor to be used for an entry that requires a pending intent to be invoked * when clicked. */ - // TODO: Remove this constructor as it is no longer used - public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice, - @NonNull PendingIntent pendingIntent, @NonNull Intent intent) { - this(key, subkey, slice); - mPendingIntent = pendingIntent; - mFrameworkExtrasIntent = intent; - } - - /** Constructor to be used for an entry that requires a pending intent to be invoked - * when clicked. - */ public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice, @NonNull Intent intent) { this(key, subkey, slice); 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/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/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java index 0a2b06ca7e70..867d2b462565 100644 --- a/core/java/android/view/InsetsFrameProvider.java +++ b/core/java/android/view/InsetsFrameProvider.java @@ -18,10 +18,14 @@ package android.view; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; +import android.annotation.IntRange; +import android.annotation.NonNull; import android.graphics.Insets; import android.graphics.Rect; +import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.view.WindowInsets.Type.InsetsType; import java.util.Arrays; import java.util.Objects; @@ -62,19 +66,18 @@ public class InsetsFrameProvider implements Parcelable { private static final int HAS_INSETS_SIZE = 1; private static final int HAS_INSETS_SIZE_OVERRIDE = 2; - private static Rect sTmpRect = new Rect(); - private static Rect sTmpRect2 = new Rect(); + private static final Rect sTmpRect = new Rect(); + private static final Rect sTmpRect2 = new Rect(); - /** - * The type of insets to provide. - */ - public @InsetsState.InternalInsetsType int type; + private final IBinder mOwner; + private final int mIndex; + private final @InsetsType int mType; /** * The source of frame. By default, all adjustment will be based on the window frame, it * can be set to window bounds or display bounds instead. */ - public int source = SOURCE_FRAME; + private int mSource = SOURCE_FRAME; /** * The provided insets size based on the source frame. The result will be used as the insets @@ -85,13 +88,13 @@ public class InsetsFrameProvider implements Parcelable { * (0, 0, 0, 50) instead, the insets frame will be a frame starting from the bottom side of the * source frame with height of 50, i.e., (0, 150) - (100, 200). */ - public Insets insetsSize = null; + private Insets mInsetsSize = null; /** * If null, the size set in insetsSize will be applied to all window types. If it contains * element of some types, the insets reported to the window with that types will be overridden. */ - public InsetsSizeOverride[] insetsSizeOverrides = null; + private InsetsSizeOverride[] mInsetsSizeOverrides = null; /** * This field, if set, is indicating the insets needs to be at least the given size inside the @@ -103,22 +106,80 @@ public class InsetsFrameProvider implements Parcelable { * * Be cautious, this will not be in effect for the window types whose insets size is overridden. */ - public Insets minimalInsetsSizeInDisplayCutoutSafe = null; + private Insets mMinimalInsetsSizeInDisplayCutoutSafe = null; + + /** + * Creates an InsetsFrameProvider which describes what frame an insets source should have. + * + * @param owner the owner of this provider. We might have multiple sources with the same type on + * a display, this is used to identify them. + * @param index the index of this provider. An owner might provide multiple sources with the + * same type, this is used to identify them. + * The value must be in a range of [0, 2047]. + * @param type the {@link InsetsType}. + * @see InsetsSource#createId(Object, int, int) + */ + public InsetsFrameProvider(IBinder owner, @IntRange(from = 0, to = 2047) int index, + @InsetsType int type) { + if (index < 0 || index >= 2048) { + throw new IllegalArgumentException(); + } + + // This throws IllegalArgumentException if the type is not valid. + WindowInsets.Type.indexOf(type); + + mOwner = owner; + mIndex = index; + mType = type; + } + + public IBinder getOwner() { + return mOwner; + } + + public int getIndex() { + return mIndex; + } + + public int getType() { + return mType; + } + + public InsetsFrameProvider setSource(int source) { + mSource = source; + return this; + } + + public int getSource() { + return mSource; + } + + public InsetsFrameProvider setInsetsSize(Insets insetsSize) { + mInsetsSize = insetsSize; + return this; + } + + public Insets getInsetsSize() { + return mInsetsSize; + } - public InsetsFrameProvider(int type) { - this(type, SOURCE_FRAME, null, null); + public InsetsFrameProvider setInsetsSizeOverrides(InsetsSizeOverride[] insetsSizeOverrides) { + mInsetsSizeOverrides = insetsSizeOverrides; + return this; } - public InsetsFrameProvider(int type, Insets insetsSize) { - this(type, SOURCE_FRAME, insetsSize, null); + public InsetsSizeOverride[] getInsetsSizeOverrides() { + return mInsetsSizeOverrides; } - public InsetsFrameProvider(int type, int source, Insets insetsSize, - InsetsSizeOverride[] insetsSizeOverride) { - this.type = type; - this.source = source; - this.insetsSize = insetsSize; - this.insetsSizeOverrides = insetsSizeOverride; + public InsetsFrameProvider setMinimalInsetsSizeInDisplayCutoutSafe( + Insets minimalInsetsSizeInDisplayCutoutSafe) { + mMinimalInsetsSizeInDisplayCutoutSafe = minimalInsetsSizeInDisplayCutoutSafe; + return this; + } + + public Insets getMinimalInsetsSizeInDisplayCutoutSafe() { + return mMinimalInsetsSizeInDisplayCutoutSafe; } @Override @@ -128,63 +189,73 @@ public class InsetsFrameProvider implements Parcelable { @Override public String toString() { - StringBuilder sb = new StringBuilder(32); - sb.append("InsetsFrameProvider: {"); - sb.append("type=").append(InsetsState.typeToString(type)); - sb.append(", source="); + final StringBuilder sb = new StringBuilder("InsetsFrameProvider: {"); + sb.append("owner=").append(mOwner); + sb.append(", index=").append(mIndex); + sb.append(", type=").append(WindowInsets.Type.toString(mType)); + sb.append(", source=").append(sourceToString(mSource)); + if (mInsetsSize != null) { + sb.append(", insetsSize=").append(mInsetsSize); + } + if (mInsetsSizeOverrides != null) { + sb.append(", insetsSizeOverrides=").append(Arrays.toString(mInsetsSizeOverrides)); + } + sb.append("}"); + return sb.toString(); + } + + private static String sourceToString(int source) { switch (source) { case SOURCE_DISPLAY: - sb.append("SOURCE_DISPLAY"); - break; + return "DISPLAY"; case SOURCE_CONTAINER_BOUNDS: - sb.append("SOURCE_CONTAINER_BOUNDS Bounds"); - break; + return "CONTAINER_BOUNDS"; case SOURCE_FRAME: - sb.append("SOURCE_FRAME"); - break; + return "FRAME"; } - if (insetsSize != null) { - sb.append(", insetsSize=").append(insetsSize); - } - if (insetsSizeOverrides != null) { - sb.append(", insetsSizeOverrides=").append(Arrays.toString(insetsSizeOverrides)); - } - sb.append("}"); - return sb.toString(); + return "UNDEFINED"; } public InsetsFrameProvider(Parcel in) { + mOwner = in.readStrongBinder(); + mIndex = in.readInt(); + mType = in.readInt(); int insetsSizeModified = in.readInt(); - type = in.readInt(); - source = in.readInt(); + mSource = in.readInt(); if ((insetsSizeModified & HAS_INSETS_SIZE) != 0) { - insetsSize = Insets.CREATOR.createFromParcel(in); + mInsetsSize = Insets.CREATOR.createFromParcel(in); } if ((insetsSizeModified & HAS_INSETS_SIZE_OVERRIDE) != 0) { - insetsSizeOverrides = in.createTypedArray(InsetsSizeOverride.CREATOR); + mInsetsSizeOverrides = in.createTypedArray(InsetsSizeOverride.CREATOR); } } @Override public void writeToParcel(Parcel out, int flags) { + out.writeStrongBinder(mOwner); + out.writeInt(mIndex); + out.writeInt(mType); int insetsSizeModified = 0; - if (insetsSize != null) { + if (mInsetsSize != null) { insetsSizeModified |= HAS_INSETS_SIZE; } - if (insetsSizeOverrides != null) { + if (mInsetsSizeOverrides != null) { insetsSizeModified |= HAS_INSETS_SIZE_OVERRIDE; } out.writeInt(insetsSizeModified); - out.writeInt(type); - out.writeInt(source); - if (insetsSize != null) { - insetsSize.writeToParcel(out, flags); + out.writeInt(mSource); + if (mInsetsSize != null) { + mInsetsSize.writeToParcel(out, flags); } - if (insetsSizeOverrides != null) { - out.writeTypedArray(insetsSizeOverrides, flags); + if (mInsetsSizeOverrides != null) { + out.writeTypedArray(mInsetsSizeOverrides, flags); } } + public boolean idEquals(InsetsFrameProvider o) { + return Objects.equals(mOwner, o.mOwner) && mIndex == o.mIndex && mType == o.mType; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -193,19 +264,21 @@ public class InsetsFrameProvider implements Parcelable { if (o == null || getClass() != o.getClass()) { return false; } - InsetsFrameProvider other = (InsetsFrameProvider) o; - return type == other.type && source == other.source - && Objects.equals(insetsSize, other.insetsSize) - && Arrays.equals(insetsSizeOverrides, other.insetsSizeOverrides); + final InsetsFrameProvider other = (InsetsFrameProvider) o; + return Objects.equals(mOwner, other.mOwner) && mIndex == other.mIndex + && mType == other.mType && mSource == other.mSource + && Objects.equals(mInsetsSize, other.mInsetsSize) + && Arrays.equals(mInsetsSizeOverrides, other.mInsetsSizeOverrides); } @Override public int hashCode() { - return Objects.hash(type, source, insetsSize, Arrays.hashCode(insetsSizeOverrides)); + return Objects.hash(mOwner, mIndex, mType, mSource, mInsetsSize, + Arrays.hashCode(mInsetsSizeOverrides)); } - public static final @android.annotation.NonNull Parcelable.Creator<InsetsFrameProvider> - CREATOR = new Parcelable.Creator<InsetsFrameProvider>() { + public static final @NonNull Parcelable.Creator<InsetsFrameProvider> CREATOR = + new Parcelable.Creator<>() { @Override public InsetsFrameProvider createFromParcel(Parcel in) { return new InsetsFrameProvider(in); @@ -282,21 +355,28 @@ public class InsetsFrameProvider implements Parcelable { * directly for that window type. */ public static class InsetsSizeOverride implements Parcelable { - public final int windowType; - public Insets insetsSize; + + private final int mWindowType; + private final Insets mInsetsSize; protected InsetsSizeOverride(Parcel in) { - windowType = in.readInt(); - insetsSize = in.readParcelable(null, Insets.class); + mWindowType = in.readInt(); + mInsetsSize = in.readParcelable(null, Insets.class); + } + + public InsetsSizeOverride(int windowType, Insets insetsSize) { + mWindowType = windowType; + mInsetsSize = insetsSize; + } + public int getWindowType() { + return mWindowType; } - public InsetsSizeOverride(int type, Insets size) { - windowType = type; - insetsSize = size; + public Insets getInsetsSize() { + return mInsetsSize; } - public static final Creator<InsetsSizeOverride> CREATOR = - new Creator<InsetsSizeOverride>() { + public static final Creator<InsetsSizeOverride> CREATOR = new Creator<>() { @Override public InsetsSizeOverride createFromParcel(Parcel in) { return new InsetsSizeOverride(in); @@ -315,8 +395,8 @@ public class InsetsFrameProvider implements Parcelable { @Override public void writeToParcel(Parcel out, int flags) { - out.writeInt(windowType); - out.writeParcelable(insetsSize, flags); + out.writeInt(mWindowType); + out.writeParcelable(mInsetsSize, flags); } @Override @@ -324,15 +404,15 @@ public class InsetsFrameProvider implements Parcelable { StringBuilder sb = new StringBuilder(32); sb.append("TypedInsetsSize: {"); sb.append("windowType=").append(ViewDebug.intToString( - WindowManager.LayoutParams.class, "type", windowType)); - sb.append(", insetsSize=").append(insetsSize); + WindowManager.LayoutParams.class, "type", mWindowType)); + sb.append(", insetsSize=").append(mInsetsSize); sb.append("}"); return sb.toString(); } @Override public int hashCode() { - return Objects.hash(windowType, insetsSize); + return Objects.hash(mWindowType, mInsetsSize); } } } diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java index 997e4a553063..761d504e87e9 100644 --- a/core/java/android/view/InsetsState.java +++ b/core/java/android/view/InsetsState.java @@ -42,7 +42,6 @@ import android.graphics.Insets; import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; -import android.util.ArraySet; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; @@ -707,40 +706,6 @@ public class InsetsState implements Parcelable { && !WindowConfiguration.inMultiWindowMode(windowingMode); } - public static @InternalInsetsType ArraySet<Integer> toInternalType(@InsetsType int types) { - final ArraySet<Integer> result = new ArraySet<>(); - if ((types & Type.STATUS_BARS) != 0) { - result.add(ITYPE_STATUS_BAR); - result.add(ITYPE_CLIMATE_BAR); - } - if ((types & Type.NAVIGATION_BARS) != 0) { - result.add(ITYPE_NAVIGATION_BAR); - result.add(ITYPE_EXTRA_NAVIGATION_BAR); - } - if ((types & Type.SYSTEM_OVERLAYS) != 0) { - result.add(ITYPE_LEFT_GENERIC_OVERLAY); - result.add(ITYPE_TOP_GENERIC_OVERLAY); - result.add(ITYPE_RIGHT_GENERIC_OVERLAY); - result.add(ITYPE_BOTTOM_GENERIC_OVERLAY); - } - if ((types & Type.CAPTION_BAR) != 0) { - result.add(ITYPE_CAPTION_BAR); - } - if ((types & Type.SYSTEM_GESTURES) != 0) { - result.add(ITYPE_LEFT_GESTURES); - result.add(ITYPE_TOP_GESTURES); - result.add(ITYPE_RIGHT_GESTURES); - result.add(ITYPE_BOTTOM_GESTURES); - } - if ((types & Type.MANDATORY_SYSTEM_GESTURES) != 0) { - result.add(ITYPE_LEFT_MANDATORY_GESTURES); - result.add(ITYPE_TOP_MANDATORY_GESTURES); - result.add(ITYPE_RIGHT_MANDATORY_GESTURES); - result.add(ITYPE_BOTTOM_MANDATORY_GESTURES); - } - return result; - } - /** * Converting a internal type to the public type. * @param type internal insets type, {@code InternalInsetsType}. 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/ITaskOrganizer.aidl b/core/java/android/window/ITaskOrganizer.aidl index fd86769293a6..bf9d3c2eefaf 100644 --- a/core/java/android/window/ITaskOrganizer.aidl +++ b/core/java/android/window/ITaskOrganizer.aidl @@ -34,9 +34,8 @@ oneway interface ITaskOrganizer { * has create a starting window for the Task. * * @param info The information about the Task that's available - * @param appToken Token of the application being started. */ - void addStartingWindow(in StartingWindowInfo info, IBinder appToken); + void addStartingWindow(in StartingWindowInfo info); /** * Called when the Task want to remove the starting window. diff --git a/core/java/android/window/IWindowlessStartingSurfaceCallback.aidl b/core/java/android/window/IWindowlessStartingSurfaceCallback.aidl new file mode 100644 index 000000000000..a0813565b256 --- /dev/null +++ b/core/java/android/window/IWindowlessStartingSurfaceCallback.aidl @@ -0,0 +1,29 @@ +/* + * 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.view.SurfaceControl; + +/** + * Interface to be invoked when a windowless starting surface added. + * + * @param addedSurface The starting surface. + * {@hide} + */ +interface IWindowlessStartingSurfaceCallback { + void onSurfaceAdded(in SurfaceControl addedSurface); +} diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java index 1a58fd556609..071c20f25e5c 100644 --- a/core/java/android/window/SnapshotDrawerUtils.java +++ b/core/java/android/window/SnapshotDrawerUtils.java @@ -329,6 +329,21 @@ public class SnapshotDrawerUtils { } /** + * Get or create a TaskDescription from a RunningTaskInfo. + */ + public static ActivityManager.TaskDescription getOrCreateTaskDescription( + ActivityManager.RunningTaskInfo runningTaskInfo) { + final ActivityManager.TaskDescription taskDescription; + if (runningTaskInfo.taskDescription != null) { + taskDescription = runningTaskInfo.taskDescription; + } else { + taskDescription = new ActivityManager.TaskDescription(); + taskDescription.setBackgroundColor(WHITE); + } + return taskDescription; + } + + /** * Help method to draw the snapshot on a surface. */ public static void drawSnapshotOnSurface(StartingWindowInfo info, WindowManager.LayoutParams lp, @@ -344,13 +359,8 @@ public class SnapshotDrawerUtils { final WindowManager.LayoutParams attrs = info.topOpaqueWindowLayoutParams; final ActivityManager.RunningTaskInfo runningTaskInfo = info.taskInfo; - final ActivityManager.TaskDescription taskDescription; - if (runningTaskInfo.taskDescription != null) { - taskDescription = runningTaskInfo.taskDescription; - } else { - taskDescription = new ActivityManager.TaskDescription(); - taskDescription.setBackgroundColor(WHITE); - } + final ActivityManager.TaskDescription taskDescription = + getOrCreateTaskDescription(runningTaskInfo); drawSurface.initiateSystemBarPainter(lp.flags, lp.privateFlags, attrs.insetsFlags.appearance, taskDescription, info.requestedVisibleTypes); final Rect systemBarInsets = getSystemBarInsets(windowBounds, topWindowInsetsState); diff --git a/core/java/android/window/StartingWindowInfo.java b/core/java/android/window/StartingWindowInfo.java index 1b64e613ed66..451acbe84a60 100644 --- a/core/java/android/window/StartingWindowInfo.java +++ b/core/java/android/window/StartingWindowInfo.java @@ -22,9 +22,12 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.TaskInfo; import android.content.pm.ActivityInfo; +import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; import android.view.InsetsState; +import android.view.SurfaceControl; import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowManager; @@ -59,6 +62,8 @@ public final class StartingWindowInfo implements Parcelable { /** @hide **/ public static final int STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN = 4; + public static final int STARTING_WINDOW_TYPE_WINDOWLESS = 5; + /** * @hide */ @@ -67,7 +72,8 @@ public final class StartingWindowInfo implements Parcelable { STARTING_WINDOW_TYPE_SPLASH_SCREEN, STARTING_WINDOW_TYPE_SNAPSHOT, STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN, - STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN + STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN, + STARTING_WINDOW_TYPE_WINDOWLESS }) public @interface StartingWindowType {} @@ -118,6 +124,7 @@ public final class StartingWindowInfo implements Parcelable { TYPE_PARAMETER_ACTIVITY_CREATED, TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN, TYPE_PARAMETER_ALLOW_HANDLE_SOLID_COLOR_SCREEN, + TYPE_PARAMETER_WINDOWLESS, TYPE_PARAMETER_LEGACY_SPLASH_SCREEN }) public @interface StartingTypeParams {} @@ -151,6 +158,12 @@ public final class StartingWindowInfo implements Parcelable { * @hide */ public static final int TYPE_PARAMETER_ALLOW_HANDLE_SOLID_COLOR_SCREEN = 0x00000080; + + /** + * Windowless surface + */ + public static final int TYPE_PARAMETER_WINDOWLESS = 0x00000100; + /** * Application is allowed to use the legacy splash screen * @hide @@ -182,7 +195,33 @@ public final class StartingWindowInfo implements Parcelable { */ public TaskSnapshot taskSnapshot; - public @InsetsType int requestedVisibleTypes = WindowInsets.Type.defaultVisible(); + @InsetsType public int requestedVisibleTypes = WindowInsets.Type.defaultVisible(); + + /** + * App token where the starting window should add to. + */ + public IBinder appToken; + + public IWindowlessStartingSurfaceCallback windowlessStartingSurfaceCallback; + + /** + * The root surface where windowless surface should attach on. + */ + public SurfaceControl rootSurface; + + /** + * Notify windowless surface is created. + * @param addedSurface Created surface. + */ + public void notifyAddComplete(SurfaceControl addedSurface) { + if (windowlessStartingSurfaceCallback != null) { + try { + windowlessStartingSurfaceCallback.onSurfaceAdded(addedSurface); + } catch (RemoteException e) { + // + } + } + } public StartingWindowInfo() { @@ -216,6 +255,9 @@ public final class StartingWindowInfo implements Parcelable { dest.writeBoolean(isKeyguardOccluded); dest.writeTypedObject(taskSnapshot, flags); dest.writeInt(requestedVisibleTypes); + dest.writeStrongBinder(appToken); + dest.writeStrongInterface(windowlessStartingSurfaceCallback); + dest.writeTypedObject(rootSurface, flags); } void readFromParcel(@NonNull Parcel source) { @@ -230,6 +272,10 @@ public final class StartingWindowInfo implements Parcelable { isKeyguardOccluded = source.readBoolean(); taskSnapshot = source.readTypedObject(TaskSnapshot.CREATOR); requestedVisibleTypes = source.readInt(); + appToken = source.readStrongBinder(); + windowlessStartingSurfaceCallback = IWindowlessStartingSurfaceCallback.Stub + .asInterface(source.readStrongBinder()); + rootSurface = source.readTypedObject(SurfaceControl.CREATOR); } @Override diff --git a/core/java/android/window/StartingWindowRemovalInfo.java b/core/java/android/window/StartingWindowRemovalInfo.java index 384dacfe89ed..518123600b9a 100644 --- a/core/java/android/window/StartingWindowRemovalInfo.java +++ b/core/java/android/window/StartingWindowRemovalInfo.java @@ -67,6 +67,16 @@ public final class StartingWindowRemovalInfo implements Parcelable { */ public float roundedCornerRadius; + /** + * Remove windowless surface. + */ + public boolean windowlessSurface; + + /** + * Remove immediately. + */ + public boolean removeImmediately; + public StartingWindowRemovalInfo() { } @@ -87,6 +97,8 @@ public final class StartingWindowRemovalInfo implements Parcelable { playRevealAnimation = source.readBoolean(); deferRemoveForIme = source.readBoolean(); roundedCornerRadius = source.readFloat(); + windowlessSurface = source.readBoolean(); + removeImmediately = source.readBoolean(); } @Override @@ -97,6 +109,8 @@ public final class StartingWindowRemovalInfo implements Parcelable { dest.writeBoolean(playRevealAnimation); dest.writeBoolean(deferRemoveForIme); dest.writeFloat(roundedCornerRadius); + dest.writeBoolean(windowlessSurface); + dest.writeBoolean(removeImmediately); } @Override @@ -105,7 +119,9 @@ public final class StartingWindowRemovalInfo implements Parcelable { + " frame=" + mainFrame + " playRevealAnimation=" + playRevealAnimation + " roundedCornerRadius=" + roundedCornerRadius - + " deferRemoveForIme=" + deferRemoveForIme + "}"; + + " deferRemoveForIme=" + deferRemoveForIme + + " windowlessSurface=" + windowlessSurface + + " removeImmediately=" + removeImmediately + "}"; } public static final @android.annotation.NonNull Creator<StartingWindowRemovalInfo> CREATOR = diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java index 02878f8ae72b..d4728c1187d7 100644 --- a/core/java/android/window/TaskOrganizer.java +++ b/core/java/android/window/TaskOrganizer.java @@ -92,13 +92,10 @@ public class TaskOrganizer extends WindowOrganizer { * has create a starting window for the Task. * * @param info The information about the Task that's available - * @param appToken Token of the application being started. - * context to for resources * @hide */ @BinderThread - public void addStartingWindow(@NonNull StartingWindowInfo info, - @NonNull IBinder appToken) {} + public void addStartingWindow(@NonNull StartingWindowInfo info) {} /** * Called when the Task want to remove the starting window. @@ -297,9 +294,8 @@ public class TaskOrganizer extends WindowOrganizer { private final ITaskOrganizer mInterface = new ITaskOrganizer.Stub() { @Override - public void addStartingWindow(StartingWindowInfo windowInfo, - IBinder appToken) { - mExecutor.execute(() -> TaskOrganizer.this.addStartingWindow(windowInfo, appToken)); + public void addStartingWindow(StartingWindowInfo windowInfo) { + mExecutor.execute(() -> TaskOrganizer.this.addStartingWindow(windowInfo)); } @Override 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/config_telephony.xml b/core/res/res/values/config_telephony.xml index ebda172061d3..f58cf8f46583 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -114,6 +114,11 @@ <string name="config_pointing_ui_package" translatable="false"></string> <java-symbol type="string" name="config_pointing_ui_package" /> + <!-- Telephony resends received satellite datagram to listener + if ack is not received within this timeout --> + <integer name="config_timeout_to_receive_delivered_ack_millis">300000</integer> + <java-symbol type="integer" name="config_timeout_to_receive_delivered_ack_millis" /> + <!-- Whether enhanced IWLAN handover check is enabled. If enabled, telephony frameworks will not perform handover if the target transport is out of service, or VoPS not supported. The network will be torn down on the source transport, and will be 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/pm/TEST_MAPPING b/core/tests/coretests/src/android/content/pm/TEST_MAPPING index 15e04d11acf7..978d80cb52f6 100644 --- a/core/tests/coretests/src/android/content/pm/TEST_MAPPING +++ b/core/tests/coretests/src/android/content/pm/TEST_MAPPING @@ -1,5 +1,5 @@ { - "presubmit-large": [ + "presubmit": [ { "name": "FrameworksCoreTests", "options": [ 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/android/provider/NameValueCacheTest.java b/core/tests/coretests/src/android/provider/NameValueCacheTest.java index b6fc137471a4..ccf8085f87ff 100644 --- a/core/tests/coretests/src/android/provider/NameValueCacheTest.java +++ b/core/tests/coretests/src/android/provider/NameValueCacheTest.java @@ -91,7 +91,7 @@ public class NameValueCacheTest { mConfigsCacheGenerationStore = new MemoryIntArray(2); mConfigsCacheGenerationStore.set(0, 123); mConfigsCacheGenerationStore.set(1, 456); - mSettingsCacheGenerationStore = new MemoryIntArray(2); + mSettingsCacheGenerationStore = new MemoryIntArray(3); mSettingsCacheGenerationStore.set(0, 234); mSettingsCacheGenerationStore.set(1, 567); mConfigsStorage = new HashMap<>(); @@ -163,6 +163,10 @@ public class NameValueCacheTest { Bundle incomingBundle = invocationOnMock.getArgument(4); String key = invocationOnMock.getArgument(3); String value = incomingBundle.getString(Settings.NameValueTable.VALUE); + boolean newSetting = false; + if (!mSettingsStorage.containsKey(key)) { + newSetting = true; + } mSettingsStorage.put(key, value); int currentGeneration; // Different settings have different generation codes @@ -173,12 +177,18 @@ public class NameValueCacheTest { currentGeneration = mSettingsCacheGenerationStore.get(1); mSettingsCacheGenerationStore.set(1, ++currentGeneration); } + if (newSetting) { + // Tracking the generation of all unset settings. + // Increment when a new setting is inserted + currentGeneration = mSettingsCacheGenerationStore.get(2); + mSettingsCacheGenerationStore.set(2, ++currentGeneration); + } return null; }); // Returns the value corresponding to a setting, or null if the setting - // doesn't have a value stored for it. Returns the generation key if the value isn't null - // and the caller asked for the generation key. + // doesn't have a value stored for it. Returns the generation key + // if the caller asked for the generation key. when(mMockIContentProvider.call(any(), eq(Settings.Secure.CONTENT_URI.getAuthority()), eq(Settings.CALL_METHOD_GET_SECURE), any(), any(Bundle.class))).thenAnswer( invocationOnMock -> { @@ -189,9 +199,15 @@ public class NameValueCacheTest { Bundle bundle = new Bundle(); bundle.putSerializable(Settings.NameValueTable.VALUE, value); - if (value != null && incomingBundle.containsKey( + if (incomingBundle.containsKey( Settings.CALL_METHOD_TRACK_GENERATION_KEY)) { - int index = key.equals(SETTING) ? 0 : 1; + int index; + if (value != null) { + index = key.equals(SETTING) ? 0 : 1; + } else { + // special index for unset settings + index = 2; + } bundle.putParcelable(Settings.CALL_METHOD_TRACK_GENERATION_KEY, mSettingsCacheGenerationStore); bundle.putInt(Settings.CALL_METHOD_GENERATION_INDEX_KEY, index); @@ -361,16 +377,38 @@ public class NameValueCacheTest { } @Test - public void testCaching_nullSetting() throws Exception { + public void testCaching_unsetSetting() throws Exception { String returnedValue = Settings.Secure.getString(mMockContentResolver, SETTING); verify(mMockIContentProvider, times(1)).call(any(), any(), eq(Settings.CALL_METHOD_GET_SECURE), any(), any(Bundle.class)); assertThat(returnedValue).isNull(); String cachedValue = Settings.Secure.getString(mMockContentResolver, SETTING); - // Empty list won't be cached + // The first unset setting's generation number is cached + verifyNoMoreInteractions(mMockIContentProvider); + assertThat(cachedValue).isNull(); + + String returnedValue2 = Settings.Secure.getString(mMockContentResolver, SETTING2); verify(mMockIContentProvider, times(2)).call(any(), any(), eq(Settings.CALL_METHOD_GET_SECURE), any(), any(Bundle.class)); - assertThat(cachedValue).isNull(); + assertThat(returnedValue2).isNull(); + + String cachedValue2 = Settings.Secure.getString(mMockContentResolver, SETTING); + // The second unset setting's generation number is cached + verifyNoMoreInteractions(mMockIContentProvider); + assertThat(cachedValue2).isNull(); + + Settings.Secure.putString(mMockContentResolver, SETTING, "a"); + // The generation for unset settings should have changed + returnedValue2 = Settings.Secure.getString(mMockContentResolver, SETTING2); + verify(mMockIContentProvider, times(3)).call(any(), any(), + eq(Settings.CALL_METHOD_GET_SECURE), any(), any(Bundle.class)); + assertThat(returnedValue2).isNull(); + + // The generation tracker for the first setting should have change because it's set now + returnedValue = Settings.Secure.getString(mMockContentResolver, SETTING); + verify(mMockIContentProvider, times(4)).call(any(), any(), + eq(Settings.CALL_METHOD_GET_SECURE), any(), any(Bundle.class)); + assertThat(returnedValue).isEqualTo("a"); } } diff --git a/core/tests/coretests/src/android/view/OWNERS b/core/tests/coretests/src/android/view/OWNERS index a142e27a1ab0..2ca99943a8a0 100644 --- a/core/tests/coretests/src/android/view/OWNERS +++ b/core/tests/coretests/src/android/view/OWNERS @@ -1,4 +1,5 @@ # Accessibility +per-file AccessibilityInteractionControllerTest.java = file:/services/accessibility/OWNERS per-file WindowInfoTest.java = file:/services/accessibility/OWNERS # Input 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/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 5cb5ffa03e00..4a98c4de3735 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -539,6 +539,12 @@ applications that come with the platform <permission name="android.permission.BIND_WALLPAPER"/> </privapp-permissions> + <privapp-permissions package="com.android.wallpaper"> + <permission name="android.permission.SET_WALLPAPER_COMPONENT"/> + <permission name="android.permission.BIND_WALLPAPER"/> + <permission name="android.permission.CUSTOMIZE_SYSTEM_UI"/> + </privapp-permissions> + <privapp-permissions package="com.android.dynsystem"> <permission name="android.permission.REBOOT"/> <permission name="android.permission.MANAGE_DYNAMIC_SYSTEM"/> 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/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java index 4c669b8c34f5..24fea014287d 100644 --- a/graphics/java/android/graphics/ColorSpace.java +++ b/graphics/java/android/graphics/ColorSpace.java @@ -170,7 +170,7 @@ public abstract class ColorSpace { /** * Standard CIE 1931 2° illuminant D65, encoded in xyY. * This illuminant has a color temperature of 6504K. This illuminant - * is commonly used in RGB color spaces such as sRGB, BT.209, etc. + * is commonly used in RGB color spaces such as sRGB, BT.709, etc. */ public static final float[] ILLUMINANT_D65 = { 0.31271f, 0.32902f }; /** @@ -862,8 +862,8 @@ public abstract class ColorSpace { public enum Model { /** * The RGB model is a color model with 3 components that - * refer to the three additive primiaries: red, green - * andd blue. + * refer to the three additive primaries: red, green + * and blue. */ RGB(3), /** @@ -2537,7 +2537,7 @@ public abstract class ColorSpace { * does not need to be specified and is assumed to be 1.0. Only the xy components * are required.</p> * - * <p class="note">The ID, areturned by {@link #getId()}, of an object created by + * <p class="note">The ID, as returned by {@link #getId()}, of an object created by * this constructor is always {@link #MIN_ID}.</p> * * @param name Name of the color space, cannot be null, its length must be >= 1 @@ -3961,7 +3961,7 @@ public abstract class ColorSpace { * * <p>We can only connect color spaces if they use the same profile * connection space. We assume the connection space is always - * CIE XYZ but we maye need to perform a chromatic adaptation to + * CIE XYZ but we maybe need to perform a chromatic adaptation to * match the white points. If an adaptation is needed, we use the * CIE standard illuminant D50. The unmatched color space is adapted * using the von Kries transform and the {@link Adaptation#BRADFORD} 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/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index caefb2e9516c..404429ad41d3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -1000,7 +1000,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; final TaskFragmentContainer container = getContainerWithActivity(activity); - if (container != null) { + // Looking for the activity below from the information we already have if the container + // only embeds activities of the same process because activities of other processes are not + // available in this embedding host process for security concern. + if (container != null && !container.hasCrossProcessActivities()) { final List<Activity> containerActivities = container.collectNonFinishingActivities(); final int index = containerActivities.indexOf(activity); if (index > 0) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 6c553a836dbd..60be9d16d749 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -153,6 +153,11 @@ class TaskFragmentContainer { Runnable mAppearEmptyTimeout; /** + * Whether this TaskFragment contains activities of another process/package. + */ + private boolean mHasCrossProcessActivities; + + /** * Creates a container with an existing activity that will be re-parented to it in a window * container transaction. * @param pairedPrimaryContainer when it is set, the new container will be add right above it @@ -418,10 +423,18 @@ class TaskFragmentContainer { mAppearEmptyTimeout = null; } + mHasCrossProcessActivities = false; mInfo = info; if (mInfo == null || mInfo.isEmpty()) { return; } + + // Contains activities of another process if the activities size is not matched to the + // running activity count + if (mInfo.getRunningActivityCount() != mInfo.getActivities().size()) { + mHasCrossProcessActivities = true; + } + // Only track the pending Intent when the container is empty. mPendingAppearedIntent = null; if (mPendingAppearedActivities.isEmpty()) { @@ -751,6 +764,11 @@ class TaskFragmentContainer { return mPendingAppearedInRequestedTaskFragmentActivities.contains(activityToken); } + /** Whether contains activities of another process */ + boolean hasCrossProcessActivities() { + return mHasCrossProcessActivities; + } + /** Gets the parent leaf Task id. */ int getTaskId() { return mTaskContainer.getTaskId(); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 3d0e1c8cf788..cbdc262c0594 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -1014,6 +1014,25 @@ public class SplitControllerTest { } @Test + public void testFindActivityBelow() { + // Create a container with two activities + final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity); + final Activity pendingAppearedActivity = createMockActivity(); + container.addPendingAppearedActivity(pendingAppearedActivity); + + // Ensure the activity below matches + assertEquals(mActivity, + mSplitController.findActivityBelow(pendingAppearedActivity)); + + // Ensure that the activity look up won't search for the in-process activities and should + // IPC to WM core to get the activity below. It should be `null` for this mock test. + spyOn(container); + doReturn(true).when(container).hasCrossProcessActivities(); + assertNotEquals(mActivity, + mSplitController.findActivityBelow(pendingAppearedActivity)); + } + + @Test public void testResolveActivityToContainer_inUnknownTaskFragment() { doReturn(new Binder()).when(mSplitController) .getTaskFragmentTokenFromActivityClientRecord(mActivity); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 585f81c81a36..b6fd0bbafc71 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -428,9 +428,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } @Override - public void addStartingWindow(StartingWindowInfo info, IBinder appToken) { + public void addStartingWindow(StartingWindowInfo info) { if (mStartingWindow != null) { - mStartingWindow.addStartingWindow(info, appToken); + mStartingWindow.addStartingWindow(info); } } 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/common/DevicePostureController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java index 22587f4c6456..8b4ac1a8dc79 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java @@ -39,6 +39,9 @@ import java.util.List; * * Note that most of the implementation here inherits from * {@link com.android.systemui.statusbar.policy.DevicePostureController}. + * + * Use the {@link TabletopModeController} if you are interested in tabletop mode change only, + * which is more common. */ public class DevicePostureController { @IntDef(prefix = {"DEVICE_POSTURE_"}, value = { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java new file mode 100644 index 000000000000..bf226283ae54 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java @@ -0,0 +1,208 @@ +/* + * 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.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_UNKNOWN; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FOLDABLE; + +import android.annotation.NonNull; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.Configuration; +import android.util.ArraySet; +import android.view.Surface; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Wrapper class to track the tabletop (aka. flex) mode change on Fold-ables. + * See also <a + * href="https://developer.android.com/guide/topics/large-screens/learn-about-foldables + * #foldable_postures">Foldable states and postures</a> for reference. + * + * Use the {@link DevicePostureController} for more detailed posture changes. + */ +public class TabletopModeController implements + DevicePostureController.OnDevicePostureChangedListener, + DisplayController.OnDisplaysChangedListener { + private static final long TABLETOP_MODE_DELAY_MILLIS = 1_000; + + private final Context mContext; + + private final DevicePostureController mDevicePostureController; + + private final DisplayController mDisplayController; + + private final ShellExecutor mMainExecutor; + + private final Set<Integer> mTabletopModeRotations = new ArraySet<>(); + + private final List<OnTabletopModeChangedListener> mListeners = new ArrayList<>(); + + @VisibleForTesting + final Runnable mOnEnterTabletopModeCallback = () -> { + if (isInTabletopMode()) { + // We are still in tabletop mode, go ahead. + mayBroadcastOnTabletopModeChange(true /* isInTabletopMode */); + } + }; + + @DevicePostureController.DevicePostureInt + private int mDevicePosture = DEVICE_POSTURE_UNKNOWN; + + @Surface.Rotation + private int mDisplayRotation = WindowConfiguration.ROTATION_UNDEFINED; + + /** + * Track the last callback value for {@link OnTabletopModeChangedListener}. + * This is to avoid duplicated {@code false} callback to {@link #mListeners}. + */ + private Boolean mLastIsInTabletopModeForCallback; + + public TabletopModeController(Context context, + ShellInit shellInit, + DevicePostureController postureController, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mDevicePostureController = postureController; + mDisplayController = displayController; + mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); + } + + @VisibleForTesting + void onInit() { + mDevicePostureController.registerOnDevicePostureChangedListener(this); + mDisplayController.addDisplayWindowListener(this); + // Aligns with what's in {@link com.android.server.wm.DisplayRotation}. + final int[] deviceTabletopRotations = mContext.getResources().getIntArray( + com.android.internal.R.array.config_deviceTabletopRotations); + if (deviceTabletopRotations == null || deviceTabletopRotations.length == 0) { + ProtoLog.e(WM_SHELL_FOLDABLE, + "No valid config_deviceTabletopRotations, can not tell" + + " tabletop mode in WMShell"); + return; + } + for (int angle : deviceTabletopRotations) { + switch (angle) { + case 0: + mTabletopModeRotations.add(Surface.ROTATION_0); + break; + case 90: + mTabletopModeRotations.add(Surface.ROTATION_90); + break; + case 180: + mTabletopModeRotations.add(Surface.ROTATION_180); + break; + case 270: + mTabletopModeRotations.add(Surface.ROTATION_270); + break; + default: + ProtoLog.e(WM_SHELL_FOLDABLE, + "Invalid surface rotation angle in " + + "config_deviceTabletopRotations: %d", + angle); + break; + } + } + } + + /** Register {@link OnTabletopModeChangedListener} to listen for tabletop mode change. */ + public void registerOnTabletopModeChangedListener( + @NonNull OnTabletopModeChangedListener listener) { + if (listener == null || mListeners.contains(listener)) return; + mListeners.add(listener); + listener.onTabletopModeChanged(isInTabletopMode()); + } + + /** Unregister {@link OnTabletopModeChangedListener} for tabletop mode change. */ + public void unregisterOnTabletopModeChangedListener( + @NonNull OnTabletopModeChangedListener listener) { + mListeners.remove(listener); + } + + @Override + public void onDevicePostureChanged(@DevicePostureController.DevicePostureInt int posture) { + if (mDevicePosture != posture) { + onDevicePostureOrDisplayRotationChanged(posture, mDisplayRotation); + } + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + final int newDisplayRotation = newConfig.windowConfiguration.getDisplayRotation(); + if (displayId == DEFAULT_DISPLAY && newDisplayRotation != mDisplayRotation) { + onDevicePostureOrDisplayRotationChanged(mDevicePosture, newDisplayRotation); + } + } + + private void onDevicePostureOrDisplayRotationChanged( + @DevicePostureController.DevicePostureInt int newPosture, + @Surface.Rotation int newDisplayRotation) { + final boolean wasInTabletopMode = isInTabletopMode(); + mDevicePosture = newPosture; + mDisplayRotation = newDisplayRotation; + final boolean couldBeInTabletopMode = isInTabletopMode(); + mMainExecutor.removeCallbacks(mOnEnterTabletopModeCallback); + if (!wasInTabletopMode && couldBeInTabletopMode) { + // May enter tabletop mode, but we need to wait for additional time since this + // could be an intermediate state. + mMainExecutor.executeDelayed(mOnEnterTabletopModeCallback, TABLETOP_MODE_DELAY_MILLIS); + } else { + // Cancel entering tabletop mode if any condition's changed. + mayBroadcastOnTabletopModeChange(false /* isInTabletopMode */); + } + } + + private boolean isHalfOpened(@DevicePostureController.DevicePostureInt int posture) { + return posture == DEVICE_POSTURE_HALF_OPENED; + } + + private boolean isInTabletopMode() { + return isHalfOpened(mDevicePosture) && mTabletopModeRotations.contains(mDisplayRotation); + } + + private void mayBroadcastOnTabletopModeChange(boolean isInTabletopMode) { + if (mLastIsInTabletopModeForCallback == null + || mLastIsInTabletopModeForCallback != isInTabletopMode) { + mListeners.forEach(l -> l.onTabletopModeChanged(isInTabletopMode)); + mLastIsInTabletopModeForCallback = isInTabletopMode; + } + } + + /** + * Listener interface for tabletop mode change. + */ + public interface OnTabletopModeChangedListener { + /** + * Callback when tabletop mode changes. Expect duplicated callbacks with {@code false}. + * @param isInTabletopMode {@code true} if enters tabletop mode, {@code false} otherwise. + */ + void onTabletopModeChanged(boolean isInTabletopMode); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 838022536e43..3d5230d5cf90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -52,6 +52,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellAnimationThread; @@ -172,6 +173,18 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static TabletopModeController provideTabletopModeController( + Context context, + ShellInit shellInit, + DevicePostureController postureController, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { + return new TabletopModeController( + context, shellInit, postureController, displayController, mainExecutor); + } + + @WMSingleton + @Provides static DragAndDropController provideDragAndDropController(Context context, ShellInit shellInit, ShellController shellController, 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/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 75f9a4c33af9..c9b3a1af6507 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -50,6 +50,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), + WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; 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..18a3849ea17d 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 { @@ -1704,7 +1801,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Split entering background. wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, true /* setReparentLeafTaskIfRelaunch */); - wct.setForceTranslucent(mRootTaskInfo.token, true); + if (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping) { + wct.setForceTranslucent(mRootTaskInfo.token, true); + } } else { wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false /* setReparentLeafTaskIfRelaunch */); @@ -1734,7 +1833,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 +1853,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 +1921,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/startingsurface/AbsSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/AbsSplashWindowCreator.java new file mode 100644 index 000000000000..1ddd8f9a3a14 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/AbsSplashWindowCreator.java @@ -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. + */ + +package com.android.wm.shell.startingsurface; + +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.hardware.display.DisplayManager; +import android.view.Display; + +import com.android.wm.shell.common.ShellExecutor; + +// abstract class to create splash screen window(or windowless window) +abstract class AbsSplashWindowCreator { + protected static final String TAG = StartingWindowController.TAG; + protected final SplashscreenContentDrawer mSplashscreenContentDrawer; + protected final Context mContext; + protected final DisplayManager mDisplayManager; + protected final ShellExecutor mSplashScreenExecutor; + protected final StartingSurfaceDrawer.StartingWindowRecordManager mStartingWindowRecordManager; + + private StartingSurface.SysuiProxy mSysuiProxy; + + AbsSplashWindowCreator(SplashscreenContentDrawer contentDrawer, Context context, + ShellExecutor splashScreenExecutor, DisplayManager displayManager, + StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager) { + mSplashscreenContentDrawer = contentDrawer; + mContext = context; + mSplashScreenExecutor = splashScreenExecutor; + mDisplayManager = displayManager; + mStartingWindowRecordManager = startingWindowRecordManager; + } + + int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) { + return splashScreenThemeResId != 0 + ? splashScreenThemeResId + : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource() + : com.android.internal.R.style.Theme_DeviceDefault_DayNight; + } + + protected Display getDisplay(int displayId) { + return mDisplayManager.getDisplay(displayId); + } + + void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) { + mSysuiProxy = sysuiProxy; + } + + protected void requestTopUi(boolean requestTopUi) { + if (mSysuiProxy != null) { + mSysuiProxy.requestTopUi(requestTopUi, TAG); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java new file mode 100644 index 000000000000..20c4d5ae5f58 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.startingsurface; + +import android.window.StartingWindowInfo; +import android.window.TaskSnapshot; + +import com.android.wm.shell.common.ShellExecutor; + +class SnapshotWindowCreator { + private final ShellExecutor mMainExecutor; + private final StartingSurfaceDrawer.StartingWindowRecordManager + mStartingWindowRecordManager; + + SnapshotWindowCreator(ShellExecutor mainExecutor, + StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager) { + mMainExecutor = mainExecutor; + mStartingWindowRecordManager = startingWindowRecordManager; + } + + void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, TaskSnapshot snapshot) { + final int taskId = startingWindowInfo.taskInfo.taskId; + // Remove any existing starting window for this task before adding. + mStartingWindowRecordManager.removeWindow(taskId, true); + final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo, + startingWindowInfo.appToken, snapshot, mMainExecutor, + () -> mStartingWindowRecordManager.removeWindow(taskId, true)); + if (surface != null) { + final SnapshotWindowRecord tView = new SnapshotWindowRecord(surface, + startingWindowInfo.taskInfo.topActivityType, mMainExecutor); + mStartingWindowRecordManager.addRecord(taskId, tView); + } + } + + private static class SnapshotWindowRecord extends StartingSurfaceDrawer.SnapshotRecord { + private final TaskSnapshotWindow mTaskSnapshotWindow; + + SnapshotWindowRecord(TaskSnapshotWindow taskSnapshotWindow, + int activityType, ShellExecutor removeExecutor) { + super(activityType, removeExecutor); + mTaskSnapshotWindow = taskSnapshotWindow; + mBGColor = mTaskSnapshotWindow.getBackgroundColor(); + } + + @Override + protected void removeImmediately() { + super.removeImmediately(); + mTaskSnapshotWindow.removeImmediately(); + } + + @Override + protected boolean hasImeSurface() { + return mTaskSnapshotWindow.hasImeSurface(); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index ebb957b2201b..dc91a11dc64f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -24,9 +24,6 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLA import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; -import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MAX_ANIMATION_DURATION; -import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MINIMAL_ANIMATION_DURATION; - import android.annotation.ColorInt; import android.annotation.IntDef; import android.annotation.NonNull; @@ -96,6 +93,25 @@ import java.util.function.UnaryOperator; public class SplashscreenContentDrawer { private static final String TAG = StartingWindowController.TAG; + /** + * The minimum duration during which the splash screen is shown when the splash screen icon is + * animated. + */ + static final long MINIMAL_ANIMATION_DURATION = 400L; + + /** + * Allow the icon style splash screen to be displayed for longer to give time for the animation + * to finish, i.e. the extra buffer time to keep the splash screen if the animation is slightly + * longer than the {@link #MINIMAL_ANIMATION_DURATION} duration. + */ + static final long TIME_WINDOW_DURATION = 100L; + + /** + * The maximum duration during which the splash screen will be shown if the application is ready + * to show before the icon animation finishes. + */ + static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION; + // The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an // icon which it's non-transparent foreground area is similar to it's background area, then // do not enlarge the foreground drawable. @@ -368,10 +384,10 @@ public class SplashscreenContentDrawer { private static int estimateWindowBGColor(Drawable themeBGDrawable) { final DrawableColorTester themeBGTester = new DrawableColorTester( - themeBGDrawable, DrawableColorTester.TRANSPARENT_FILTER /* filterType */); - if (themeBGTester.passFilterRatio() == 0) { - // the window background is transparent, unable to draw - Slog.w(TAG, "Window background is transparent, fill background with black color"); + themeBGDrawable, DrawableColorTester.TRANSLUCENT_FILTER /* filterType */); + if (themeBGTester.passFilterRatio() != 1) { + // the window background is translucent, unable to draw + Slog.w(TAG, "Window background is translucent, fill background with black color"); return getSystemBGColor(); } else { return themeBGTester.getDominateColor(); @@ -854,7 +870,7 @@ public class SplashscreenContentDrawer { @Override public float passFilterRatio() { final int alpha = mColorDrawable.getAlpha(); - return (float) (alpha / 255); + return alpha / 255.0f; } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java new file mode 100644 index 000000000000..8a4d4c21194a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java @@ -0,0 +1,508 @@ +/* + * 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.startingsurface; + +import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; +import static android.view.Choreographer.CALLBACK_INSETS_ANIMATION; +import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.ActivityThread; +import android.app.TaskInfo; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.hardware.display.DisplayManager; +import android.os.IBinder; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.Trace; +import android.os.UserHandle; +import android.util.Slog; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.Display; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowInsetsController; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; +import android.widget.FrameLayout; +import android.window.SplashScreenView; +import android.window.StartingWindowInfo; +import android.window.StartingWindowRemovalInfo; + +import com.android.internal.R; +import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.ContrastColorUtil; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.function.Supplier; + +/** + * A class which able to draw splash screen as the starting window for a task. + * + * In order to speed up, there will use two threads to creating a splash screen in parallel. + * Right now we are still using PhoneWindow to create splash screen window, so the view is added to + * the ViewRootImpl, and those view won't be draw immediately because the ViewRootImpl will call + * scheduleTraversal to register a callback from Choreographer, so the drawing result of the view + * can synchronize on each frame. + * + * The bad thing is that we cannot decide when would Choreographer#doFrame happen, and drawing + * the AdaptiveIconDrawable object can be time consuming, so we use the splash-screen background + * thread to draw the AdaptiveIconDrawable object to a Bitmap and cache it to a BitmapShader after + * the SplashScreenView just created, once we get the BitmapShader then the #draw call can be very + * quickly. + * + * So basically we are using the spare time to prepare the SplashScreenView while splash screen + * thread is waiting for + * 1. WindowManager#addView(binder call to WM), + * 2. Choreographer#doFrame happen(uncertain time for next frame, depends on device), + * 3. Session#relayout(another binder call to WM which under Choreographer#doFrame, but will + * always happen before #draw). + * Because above steps are running on splash-screen thread, so pre-draw the BitmapShader on + * splash-screen background tread can make they execute in parallel, which ensure it is faster then + * to draw the AdaptiveIconDrawable when receive callback from Choreographer#doFrame. + * + * Here is the sequence to compare the difference between using single and two thread. + * + * Single thread: + * => makeSplashScreenContentView -> WM#addView .. waiting for Choreographer#doFrame -> relayout + * -> draw -> AdaptiveIconDrawable#draw + * + * Two threads: + * => makeSplashScreenContentView -> cachePaint(=AdaptiveIconDrawable#draw) + * => WM#addView -> .. waiting for Choreographer#doFrame -> relayout -> draw -> (draw the Paint + * directly). + */ +class SplashscreenWindowCreator extends AbsSplashWindowCreator { + private static final int LIGHT_BARS_MASK = + WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS + | WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; + + private final WindowManagerGlobal mWindowManagerGlobal; + private Choreographer mChoreographer; + + /** + * Records of {@link SurfaceControlViewHost} where the splash screen icon animation is + * rendered and that have not yet been removed by their client. + */ + private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts = + new SparseArray<>(1); + + SplashscreenWindowCreator(SplashscreenContentDrawer contentDrawer, Context context, + ShellExecutor splashScreenExecutor, DisplayManager displayManager, + StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager) { + super(contentDrawer, context, splashScreenExecutor, displayManager, + startingWindowRecordManager); + mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance()); + mWindowManagerGlobal = WindowManagerGlobal.getInstance(); + } + + void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, + @StartingWindowInfo.StartingWindowType int suggestType) { + final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo; + final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null + ? windowInfo.targetActivityInfo + : taskInfo.topActivityInfo; + if (activityInfo == null || activityInfo.packageName == null) { + return; + } + // replace with the default theme if the application didn't set + final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo); + final Context context = SplashscreenContentDrawer.createContext(mContext, windowInfo, theme, + suggestType, mDisplayManager); + if (context == null) { + return; + } + final WindowManager.LayoutParams params = SplashscreenContentDrawer.createLayoutParameters( + context, windowInfo, suggestType, activityInfo.packageName, + suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN + ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT, windowInfo.appToken); + + final int displayId = taskInfo.displayId; + final int taskId = taskInfo.taskId; + final Display display = getDisplay(displayId); + + // TODO(b/173975965) tracking performance + // Prepare the splash screen content view on splash screen worker thread in parallel, so the + // content view won't be blocked by binder call like addWindow and relayout. + // 1. Trigger splash screen worker thread to create SplashScreenView before/while + // Session#addWindow. + // 2. Synchronize the SplashscreenView to splash screen thread before Choreographer start + // traversal, which will call Session#relayout on splash screen thread. + // 3. Pre-draw the BitmapShader if the icon is immobile on splash screen worker thread, at + // the same time the splash screen thread should be executing Session#relayout. Blocking the + // traversal -> draw on splash screen thread until the BitmapShader of the icon is ready. + + // Record whether create splash screen view success, notify to current thread after + // create splash screen view finished. + final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier(); + final FrameLayout rootLayout = new FrameLayout( + mSplashscreenContentDrawer.createViewContextWrapper(context)); + rootLayout.setPadding(0, 0, 0, 0); + rootLayout.setFitsSystemWindows(false); + final Runnable setViewSynchronized = () -> { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView"); + // waiting for setContentView before relayoutWindow + SplashScreenView contentView = viewSupplier.get(); + final StartingSurfaceDrawer.StartingWindowRecord sRecord = + mStartingWindowRecordManager.getRecord(taskId); + final SplashWindowRecord record = sRecord instanceof SplashWindowRecord + ? (SplashWindowRecord) sRecord : null; + // If record == null, either the starting window added fail or removed already. + // Do not add this view if the token is mismatch. + if (record != null && windowInfo.appToken == record.mAppToken) { + // if view == null then creation of content view was failed. + if (contentView != null) { + try { + rootLayout.addView(contentView); + } catch (RuntimeException e) { + Slog.w(TAG, "failed set content view to starting window " + + "at taskId: " + taskId, e); + contentView = null; + } + } + record.setSplashScreenView(contentView); + } + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + }; + requestTopUi(true); + mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo, + viewSupplier::setView, viewSupplier::setUiThreadInitTask); + try { + if (addWindow(taskId, windowInfo.appToken, rootLayout, display, params, suggestType)) { + // We use the splash screen worker thread to create SplashScreenView while adding + // the window, as otherwise Choreographer#doFrame might be delayed on this thread. + // And since Choreographer#doFrame won't happen immediately after adding the window, + // if the view is not added to the PhoneWindow on the first #doFrame, the view will + // not be rendered on the first frame. So here we need to synchronize the view on + // the window before first round relayoutWindow, which will happen after insets + // animation. + mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null); + final SplashWindowRecord record = + (SplashWindowRecord) mStartingWindowRecordManager.getRecord(taskId); + if (record != null) { + record.parseAppSystemBarColor(context); + // Block until we get the background color. + final SplashScreenView contentView = viewSupplier.get(); + if (suggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { + contentView.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + final int lightBarAppearance = + ContrastColorUtil.isColorLight( + contentView.getInitBackgroundColor()) + ? LIGHT_BARS_MASK : 0; + contentView.getWindowInsetsController() + .setSystemBarsAppearance( + lightBarAppearance, LIGHT_BARS_MASK); + } + + @Override + public void onViewDetachedFromWindow(View v) { + } + }); + } + } + } else { + // release the icon view host + final SplashScreenView contentView = viewSupplier.get(); + if (contentView.getSurfaceHost() != null) { + SplashScreenView.releaseIconHost(contentView.getSurfaceHost()); + } + } + } catch (RuntimeException e) { + // don't crash if something else bad happens, for example a + // failure loading resources because we are loading from an app + // on external storage that has been unmounted. + Slog.w(TAG, "failed creating starting window at taskId: " + taskId, e); + } + } + + int estimateTaskBackgroundColor(TaskInfo taskInfo) { + if (taskInfo.topActivityInfo == null) { + return Color.TRANSPARENT; + } + final ActivityInfo activityInfo = taskInfo.topActivityInfo; + final String packageName = activityInfo.packageName; + final int userId = taskInfo.userId; + final Context windowContext; + try { + windowContext = mContext.createPackageContextAsUser( + packageName, Context.CONTEXT_RESTRICTED, UserHandle.of(userId)); + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "Failed creating package context with package name " + + packageName + " for user " + taskInfo.userId, e); + return Color.TRANSPARENT; + } + try { + final IPackageManager packageManager = ActivityThread.getPackageManager(); + final String splashScreenThemeName = packageManager.getSplashScreenTheme(packageName, + userId); + final int splashScreenThemeId = splashScreenThemeName != null + ? windowContext.getResources().getIdentifier(splashScreenThemeName, null, null) + : 0; + + final int theme = getSplashScreenTheme(splashScreenThemeId, activityInfo); + + if (theme != windowContext.getThemeResId()) { + windowContext.setTheme(theme); + } + return mSplashscreenContentDrawer.estimateTaskBackgroundColor(windowContext); + } catch (RuntimeException | RemoteException e) { + Slog.w(TAG, "failed get starting window background color at taskId: " + + taskInfo.taskId, e); + } + return Color.TRANSPARENT; + } + + /** + * Called when the Task wants to copy the splash screen. + */ + public void copySplashScreenView(int taskId) { + final StartingSurfaceDrawer.StartingWindowRecord record = + mStartingWindowRecordManager.getRecord(taskId); + final SplashWindowRecord preView = record instanceof SplashWindowRecord + ? (SplashWindowRecord) record : null; + SplashScreenView.SplashScreenViewParcelable parcelable; + SplashScreenView splashScreenView = preView != null ? preView.mSplashView : null; + if (splashScreenView != null && splashScreenView.isCopyable()) { + parcelable = new SplashScreenView.SplashScreenViewParcelable(splashScreenView); + parcelable.setClientCallback( + new RemoteCallback((bundle) -> mSplashScreenExecutor.execute( + () -> onAppSplashScreenViewRemoved(taskId, false)))); + splashScreenView.onCopied(); + mAnimatedSplashScreenSurfaceHosts.append(taskId, splashScreenView.getSurfaceHost()); + } else { + parcelable = null; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "Copying splash screen window view for task: %d with parcelable %b", + taskId, parcelable != null); + ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable); + } + + /** + * Called when the {@link SplashScreenView} is removed from the client Activity view's hierarchy + * or when the Activity is clean up. + * + * @param taskId The Task id on which the splash screen was attached + */ + public void onAppSplashScreenViewRemoved(int taskId) { + onAppSplashScreenViewRemoved(taskId, true /* fromServer */); + } + + /** + * @param fromServer If true, this means the removal was notified by the server. This is only + * used for debugging purposes. + * @see #onAppSplashScreenViewRemoved(int) + */ + private void onAppSplashScreenViewRemoved(int taskId, boolean fromServer) { + SurfaceControlViewHost viewHost = + mAnimatedSplashScreenSurfaceHosts.get(taskId); + if (viewHost == null) { + return; + } + mAnimatedSplashScreenSurfaceHosts.remove(taskId); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "%s the splash screen. Releasing SurfaceControlViewHost for task: %d", + fromServer ? "Server cleaned up" : "App removed", taskId); + SplashScreenView.releaseIconHost(viewHost); + } + + protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, + WindowManager.LayoutParams params, + @StartingWindowInfo.StartingWindowType int suggestType) { + boolean shouldSaveView = true; + final Context context = view.getContext(); + try { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView"); + mWindowManagerGlobal.addView(view, params, display, + null /* parentWindow */, context.getUserId()); + } catch (WindowManager.BadTokenException e) { + // ignore + Slog.w(TAG, appToken + " already running, starting window not displayed. " + + e.getMessage()); + shouldSaveView = false; + } finally { + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + if (view.getParent() == null) { + Slog.w(TAG, "view not successfully added to wm, removing view"); + mWindowManagerGlobal.removeView(view, true /* immediate */); + shouldSaveView = false; + } + } + if (shouldSaveView) { + mStartingWindowRecordManager.removeWindow(taskId, true); + saveSplashScreenRecord(appToken, taskId, view, suggestType); + } + return shouldSaveView; + } + + private void saveSplashScreenRecord(IBinder appToken, int taskId, View view, + @StartingWindowInfo.StartingWindowType int suggestType) { + final SplashWindowRecord tView = + new SplashWindowRecord(appToken, view, suggestType); + mStartingWindowRecordManager.addRecord(taskId, tView); + } + + private void removeWindowInner(View decorView, boolean hideView) { + requestTopUi(false); + if (hideView) { + decorView.setVisibility(View.GONE); + } + mWindowManagerGlobal.removeView(decorView, false /* immediate */); + } + + private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> { + private SplashScreenView mView; + private boolean mIsViewSet; + private Runnable mUiThreadInitTask; + void setView(SplashScreenView view) { + synchronized (this) { + mView = view; + mIsViewSet = true; + notify(); + } + } + + void setUiThreadInitTask(Runnable initTask) { + synchronized (this) { + mUiThreadInitTask = initTask; + } + } + + @Override + @Nullable + public SplashScreenView get() { + synchronized (this) { + while (!mIsViewSet) { + try { + wait(); + } catch (InterruptedException ignored) { + } + } + if (mUiThreadInitTask != null) { + mUiThreadInitTask.run(); + mUiThreadInitTask = null; + } + return mView; + } + } + } + + private class SplashWindowRecord extends StartingSurfaceDrawer.StartingWindowRecord { + private final IBinder mAppToken; + private final View mRootView; + @StartingWindowInfo.StartingWindowType private final int mSuggestType; + private final long mCreateTime; + + private boolean mSetSplashScreen; + private SplashScreenView mSplashView; + private int mSystemBarAppearance; + private boolean mDrawsSystemBarBackgrounds; + + SplashWindowRecord(IBinder appToken, View decorView, + @StartingWindowInfo.StartingWindowType int suggestType) { + mAppToken = appToken; + mRootView = decorView; + mSuggestType = suggestType; + mCreateTime = SystemClock.uptimeMillis(); + } + + void setSplashScreenView(@Nullable SplashScreenView splashScreenView) { + if (mSetSplashScreen) { + return; + } + mSplashView = splashScreenView; + mBGColor = mSplashView != null ? mSplashView.getInitBackgroundColor() + : Color.TRANSPARENT; + mSetSplashScreen = true; + } + + void parseAppSystemBarColor(Context context) { + final TypedArray a = context.obtainStyledAttributes(R.styleable.Window); + mDrawsSystemBarBackgrounds = a.getBoolean( + R.styleable.Window_windowDrawsSystemBarBackgrounds, false); + if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) { + mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; + } + if (a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)) { + mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; + } + a.recycle(); + } + + // Reset the system bar color which set by splash screen, make it align to the app. + void clearSystemBarColor() { + if (mRootView == null || !mRootView.isAttachedToWindow()) { + return; + } + if (mRootView.getLayoutParams() instanceof WindowManager.LayoutParams) { + final WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mRootView.getLayoutParams(); + if (mDrawsSystemBarBackgrounds) { + lp.flags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; + } else { + lp.flags &= ~WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; + } + mRootView.setLayoutParams(lp); + } + mRootView.getWindowInsetsController().setSystemBarsAppearance( + mSystemBarAppearance, LIGHT_BARS_MASK); + } + + @Override + public void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { + if (mRootView == null) { + return; + } + if (mSplashView == null) { + // shouldn't happen, the app window may be drawn earlier than starting window? + Slog.e(TAG, "Found empty splash screen, remove!"); + removeWindowInner(mRootView, false); + return; + } + clearSystemBarColor(); + if (immediately + || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { + removeWindowInner(mRootView, false); + } else { + if (info.playRevealAnimation) { + mSplashscreenContentDrawer.applyExitAnimation(mSplashView, + info.windowAnimationLeash, info.mainFrame, + () -> removeWindowInner(mRootView, true), + mCreateTime, info.roundedCornerRadius); + } else { + // the SplashScreenView has been copied to client, hide the view to skip + // default exit animation + removeWindowInner(mRootView, true); + } + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java index 4f07bfeacce5..ff06db370d1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -16,169 +16,80 @@ package com.android.wm.shell.startingsurface; -import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.view.Choreographer.CALLBACK_INSETS_ANIMATION; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.view.Display.DEFAULT_DISPLAY; -import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN; -import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT; -import android.annotation.Nullable; -import android.app.ActivityManager.RunningTaskInfo; -import android.app.ActivityTaskManager; -import android.app.ActivityThread; +import android.annotation.CallSuper; import android.app.TaskInfo; +import android.app.WindowConfiguration; import android.content.Context; -import android.content.pm.ActivityInfo; -import android.content.pm.IPackageManager; -import android.content.pm.PackageManager; -import android.content.res.TypedArray; +import android.content.res.Configuration; import android.graphics.Color; -import android.graphics.PixelFormat; import android.hardware.display.DisplayManager; -import android.os.IBinder; -import android.os.RemoteCallback; -import android.os.RemoteException; -import android.os.SystemClock; -import android.os.Trace; -import android.os.UserHandle; -import android.util.Slog; import android.util.SparseArray; -import android.view.Choreographer; -import android.view.Display; -import android.view.SurfaceControlViewHost; -import android.view.View; -import android.view.WindowInsetsController; +import android.view.IWindow; +import android.view.SurfaceControl; +import android.view.SurfaceSession; import android.view.WindowManager; -import android.view.WindowManagerGlobal; -import android.widget.FrameLayout; +import android.view.WindowlessWindowManager; import android.window.SplashScreenView; -import android.window.SplashScreenView.SplashScreenViewParcelable; import android.window.StartingWindowInfo; import android.window.StartingWindowInfo.StartingWindowType; import android.window.StartingWindowRemovalInfo; import android.window.TaskSnapshot; -import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; -import com.android.internal.util.ContrastColorUtil; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellSplashscreenThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.function.Supplier; - /** * A class which able to draw splash screen or snapshot as the starting window for a task. - * - * In order to speed up, there will use two threads to creating a splash screen in parallel. - * Right now we are still using PhoneWindow to create splash screen window, so the view is added to - * the ViewRootImpl, and those view won't be draw immediately because the ViewRootImpl will call - * scheduleTraversal to register a callback from Choreographer, so the drawing result of the view - * can synchronize on each frame. - * - * The bad thing is that we cannot decide when would Choreographer#doFrame happen, and drawing - * the AdaptiveIconDrawable object can be time consuming, so we use the splash-screen background - * thread to draw the AdaptiveIconDrawable object to a Bitmap and cache it to a BitmapShader after - * the SplashScreenView just created, once we get the BitmapShader then the #draw call can be very - * quickly. - * - * So basically we are using the spare time to prepare the SplashScreenView while splash screen - * thread is waiting for - * 1. WindowManager#addView(binder call to WM), - * 2. Choreographer#doFrame happen(uncertain time for next frame, depends on device), - * 3. Session#relayout(another binder call to WM which under Choreographer#doFrame, but will - * always happen before #draw). - * Because above steps are running on splash-screen thread, so pre-draw the BitmapShader on - * splash-screen background tread can make they execute in parallel, which ensure it is faster then - * to draw the AdaptiveIconDrawable when receive callback from Choreographer#doFrame. - * - * Here is the sequence to compare the difference between using single and two thread. - * - * Single thread: - * => makeSplashScreenContentView -> WM#addView .. waiting for Choreographer#doFrame -> relayout - * -> draw -> AdaptiveIconDrawable#draw - * - * Two threads: - * => makeSplashScreenContentView -> cachePaint(=AdaptiveIconDrawable#draw) - * => WM#addView -> .. waiting for Choreographer#doFrame -> relayout -> draw -> (draw the Paint - * directly). */ @ShellSplashscreenThread public class StartingSurfaceDrawer { - private static final String TAG = StartingWindowController.TAG; - private final Context mContext; - private final DisplayManager mDisplayManager; private final ShellExecutor mSplashScreenExecutor; @VisibleForTesting final SplashscreenContentDrawer mSplashscreenContentDrawer; - private Choreographer mChoreographer; - private final WindowManagerGlobal mWindowManagerGlobal; - private StartingSurface.SysuiProxy mSysuiProxy; - private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo(); - - private static final int LIGHT_BARS_MASK = - WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS - | WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; - /** - * The minimum duration during which the splash screen is shown when the splash screen icon is - * animated. - */ - static final long MINIMAL_ANIMATION_DURATION = 400L; - - /** - * Allow the icon style splash screen to be displayed for longer to give time for the animation - * to finish, i.e. the extra buffer time to keep the splash screen if the animation is slightly - * longer than the {@link #MINIMAL_ANIMATION_DURATION} duration. - */ - static final long TIME_WINDOW_DURATION = 100L; - - /** - * The maximum duration during which the splash screen will be shown if the application is ready - * to show before the icon animation finishes. - */ - static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION; + @VisibleForTesting + final SplashscreenWindowCreator mSplashscreenWindowCreator; + private final SnapshotWindowCreator mSnapshotWindowCreator; + private final WindowlessSplashWindowCreator mWindowlessSplashWindowCreator; + private final WindowlessSnapshotWindowCreator mWindowlessSnapshotWindowCreator; + @VisibleForTesting + final StartingWindowRecordManager mWindowRecords = new StartingWindowRecordManager(); + // Windowless surface could co-exist with starting window in a task. + @VisibleForTesting + final StartingWindowRecordManager mWindowlessRecords = new StartingWindowRecordManager(); /** * @param splashScreenExecutor The thread used to control add and remove starting window. */ public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, IconProvider iconProvider, TransactionPool pool) { - mContext = context; - mDisplayManager = mContext.getSystemService(DisplayManager.class); mSplashScreenExecutor = splashScreenExecutor; - mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool); - mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance()); - mWindowManagerGlobal = WindowManagerGlobal.getInstance(); - mDisplayManager.getDisplay(DEFAULT_DISPLAY); - } - - @VisibleForTesting - final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>(); - - /** - * Records of {@link SurfaceControlViewHost} where the splash screen icon animation is - * rendered and that have not yet been removed by their client. - */ - private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts = - new SparseArray<>(1); - - private Display getDisplay(int displayId) { - return mDisplayManager.getDisplay(displayId); - } - - int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) { - return splashScreenThemeResId != 0 - ? splashScreenThemeResId - : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource() - : com.android.internal.R.style.Theme_DeviceDefault_DayNight; + final DisplayManager displayManager = context.getSystemService(DisplayManager.class); + mSplashscreenContentDrawer = new SplashscreenContentDrawer(context, iconProvider, pool); + displayManager.getDisplay(DEFAULT_DISPLAY); + + mSplashscreenWindowCreator = new SplashscreenWindowCreator(mSplashscreenContentDrawer, + context, splashScreenExecutor, displayManager, mWindowRecords); + mSnapshotWindowCreator = new SnapshotWindowCreator(splashScreenExecutor, + mWindowRecords); + mWindowlessSplashWindowCreator = new WindowlessSplashWindowCreator( + mSplashscreenContentDrawer, context, splashScreenExecutor, displayManager, + mWindowlessRecords, pool); + mWindowlessSnapshotWindowCreator = new WindowlessSnapshotWindowCreator( + mWindowlessRecords, context, displayManager, mSplashscreenContentDrawer, pool); } void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) { - mSysuiProxy = sysuiProxy; + mSplashscreenWindowCreator.setSysuiProxy(sysuiProxy); + mWindowlessSplashWindowCreator.setSysuiProxy(sysuiProxy); } /** @@ -186,231 +97,55 @@ public class StartingSurfaceDrawer { * * @param suggestType The suggestion type to draw the splash screen. */ - void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken, + void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, @StartingWindowType int suggestType) { - final RunningTaskInfo taskInfo = windowInfo.taskInfo; - final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null - ? windowInfo.targetActivityInfo - : taskInfo.topActivityInfo; - if (activityInfo == null || activityInfo.packageName == null) { - return; - } - // replace with the default theme if the application didn't set - final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo); - final Context context = SplashscreenContentDrawer.createContext(mContext, windowInfo, theme, - suggestType, mDisplayManager); - if (context == null) { - return; - } - final WindowManager.LayoutParams params = SplashscreenContentDrawer.createLayoutParameters( - context, windowInfo, suggestType, activityInfo.packageName, - suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN - ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT, appToken); - - final int displayId = taskInfo.displayId; - final int taskId = taskInfo.taskId; - final Display display = getDisplay(displayId); - - // TODO(b/173975965) tracking performance - // Prepare the splash screen content view on splash screen worker thread in parallel, so the - // content view won't be blocked by binder call like addWindow and relayout. - // 1. Trigger splash screen worker thread to create SplashScreenView before/while - // Session#addWindow. - // 2. Synchronize the SplashscreenView to splash screen thread before Choreographer start - // traversal, which will call Session#relayout on splash screen thread. - // 3. Pre-draw the BitmapShader if the icon is immobile on splash screen worker thread, at - // the same time the splash screen thread should be executing Session#relayout. Blocking the - // traversal -> draw on splash screen thread until the BitmapShader of the icon is ready. - - // Record whether create splash screen view success, notify to current thread after - // create splash screen view finished. - final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier(); - final FrameLayout rootLayout = new FrameLayout( - mSplashscreenContentDrawer.createViewContextWrapper(context)); - rootLayout.setPadding(0, 0, 0, 0); - rootLayout.setFitsSystemWindows(false); - final Runnable setViewSynchronized = () -> { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView"); - // waiting for setContentView before relayoutWindow - SplashScreenView contentView = viewSupplier.get(); - final StartingWindowRecord record = mStartingWindowRecords.get(taskId); - // If record == null, either the starting window added fail or removed already. - // Do not add this view if the token is mismatch. - if (record != null && appToken == record.mAppToken) { - // if view == null then creation of content view was failed. - if (contentView != null) { - try { - rootLayout.addView(contentView); - } catch (RuntimeException e) { - Slog.w(TAG, "failed set content view to starting window " - + "at taskId: " + taskId, e); - contentView = null; - } - } - record.setSplashScreenView(contentView); - } - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - }; - if (mSysuiProxy != null) { - mSysuiProxy.requestTopUi(true, TAG); - } - mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo, - viewSupplier::setView, viewSupplier::setUiThreadInitTask); - try { - if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) { - // We use the splash screen worker thread to create SplashScreenView while adding - // the window, as otherwise Choreographer#doFrame might be delayed on this thread. - // And since Choreographer#doFrame won't happen immediately after adding the window, - // if the view is not added to the PhoneWindow on the first #doFrame, the view will - // not be rendered on the first frame. So here we need to synchronize the view on - // the window before first round relayoutWindow, which will happen after insets - // animation. - mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null); - final StartingWindowRecord record = mStartingWindowRecords.get(taskId); - record.parseAppSystemBarColor(context); - // Block until we get the background color. - final SplashScreenView contentView = viewSupplier.get(); - if (suggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { - contentView.addOnAttachStateChangeListener( - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - final int lightBarAppearance = ContrastColorUtil.isColorLight( - contentView.getInitBackgroundColor()) - ? LIGHT_BARS_MASK : 0; - contentView.getWindowInsetsController().setSystemBarsAppearance( - lightBarAppearance, LIGHT_BARS_MASK); - } - - @Override - public void onViewDetachedFromWindow(View v) { - } - }); - } - record.mBGColor = contentView.getInitBackgroundColor(); - } else { - // release the icon view host - final SplashScreenView contentView = viewSupplier.get(); - if (contentView.getSurfaceHost() != null) { - SplashScreenView.releaseIconHost(contentView.getSurfaceHost()); - } - } - } catch (RuntimeException e) { - // don't crash if something else bad happens, for example a - // failure loading resources because we are loading from an app - // on external storage that has been unmounted. - Slog.w(TAG, "failed creating starting window at taskId: " + taskId, e); - } + mSplashscreenWindowCreator.addSplashScreenStartingWindow(windowInfo, suggestType); } int getStartingWindowBackgroundColorForTask(int taskId) { - final StartingWindowRecord startingWindowRecord = mStartingWindowRecords.get(taskId); + final StartingWindowRecord startingWindowRecord = mWindowRecords.getRecord(taskId); if (startingWindowRecord == null) { return Color.TRANSPARENT; } - return startingWindowRecord.mBGColor; - } - - private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> { - private SplashScreenView mView; - private boolean mIsViewSet; - private Runnable mUiThreadInitTask; - void setView(SplashScreenView view) { - synchronized (this) { - mView = view; - mIsViewSet = true; - notify(); - } - } - - void setUiThreadInitTask(Runnable initTask) { - synchronized (this) { - mUiThreadInitTask = initTask; - } - } - - @Override - @Nullable - public SplashScreenView get() { - synchronized (this) { - while (!mIsViewSet) { - try { - wait(); - } catch (InterruptedException ignored) { - } - } - if (mUiThreadInitTask != null) { - mUiThreadInitTask.run(); - mUiThreadInitTask = null; - } - return mView; - } - } + return startingWindowRecord.getBGColor(); } int estimateTaskBackgroundColor(TaskInfo taskInfo) { - if (taskInfo.topActivityInfo == null) { - return Color.TRANSPARENT; - } - final ActivityInfo activityInfo = taskInfo.topActivityInfo; - final String packageName = activityInfo.packageName; - final int userId = taskInfo.userId; - final Context windowContext; - try { - windowContext = mContext.createPackageContextAsUser( - packageName, Context.CONTEXT_RESTRICTED, UserHandle.of(userId)); - } catch (PackageManager.NameNotFoundException e) { - Slog.w(TAG, "Failed creating package context with package name " - + packageName + " for user " + taskInfo.userId, e); - return Color.TRANSPARENT; - } - try { - final IPackageManager packageManager = ActivityThread.getPackageManager(); - final String splashScreenThemeName = packageManager.getSplashScreenTheme(packageName, - userId); - final int splashScreenThemeId = splashScreenThemeName != null - ? windowContext.getResources().getIdentifier(splashScreenThemeName, null, null) - : 0; - - final int theme = getSplashScreenTheme(splashScreenThemeId, activityInfo); - - if (theme != windowContext.getThemeResId()) { - windowContext.setTheme(theme); - } - return mSplashscreenContentDrawer.estimateTaskBackgroundColor(windowContext); - } catch (RuntimeException | RemoteException e) { - Slog.w(TAG, "failed get starting window background color at taskId: " - + taskInfo.taskId, e); - } - return Color.TRANSPARENT; + return mSplashscreenWindowCreator.estimateTaskBackgroundColor(taskInfo); } /** * Called when a task need a snapshot starting window. */ - void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, IBinder appToken, - TaskSnapshot snapshot) { - final int taskId = startingWindowInfo.taskInfo.taskId; - // Remove any existing starting window for this task before adding. - removeWindowNoAnimate(taskId); - final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo, appToken, - snapshot, mSplashScreenExecutor, () -> removeWindowNoAnimate(taskId)); - if (surface == null) { - return; - } - final StartingWindowRecord tView = new StartingWindowRecord(appToken, - null/* decorView */, surface, STARTING_WINDOW_TYPE_SNAPSHOT); - mStartingWindowRecords.put(taskId, tView); + void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, TaskSnapshot snapshot) { + mSnapshotWindowCreator.makeTaskSnapshotWindow(startingWindowInfo, snapshot); } /** * Called when the content of a task is ready to show, starting window can be removed. */ public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Task start finish, remove starting surface for task: %d", - removalInfo.taskId); - removeWindowSynced(removalInfo, false /* immediately */); + if (removalInfo.windowlessSurface) { + mWindowlessRecords.removeWindow(removalInfo, removalInfo.removeImmediately); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "Task start finish, remove starting surface for task: %d", + removalInfo.taskId); + mWindowRecords.removeWindow(removalInfo, removalInfo.removeImmediately); + } + } + + /** + * Create a windowless starting surface and attach to the root surface. + */ + void addWindowlessStartingSurface(StartingWindowInfo windowInfo) { + if (windowInfo.taskSnapshot != null) { + mWindowlessSnapshotWindowCreator.makeTaskSnapshotWindow(windowInfo, + windowInfo.rootSurface, windowInfo.taskSnapshot, mSplashScreenExecutor); + } else { + mWindowlessSplashWindowCreator.addSplashScreenStartingWindow( + windowInfo, windowInfo.rootSurface); + } } /** @@ -419,37 +154,15 @@ public class StartingSurfaceDrawer { public void clearAllWindows() { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, "Clear all starting windows immediately"); - final int taskSize = mStartingWindowRecords.size(); - final int[] taskIds = new int[taskSize]; - for (int i = taskSize - 1; i >= 0; --i) { - taskIds[i] = mStartingWindowRecords.keyAt(i); - } - for (int i = taskSize - 1; i >= 0; --i) { - removeWindowNoAnimate(taskIds[i]); - } + mWindowRecords.clearAllWindows(); + mWindowlessRecords.clearAllWindows(); } /** * Called when the Task wants to copy the splash screen. */ public void copySplashScreenView(int taskId) { - final StartingWindowRecord preView = mStartingWindowRecords.get(taskId); - SplashScreenViewParcelable parcelable; - SplashScreenView splashScreenView = preView != null ? preView.mContentView : null; - if (splashScreenView != null && splashScreenView.isCopyable()) { - parcelable = new SplashScreenViewParcelable(splashScreenView); - parcelable.setClientCallback( - new RemoteCallback((bundle) -> mSplashScreenExecutor.execute( - () -> onAppSplashScreenViewRemoved(taskId, false)))); - splashScreenView.onCopied(); - mAnimatedSplashScreenSurfaceHosts.append(taskId, splashScreenView.getSurfaceHost()); - } else { - parcelable = null; - } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Copying splash screen window view for task: %d with parcelable %b", - taskId, parcelable != null); - ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable); + mSplashscreenWindowCreator.copySplashScreenView(taskId); } /** @@ -459,195 +172,148 @@ public class StartingSurfaceDrawer { * @param taskId The Task id on which the splash screen was attached */ public void onAppSplashScreenViewRemoved(int taskId) { - onAppSplashScreenViewRemoved(taskId, true /* fromServer */); + mSplashscreenWindowCreator.onAppSplashScreenViewRemoved(taskId); } - /** - * @param fromServer If true, this means the removal was notified by the server. This is only - * used for debugging purposes. - * @see #onAppSplashScreenViewRemoved(int) - */ - private void onAppSplashScreenViewRemoved(int taskId, boolean fromServer) { - SurfaceControlViewHost viewHost = - mAnimatedSplashScreenSurfaceHosts.get(taskId); - if (viewHost == null) { - return; + void onImeDrawnOnTask(int taskId) { + onImeDrawnOnTask(mWindowRecords, taskId); + onImeDrawnOnTask(mWindowlessRecords, taskId); + } + + private void onImeDrawnOnTask(StartingWindowRecordManager records, int taskId) { + final StartingSurfaceDrawer.StartingWindowRecord sRecord = + records.getRecord(taskId); + final SnapshotRecord record = sRecord instanceof SnapshotRecord + ? (SnapshotRecord) sRecord : null; + if (record != null && record.hasImeSurface()) { + records.removeWindow(taskId, true); } - mAnimatedSplashScreenSurfaceHosts.remove(taskId); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "%s the splash screen. Releasing SurfaceControlViewHost for task: %d", - fromServer ? "Server cleaned up" : "App removed", taskId); - SplashScreenView.releaseIconHost(viewHost); } - protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, - WindowManager.LayoutParams params, @StartingWindowType int suggestType) { - boolean shouldSaveView = true; - final Context context = view.getContext(); - try { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView"); - mWindowManagerGlobal.addView(view, params, display, - null /* parentWindow */, context.getUserId()); - } catch (WindowManager.BadTokenException e) { - // ignore - Slog.w(TAG, appToken + " already running, starting window not displayed. " - + e.getMessage()); - shouldSaveView = false; - } finally { - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - if (view.getParent() == null) { - Slog.w(TAG, "view not successfully added to wm, removing view"); - mWindowManagerGlobal.removeView(view, true /* immediate */); - shouldSaveView = false; + static class WindowlessStartingWindow extends WindowlessWindowManager { + SurfaceControl mChildSurface; + + WindowlessStartingWindow(Configuration c, SurfaceControl rootSurface) { + super(c, rootSurface, null /* hostInputToken */); + } + + @Override + protected SurfaceControl getParentSurface(IWindow window, + WindowManager.LayoutParams attrs) { + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName("Windowless window") + .setHidden(false) + .setParent(mRootSurface) + .setCallsite("WindowlessStartingWindow#attachToParentSurface"); + mChildSurface = builder.build(); + try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) { + t.setLayer(mChildSurface, Integer.MAX_VALUE); + t.apply(); } + return mChildSurface; } - if (shouldSaveView) { - removeWindowNoAnimate(taskId); - saveSplashScreenRecord(appToken, taskId, view, suggestType); + } + abstract static class StartingWindowRecord { + protected int mBGColor; + abstract void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately); + int getBGColor() { + return mBGColor; } - return shouldSaveView; } - @VisibleForTesting - void saveSplashScreenRecord(IBinder appToken, int taskId, View view, - @StartingWindowType int suggestType) { - final StartingWindowRecord tView = new StartingWindowRecord(appToken, view, - null/* TaskSnapshotWindow */, suggestType); - mStartingWindowRecords.put(taskId, tView); - } + abstract static class SnapshotRecord extends StartingWindowRecord { + private static final long DELAY_REMOVAL_TIME_GENERAL = 100; + /** + * The max delay time in milliseconds for removing the task snapshot window with IME + * visible. + * Ideally the delay time will be shorter when receiving + * {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}. + */ + private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 600; + private final Runnable mScheduledRunnable = this::removeImmediately; + + @WindowConfiguration.ActivityType protected final int mActivityType; + protected final ShellExecutor mRemoveExecutor; + + SnapshotRecord(int activityType, ShellExecutor removeExecutor) { + mActivityType = activityType; + mRemoveExecutor = removeExecutor; + } - private void removeWindowNoAnimate(int taskId) { - mTmpRemovalInfo.taskId = taskId; - removeWindowSynced(mTmpRemovalInfo, true /* immediately */); - } + @Override + public final void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { + if (immediately) { + removeImmediately(); + } else { + scheduleRemove(info.deferRemoveForIme); + } + } - void onImeDrawnOnTask(int taskId) { - final StartingWindowRecord record = mStartingWindowRecords.get(taskId); - if (record != null && record.mTaskSnapshotWindow != null - && record.mTaskSnapshotWindow.hasImeSurface()) { - removeWindowNoAnimate(taskId); + void scheduleRemove(boolean deferRemoveForIme) { + // Show the latest content as soon as possible for unlocking to home. + if (mActivityType == ACTIVITY_TYPE_HOME) { + removeImmediately(); + return; + } + mRemoveExecutor.removeCallbacks(mScheduledRunnable); + final long delayRemovalTime = hasImeSurface() && deferRemoveForIme + ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE + : DELAY_REMOVAL_TIME_GENERAL; + mRemoveExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, + "Defer removing snapshot surface in %d", delayRemovalTime); + } + + protected abstract boolean hasImeSurface(); + + @CallSuper + protected void removeImmediately() { + mRemoveExecutor.removeCallbacks(mScheduledRunnable); } } - protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo, boolean immediately) { - final int taskId = removalInfo.taskId; - final StartingWindowRecord record = mStartingWindowRecords.get(taskId); - if (record != null) { - if (record.mDecorView != null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Removing splash screen window for task: %d", taskId); - if (record.mContentView != null) { - record.clearSystemBarColor(); - if (immediately - || record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { - removeWindowInner(record.mDecorView, false); - } else { - if (removalInfo.playRevealAnimation) { - mSplashscreenContentDrawer.applyExitAnimation(record.mContentView, - removalInfo.windowAnimationLeash, removalInfo.mainFrame, - () -> removeWindowInner(record.mDecorView, true), - record.mCreateTime, removalInfo.roundedCornerRadius); - } else { - // the SplashScreenView has been copied to client, hide the view to skip - // default exit animation - removeWindowInner(record.mDecorView, true); - } - } - } else { - // shouldn't happen - Slog.e(TAG, "Found empty splash screen, remove!"); - removeWindowInner(record.mDecorView, false); - } + static class StartingWindowRecordManager { + private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo(); + private final SparseArray<StartingWindowRecord> mStartingWindowRecords = + new SparseArray<>(); + void clearAllWindows() { + final int taskSize = mStartingWindowRecords.size(); + final int[] taskIds = new int[taskSize]; + for (int i = taskSize - 1; i >= 0; --i) { + taskIds[i] = mStartingWindowRecords.keyAt(i); } - if (record.mTaskSnapshotWindow != null) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Removing task snapshot window for %d", taskId); - if (immediately) { - record.mTaskSnapshotWindow.removeImmediately(); - } else { - record.mTaskSnapshotWindow.scheduleRemove(removalInfo.deferRemoveForIme); - } + for (int i = taskSize - 1; i >= 0; --i) { + removeWindow(taskIds[i], true); } - mStartingWindowRecords.remove(taskId); } - } - private void removeWindowInner(View decorView, boolean hideView) { - if (mSysuiProxy != null) { - mSysuiProxy.requestTopUi(false, TAG); - } - if (hideView) { - decorView.setVisibility(View.GONE); + void addRecord(int taskId, StartingWindowRecord record) { + mStartingWindowRecords.put(taskId, record); } - mWindowManagerGlobal.removeView(decorView, false /* immediate */); - } - /** - * Record the view or surface for a starting window. - */ - private static class StartingWindowRecord { - private final IBinder mAppToken; - private final View mDecorView; - private final TaskSnapshotWindow mTaskSnapshotWindow; - private SplashScreenView mContentView; - private boolean mSetSplashScreen; - @StartingWindowType private int mSuggestType; - private int mBGColor; - private final long mCreateTime; - private int mSystemBarAppearance; - private boolean mDrawsSystemBarBackgrounds; - - StartingWindowRecord(IBinder appToken, View decorView, - TaskSnapshotWindow taskSnapshotWindow, @StartingWindowType int suggestType) { - mAppToken = appToken; - mDecorView = decorView; - mTaskSnapshotWindow = taskSnapshotWindow; - if (mTaskSnapshotWindow != null) { - mBGColor = mTaskSnapshotWindow.getBackgroundColor(); + void removeWindow(StartingWindowRemovalInfo removeInfo, boolean immediately) { + final int taskId = removeInfo.taskId; + final StartingWindowRecord record = mStartingWindowRecords.get(taskId); + if (record != null) { + record.removeIfPossible(removeInfo, immediately); + mStartingWindowRecords.remove(taskId); } - mSuggestType = suggestType; - mCreateTime = SystemClock.uptimeMillis(); } - private void setSplashScreenView(SplashScreenView splashScreenView) { - if (mSetSplashScreen) { - return; - } - mContentView = splashScreenView; - mSetSplashScreen = true; + void removeWindow(int taskId, boolean immediately) { + mTmpRemovalInfo.taskId = taskId; + removeWindow(mTmpRemovalInfo, immediately); } - private void parseAppSystemBarColor(Context context) { - final TypedArray a = context.obtainStyledAttributes(R.styleable.Window); - mDrawsSystemBarBackgrounds = a.getBoolean( - R.styleable.Window_windowDrawsSystemBarBackgrounds, false); - if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) { - mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; - } - if (a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)) { - mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; - } - a.recycle(); + StartingWindowRecord getRecord(int taskId) { + return mStartingWindowRecords.get(taskId); } - // Reset the system bar color which set by splash screen, make it align to the app. - private void clearSystemBarColor() { - if (mDecorView == null || !mDecorView.isAttachedToWindow()) { - return; - } - if (mDecorView.getLayoutParams() instanceof WindowManager.LayoutParams) { - final WindowManager.LayoutParams lp = - (WindowManager.LayoutParams) mDecorView.getLayoutParams(); - if (mDrawsSystemBarBackgrounds) { - lp.flags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; - } else { - lp.flags &= ~WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; - } - mDecorView.setLayoutParams(lp); - } - mDecorView.getWindowInsetsController().setSystemBarsAppearance( - mSystemBarAppearance, LIGHT_BARS_MASK); + @VisibleForTesting + int recordSize() { + return mStartingWindowRecords.size(); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java index be2e79342d07..bec4ba3bf0d1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java @@ -21,6 +21,7 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; +import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_WINDOWLESS; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW; @@ -29,7 +30,6 @@ import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; import android.content.Context; import android.graphics.Color; -import android.os.IBinder; import android.os.Trace; import android.util.SparseIntArray; import android.window.StartingWindowInfo; @@ -152,22 +152,23 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo /** * Called when a task need a starting window. */ - public void addStartingWindow(StartingWindowInfo windowInfo, IBinder appToken) { + public void addStartingWindow(StartingWindowInfo windowInfo) { mSplashScreenExecutor.execute(() -> { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addStartingWindow"); final int suggestionType = mStartingWindowTypeAlgorithm.getSuggestedWindowType( windowInfo); final RunningTaskInfo runningTaskInfo = windowInfo.taskInfo; - if (isSplashScreenType(suggestionType)) { - mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, appToken, - suggestionType); + if (suggestionType == STARTING_WINDOW_TYPE_WINDOWLESS) { + mStartingSurfaceDrawer.addWindowlessStartingSurface(windowInfo); + } else if (isSplashScreenType(suggestionType)) { + mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, suggestionType); } else if (suggestionType == STARTING_WINDOW_TYPE_SNAPSHOT) { final TaskSnapshot snapshot = windowInfo.taskSnapshot; - mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, appToken, - snapshot); + mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, snapshot); } - if (suggestionType != STARTING_WINDOW_TYPE_NONE) { + if (suggestionType != STARTING_WINDOW_TYPE_NONE + && suggestionType != STARTING_WINDOW_TYPE_WINDOWLESS) { int taskId = runningTaskInfo.taskId; int color = mStartingSurfaceDrawer .getStartingWindowBackgroundColorForTask(taskId); @@ -218,11 +219,13 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.removeStartingWindow( removalInfo)); - mSplashScreenExecutor.executeDelayed(() -> { - synchronized (mTaskBackgroundColors) { - mTaskBackgroundColors.delete(removalInfo.taskId); - } - }, TASK_BG_COLOR_RETAIN_TIME_MS); + if (!removalInfo.windowlessSurface) { + mSplashScreenExecutor.executeDelayed(() -> { + synchronized (mTaskBackgroundColors) { + mTaskBackgroundColors.delete(removalInfo.taskId); + } + }, TASK_BG_COLOR_RETAIN_TIME_MS); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index a05ed4f24a08..c964df1452e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -16,7 +16,6 @@ package com.android.wm.shell.startingsurface; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.graphics.Color.WHITE; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; @@ -63,24 +62,14 @@ public class TaskSnapshotWindow { private static final String TAG = StartingWindowController.TAG; private static final String TITLE_FORMAT = "SnapshotStartingWindow for taskId="; - private static final long DELAY_REMOVAL_TIME_GENERAL = 100; - /** - * The max delay time in milliseconds for removing the task snapshot window with IME visible. - * Ideally the delay time will be shorter when receiving - * {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}. - */ - private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 600; - private final Window mWindow; private final Runnable mClearWindowHandler; private final ShellExecutor mSplashScreenExecutor; private final IWindowSession mSession; private boolean mHasDrawn; private final Paint mBackgroundPaint = new Paint(); - private final int mActivityType; private final int mOrientationOnCreation; - private final Runnable mScheduledRunnable = this::removeImmediately; private final boolean mHasImeSurface; static TaskSnapshotWindow create(StartingWindowInfo info, IBinder appToken, @@ -104,7 +93,6 @@ public class TaskSnapshotWindow { final Point taskSize = snapshot.getTaskSize(); final Rect taskBounds = new Rect(0, 0, taskSize.x, taskSize.y); final int orientation = snapshot.getOrientation(); - final int activityType = runningTaskInfo.topActivityType; final int displayId = runningTaskInfo.displayId; final IWindowSession session = WindowManagerGlobal.getWindowSession(); @@ -114,16 +102,11 @@ public class TaskSnapshotWindow { final InsetsSourceControl.Array tmpControls = new InsetsSourceControl.Array(); final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); - final TaskDescription taskDescription; - if (runningTaskInfo.taskDescription != null) { - taskDescription = runningTaskInfo.taskDescription; - } else { - taskDescription = new TaskDescription(); - taskDescription.setBackgroundColor(WHITE); - } + final TaskDescription taskDescription = + SnapshotDrawerUtils.getOrCreateTaskDescription(runningTaskInfo); final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow( - snapshot, taskDescription, orientation, activityType, + snapshot, taskDescription, orientation, clearWindowHandler, splashScreenExecutor); final Window window = snapshotSurface.mWindow; @@ -153,6 +136,8 @@ public class TaskSnapshotWindow { Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); + Slog.w(TAG, "Failed to relayout snapshot starting window"); + return null; } SnapshotDrawerUtils.drawSnapshotOnSurface(info, layoutParams, surfaceControl, snapshot, @@ -164,7 +149,7 @@ public class TaskSnapshotWindow { } public TaskSnapshotWindow(TaskSnapshot snapshot, TaskDescription taskDescription, - int currentOrientation, int activityType, Runnable clearWindowHandler, + int currentOrientation, Runnable clearWindowHandler, ShellExecutor splashScreenExecutor) { mSplashScreenExecutor = splashScreenExecutor; mSession = WindowManagerGlobal.getWindowSession(); @@ -173,7 +158,6 @@ public class TaskSnapshotWindow { int backgroundColor = taskDescription.getBackgroundColor(); mBackgroundPaint.setColor(backgroundColor != 0 ? backgroundColor : WHITE); mOrientationOnCreation = currentOrientation; - mActivityType = activityType; mClearWindowHandler = clearWindowHandler; mHasImeSurface = snapshot.hasImeSurface(); } @@ -186,23 +170,7 @@ public class TaskSnapshotWindow { return mHasImeSurface; } - void scheduleRemove(boolean deferRemoveForIme) { - // Show the latest content as soon as possible for unlocking to home. - if (mActivityType == ACTIVITY_TYPE_HOME) { - removeImmediately(); - return; - } - mSplashScreenExecutor.removeCallbacks(mScheduledRunnable); - final long delayRemovalTime = mHasImeSurface && deferRemoveForIme - ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE - : DELAY_REMOVAL_TIME_GENERAL; - mSplashScreenExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "Defer removing snapshot surface in %d", delayRemovalTime); - } - void removeImmediately() { - mSplashScreenExecutor.removeCallbacks(mScheduledRunnable); try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, "Removing taskSnapshot surface, mHasDrawn=%b", mHasDrawn); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java new file mode 100644 index 000000000000..144547885501 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java @@ -0,0 +1,165 @@ +/* + * 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.startingsurface; + +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.view.Display; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.window.SnapshotDrawerUtils; +import android.window.StartingWindowInfo; +import android.window.TaskSnapshot; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +class WindowlessSnapshotWindowCreator { + private static final int DEFAULT_FADEOUT_DURATION = 233; + private final StartingSurfaceDrawer.StartingWindowRecordManager + mStartingWindowRecordManager; + private final DisplayManager mDisplayManager; + private final Context mContext; + private final SplashscreenContentDrawer mSplashscreenContentDrawer; + private final TransactionPool mTransactionPool; + + WindowlessSnapshotWindowCreator( + StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager, + Context context, + DisplayManager displayManager, SplashscreenContentDrawer splashscreenContentDrawer, + TransactionPool transactionPool) { + mStartingWindowRecordManager = startingWindowRecordManager; + mContext = context; + mDisplayManager = displayManager; + mSplashscreenContentDrawer = splashscreenContentDrawer; + mTransactionPool = transactionPool; + } + + void makeTaskSnapshotWindow(StartingWindowInfo info, SurfaceControl rootSurface, + TaskSnapshot snapshot, ShellExecutor removeExecutor) { + final ActivityManager.RunningTaskInfo runningTaskInfo = info.taskInfo; + final int taskId = runningTaskInfo.taskId; + final String title = "Windowless Snapshot " + taskId; + final WindowManager.LayoutParams lp = SnapshotDrawerUtils.createLayoutParameters( + info, title, TYPE_APPLICATION_OVERLAY, snapshot.getHardwareBuffer().getFormat(), + null /* token */); + if (lp == null) { + return; + } + final Display display = mDisplayManager.getDisplay(runningTaskInfo.displayId); + final StartingSurfaceDrawer.WindowlessStartingWindow wlw = + new StartingSurfaceDrawer.WindowlessStartingWindow( + runningTaskInfo.configuration, rootSurface); + final SurfaceControlViewHost mViewHost = new SurfaceControlViewHost( + mContext, display, wlw, "WindowlessSnapshotWindowCreator"); + final Point taskSize = snapshot.getTaskSize(); + final Rect snapshotBounds = new Rect(0, 0, taskSize.x, taskSize.y); + final Rect windowBounds = runningTaskInfo.configuration.windowConfiguration.getBounds(); + final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState; + final FrameLayout rootLayout = new FrameLayout( + mSplashscreenContentDrawer.createViewContextWrapper(mContext)); + mViewHost.setView(rootLayout, lp); + SnapshotDrawerUtils.drawSnapshotOnSurface(info, lp, wlw.mChildSurface, snapshot, + snapshotBounds, windowBounds, topWindowInsetsState, false /* releaseAfterDraw */); + + final ActivityManager.TaskDescription taskDescription = + SnapshotDrawerUtils.getOrCreateTaskDescription(runningTaskInfo); + + final SnapshotWindowRecord record = new SnapshotWindowRecord(mViewHost, wlw.mChildSurface, + taskDescription.getBackgroundColor(), snapshot.hasImeSurface(), + runningTaskInfo.topActivityType, removeExecutor); + mStartingWindowRecordManager.addRecord(taskId, record); + info.notifyAddComplete(wlw.mChildSurface); + } + + private class SnapshotWindowRecord extends StartingSurfaceDrawer.SnapshotRecord { + private SurfaceControlViewHost mViewHost; + private SurfaceControl mChildSurface; + private final boolean mHasImeSurface; + + SnapshotWindowRecord(SurfaceControlViewHost viewHost, SurfaceControl childSurface, + int bgColor, boolean hasImeSurface, int activityType, + ShellExecutor removeExecutor) { + super(activityType, removeExecutor); + mViewHost = viewHost; + mChildSurface = childSurface; + mBGColor = bgColor; + mHasImeSurface = hasImeSurface; + } + + @Override + protected void removeImmediately() { + super.removeImmediately(); + fadeoutThenRelease(); + } + + void fadeoutThenRelease() { + final ValueAnimator fadeOutAnimator = ValueAnimator.ofFloat(1f, 0f); + fadeOutAnimator.setDuration(DEFAULT_FADEOUT_DURATION); + final SurfaceControl.Transaction t = mTransactionPool.acquire(); + fadeOutAnimator.addUpdateListener(animation -> { + if (mChildSurface == null || !mChildSurface.isValid()) { + fadeOutAnimator.cancel(); + return; + } + t.setAlpha(mChildSurface, (float) animation.getAnimatedValue()); + t.apply(); + }); + + fadeOutAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (mChildSurface == null || !mChildSurface.isValid()) { + fadeOutAnimator.cancel(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + mTransactionPool.release(t); + if (mChildSurface != null) { + final SurfaceControl.Transaction t = mTransactionPool.acquire(); + t.remove(mChildSurface).apply(); + mTransactionPool.release(t); + mChildSurface = null; + } + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + } + }); + fadeOutAnimator.start(); + } + + @Override + protected boolean hasImeSurface() { + return mHasImeSurface; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java new file mode 100644 index 000000000000..12a0d4054b4d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java @@ -0,0 +1,150 @@ +/* + * 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.startingsurface; + +import static android.graphics.Color.WHITE; +import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.os.Binder; +import android.os.SystemClock; +import android.view.Display; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.window.SplashScreenView; +import android.window.StartingWindowInfo; +import android.window.StartingWindowRemovalInfo; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { + + private final TransactionPool mTransactionPool; + + WindowlessSplashWindowCreator(SplashscreenContentDrawer contentDrawer, + Context context, + ShellExecutor splashScreenExecutor, + DisplayManager displayManager, + StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager, + TransactionPool pool) { + super(contentDrawer, context, splashScreenExecutor, displayManager, + startingWindowRecordManager); + mTransactionPool = pool; + } + + void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, SurfaceControl rootSurface) { + final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo; + final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null + ? windowInfo.targetActivityInfo + : taskInfo.topActivityInfo; + if (activityInfo == null || activityInfo.packageName == null) { + return; + } + + final int displayId = taskInfo.displayId; + final Display display = mDisplayManager.getDisplay(displayId); + if (display == null) { + // Can't show splash screen on requested display, so skip showing at all. + return; + } + final Context myContext = SplashscreenContentDrawer.createContext(mContext, windowInfo, + 0 /* theme */, STARTING_WINDOW_TYPE_SPLASH_SCREEN, mDisplayManager); + if (myContext == null) { + return; + } + final StartingSurfaceDrawer.WindowlessStartingWindow wlw = + new StartingSurfaceDrawer.WindowlessStartingWindow( + taskInfo.configuration, rootSurface); + final SurfaceControlViewHost viewHost = new SurfaceControlViewHost( + myContext, display, wlw, "WindowlessSplashWindowCreator"); + final String title = "Windowless Splash " + taskInfo.taskId; + final WindowManager.LayoutParams lp = SplashscreenContentDrawer.createLayoutParameters( + myContext, windowInfo, STARTING_WINDOW_TYPE_SPLASH_SCREEN, title, + PixelFormat.TRANSLUCENT, new Binder()); + final Rect windowBounds = taskInfo.configuration.windowConfiguration.getBounds(); + lp.width = windowBounds.width(); + lp.height = windowBounds.height(); + final ActivityManager.TaskDescription taskDescription; + if (taskInfo.taskDescription != null) { + taskDescription = taskInfo.taskDescription; + } else { + taskDescription = new ActivityManager.TaskDescription(); + taskDescription.setBackgroundColor(WHITE); + } + + final FrameLayout rootLayout = new FrameLayout( + mSplashscreenContentDrawer.createViewContextWrapper(mContext)); + viewHost.setView(rootLayout, lp); + + final int bgColor = taskDescription.getBackgroundColor(); + final SplashScreenView splashScreenView = mSplashscreenContentDrawer + .makeSimpleSplashScreenContentView(myContext, windowInfo, bgColor); + rootLayout.addView(splashScreenView); + final SplashWindowRecord record = new SplashWindowRecord(viewHost, splashScreenView, + wlw.mChildSurface, bgColor); + mStartingWindowRecordManager.addRecord(taskInfo.taskId, record); + windowInfo.notifyAddComplete(wlw.mChildSurface); + } + + private class SplashWindowRecord extends StartingSurfaceDrawer.StartingWindowRecord { + private SurfaceControlViewHost mViewHost; + private final long mCreateTime; + private SurfaceControl mChildSurface; + private final SplashScreenView mSplashView; + + SplashWindowRecord(SurfaceControlViewHost viewHost, SplashScreenView splashView, + SurfaceControl childSurface, int bgColor) { + mViewHost = viewHost; + mSplashView = splashView; + mChildSurface = childSurface; + mBGColor = bgColor; + mCreateTime = SystemClock.uptimeMillis(); + } + + @Override + public void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) { + if (!immediately) { + mSplashscreenContentDrawer.applyExitAnimation(mSplashView, + info.windowAnimationLeash, info.mainFrame, + this::release, mCreateTime, 0 /* roundedCornerRadius */); + } else { + release(); + } + } + + void release() { + if (mChildSurface != null) { + final SurfaceControl.Transaction t = mTransactionPool.acquire(); + t.remove(mChildSurface).apply(); + mTransactionPool.release(t); + mChildSurface = null; + } + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java index bb43d7c1a090..72fc8686f648 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java @@ -22,6 +22,7 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; +import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_WINDOWLESS; import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED; import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_DRAWN; import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT; @@ -30,6 +31,7 @@ import static android.window.StartingWindowInfo.TYPE_PARAMETER_NEW_TASK; import static android.window.StartingWindowInfo.TYPE_PARAMETER_PROCESS_RUNNING; import static android.window.StartingWindowInfo.TYPE_PARAMETER_TASK_SWITCH; import static android.window.StartingWindowInfo.TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN; +import static android.window.StartingWindowInfo.TYPE_PARAMETER_WINDOWLESS; import android.window.StartingWindowInfo; @@ -55,6 +57,7 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor final boolean legacySplashScreen = ((parameter & TYPE_PARAMETER_LEGACY_SPLASH_SCREEN) != 0); final boolean activityDrawn = (parameter & TYPE_PARAMETER_ACTIVITY_DRAWN) != 0; + final boolean windowlessSurface = (parameter & TYPE_PARAMETER_WINDOWLESS) != 0; final boolean topIsHome = windowInfo.taskInfo.topActivityType == ACTIVITY_TYPE_HOME; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, @@ -67,10 +70,15 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor + "isSolidColorSplashScreen=%b, " + "legacySplashScreen=%b, " + "activityDrawn=%b, " + + "windowless=%b, " + "topIsHome=%b", newTask, taskSwitch, processRunning, allowTaskSnapshot, activityCreated, - isSolidColorSplashScreen, legacySplashScreen, activityDrawn, topIsHome); + isSolidColorSplashScreen, legacySplashScreen, activityDrawn, windowlessSurface, + topIsHome); + if (windowlessSurface) { + return STARTING_WINDOW_TYPE_WINDOWLESS; + } if (!topIsHome) { if (!processRunning || newTask || (taskSwitch && !activityCreated)) { return getSplashscreenType(isSolidColorSplashScreen, legacySplashScreen); 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/common/TabletopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java new file mode 100644 index 000000000000..96d202ce3a85 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java @@ -0,0 +1,270 @@ +/* + * 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.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_CLOSED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_OPENED; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Surface; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link TabletopModeController}. + */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class TabletopModeControllerTest extends ShellTestCase { + // It's considered tabletop mode if the display rotation angle matches what's in this array. + // It's defined as com.android.internal.R.array.config_deviceTabletopRotations on real devices. + private static final int[] TABLETOP_MODE_ROTATIONS = new int[] { + 90 /* Surface.ROTATION_90 */, + 270 /* Surface.ROTATION_270 */ + }; + + private TestShellExecutor mMainExecutor; + + private Configuration mConfiguration; + + private TabletopModeController mPipTabletopController; + + @Mock + private Context mContext; + + @Mock + private ShellInit mShellInit; + + @Mock + private Resources mResources; + + @Mock + private DevicePostureController mDevicePostureController; + + @Mock + private DisplayController mDisplayController; + + @Mock + private TabletopModeController.OnTabletopModeChangedListener mOnTabletopModeChangedListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mResources.getIntArray(com.android.internal.R.array.config_deviceTabletopRotations)) + .thenReturn(TABLETOP_MODE_ROTATIONS); + when(mContext.getResources()).thenReturn(mResources); + mMainExecutor = new TestShellExecutor(); + mConfiguration = new Configuration(); + mPipTabletopController = new TabletopModeController(mContext, mShellInit, + mDevicePostureController, mDisplayController, mMainExecutor); + mPipTabletopController.onInit(); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), eq(mPipTabletopController)); + } + + @Test + public void registerOnTabletopModeChangedListener_notInTabletopMode_callbackFalse() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + + verify(mOnTabletopModeChangedListener, times(1)) + .onTabletopModeChanged(false); + } + + @Test + public void registerOnTabletopModeChangedListener_inTabletopMode_callbackTrue() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + + verify(mOnTabletopModeChangedListener, times(1)) + .onTabletopModeChanged(true); + } + + @Test + public void registerOnTabletopModeChangedListener_notInTabletopModeTwice_callbackOnce() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + clearInvocations(mOnTabletopModeChangedListener); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + verifyZeroInteractions(mOnTabletopModeChangedListener); + } + + // Test cases starting from folded state (DEVICE_POSTURE_CLOSED) + @Test + public void foldedRotation90_halfOpen_scheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation0_halfOpen_noScheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + // Test cases starting from unfolded state (DEVICE_POSTURE_OPENED) + @Test + public void unfoldedRotation90_halfOpen_scheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation0_halfOpen_noScheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java index 11fda8bf7bbc..bf62acfc47a1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java @@ -24,8 +24,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MAX_ANIMATION_DURATION; -import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MINIMAL_ANIMATION_DURATION; +import static com.android.wm.shell.startingsurface.SplashscreenContentDrawer.MAX_ANIMATION_DURATION; +import static com.android.wm.shell.startingsurface.SplashscreenContentDrawer.MINIMAL_ANIMATION_DURATION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -56,11 +56,9 @@ import android.os.IBinder; import android.os.Looper; import android.os.UserHandle; import android.testing.TestableContext; -import android.view.Display; import android.view.IWindowSession; import android.view.InsetsState; import android.view.Surface; -import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.WindowMetrics; @@ -106,36 +104,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { private ShellExecutor mTestExecutor; private final TestableContext mTestContext = new TestContext( InstrumentationRegistry.getInstrumentation().getTargetContext()); - TestStartingSurfaceDrawer mStartingSurfaceDrawer; - - static final class TestStartingSurfaceDrawer extends StartingSurfaceDrawer{ - int mAddWindowForTask = 0; - - TestStartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, - IconProvider iconProvider, TransactionPool pool) { - super(context, splashScreenExecutor, iconProvider, pool); - } - - @Override - protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, - WindowManager.LayoutParams params, int suggestType) { - // listen for addView - mAddWindowForTask = taskId; - saveSplashScreenRecord(appToken, taskId, view, suggestType); - // Do not wait for background color - return false; - } - - @Override - protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo, - boolean immediately) { - // listen for removeView - if (mAddWindowForTask == removalInfo.taskId) { - mAddWindowForTask = 0; - } - mStartingWindowRecords.remove(removalInfo.taskId); - } - } + StartingSurfaceDrawer mStartingSurfaceDrawer; private static class TestContext extends TestableContext { TestContext(Context context) { @@ -165,44 +134,51 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { doReturn(metrics).when(mMockWindowManager).getMaximumWindowMetrics(); doNothing().when(mMockWindowManager).addView(any(), any()); mTestExecutor = new HandlerExecutor(mTestHandler); + mStartingSurfaceDrawer = new StartingSurfaceDrawer(mTestContext, mTestExecutor, + mIconProvider, mTransactionPool); mStartingSurfaceDrawer = spy( - new TestStartingSurfaceDrawer(mTestContext, mTestExecutor, mIconProvider, + new StartingSurfaceDrawer(mTestContext, mTestExecutor, mIconProvider, mTransactionPool)); + spyOn(mStartingSurfaceDrawer.mSplashscreenWindowCreator); + spyOn(mStartingSurfaceDrawer.mWindowRecords); + spyOn(mStartingSurfaceDrawer.mWindowlessRecords); } @Test public void testAddSplashScreenSurface() { final int taskId = 1; final StartingWindowInfo windowInfo = - createWindowInfo(taskId, android.R.style.Theme); - mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, + createWindowInfo(taskId, android.R.style.Theme, mBinder); + mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, STARTING_WINDOW_TYPE_SPLASH_SCREEN); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(), + verify(mStartingSurfaceDrawer.mSplashscreenWindowCreator).addWindow( + eq(taskId), eq(mBinder), any(), any(), any(), eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN)); - assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo(); removalInfo.taskId = windowInfo.taskInfo.taskId; mStartingSurfaceDrawer.removeStartingWindow(removalInfo); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).removeWindowSynced(any(), eq(false)); - assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, 0); + verify(mStartingSurfaceDrawer.mWindowRecords).removeWindow(any(), eq(false)); + assertEquals(mStartingSurfaceDrawer.mWindowRecords.recordSize(), 0); } @Test public void testFallbackDefaultTheme() { final int taskId = 1; final StartingWindowInfo windowInfo = - createWindowInfo(taskId, 0); + createWindowInfo(taskId, 0, mBinder); final int[] theme = new int[1]; doAnswer(invocation -> theme[0] = (Integer) invocation.callRealMethod()) - .when(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any()); + .when(mStartingSurfaceDrawer.mSplashscreenWindowCreator) + .getSplashScreenTheme(eq(0), any()); - mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, + mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, STARTING_WINDOW_TYPE_SPLASH_SCREEN); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any()); + verify(mStartingSurfaceDrawer.mSplashscreenWindowCreator) + .getSplashScreenTheme(eq(0), any()); assertNotEquals(theme[0], 0); } @@ -241,7 +217,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { public void testRemoveTaskSnapshotWithImeSurfaceWhenOnImeDrawn() throws Exception { final int taskId = 1; final StartingWindowInfo windowInfo = - createWindowInfo(taskId, android.R.style.Theme); + createWindowInfo(taskId, android.R.style.Theme, mBinder); TaskSnapshot snapshot = createTaskSnapshot(100, 100, new Point(100, 100), new Rect(0, 0, 0, 50), true /* hasImeSurface */); final IWindowSession session = WindowManagerGlobal.getWindowSession(); @@ -270,7 +246,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { when(TaskSnapshotWindow.create(eq(windowInfo), eq(mBinder), eq(snapshot), any(), any())).thenReturn(mockSnapshotWindow); // Simulate a task snapshot window created with IME snapshot shown. - mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, mBinder, snapshot); + mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, snapshot); waitHandlerIdle(mTestHandler); // Verify the task snapshot with IME snapshot will be removed when received the real IME @@ -278,27 +254,36 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { // makeTaskSnapshotWindow shall call removeWindowSynced before there add a new // StartingWindowRecord for the task. mStartingSurfaceDrawer.onImeDrawnOnTask(1); - verify(mStartingSurfaceDrawer, times(2)) - .removeWindowSynced(any(), eq(true)); + verify(mStartingSurfaceDrawer.mWindowRecords, times(2)) + .removeWindow(any(), eq(true)); } } @Test public void testClearAllWindows() { final int taskId = 1; - final StartingWindowInfo windowInfo = - createWindowInfo(taskId, android.R.style.Theme); - mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, - STARTING_WINDOW_TYPE_SPLASH_SCREEN); - waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(), - eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN)); - assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); + mStartingSurfaceDrawer.mWindowRecords.addRecord(taskId, + new StartingSurfaceDrawer.StartingWindowRecord() { + @Override + public void removeIfPossible(StartingWindowRemovalInfo info, + boolean immediately) { + + } + }); + mStartingSurfaceDrawer.mWindowlessRecords.addRecord(taskId, + new StartingSurfaceDrawer.StartingWindowRecord() { + @Override + public void removeIfPossible(StartingWindowRemovalInfo info, + boolean immediately) { + } + }); mStartingSurfaceDrawer.clearAllWindows(); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).removeWindowSynced(any(), eq(true)); - assertEquals(mStartingSurfaceDrawer.mStartingWindowRecords.size(), 0); + verify(mStartingSurfaceDrawer.mWindowRecords).removeWindow(any(), eq(true)); + assertEquals(mStartingSurfaceDrawer.mWindowRecords.recordSize(), 0); + verify(mStartingSurfaceDrawer.mWindowlessRecords).removeWindow(any(), eq(true)); + assertEquals(mStartingSurfaceDrawer.mWindowlessRecords.recordSize(), 0); } @Test @@ -351,7 +336,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { longAppDuration, longAppDuration)); } - private StartingWindowInfo createWindowInfo(int taskId, int themeResId) { + private StartingWindowInfo createWindowInfo(int taskId, int themeResId, IBinder appToken) { StartingWindowInfo windowInfo = new StartingWindowInfo(); final ActivityInfo info = new ActivityInfo(); info.applicationInfo = new ApplicationInfo(); @@ -360,6 +345,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.topActivityInfo = info; taskInfo.taskId = taskId; + windowInfo.appToken = appToken; windowInfo.targetActivityInfo = info; windowInfo.taskInfo = taskInfo; windowInfo.topOpaqueWindowInsetsState = new InsetsState(); 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..3b129720c727 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: [ @@ -535,6 +537,7 @@ cc_defaults { "AnimatorManager.cpp", "CanvasTransform.cpp", "DamageAccumulator.cpp", + "Gainmap.cpp", "Interpolator.cpp", "LightingInfo.cpp", "Matrix.cpp", diff --git a/libs/hwui/Gainmap.cpp b/libs/hwui/Gainmap.cpp new file mode 100644 index 000000000000..30f401ef5f01 --- /dev/null +++ b/libs/hwui/Gainmap.cpp @@ -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. + */ +#include "Gainmap.h" + +namespace android::uirenderer { + +sp<Gainmap> Gainmap::allocateHardwareGainmap(const sp<Gainmap>& srcGainmap) { + auto gainmap = sp<Gainmap>::make(); + gainmap->info = srcGainmap->info; + const SkBitmap skSrcBitmap = srcGainmap->bitmap->getSkBitmap(); + sk_sp<Bitmap> skBitmap(Bitmap::allocateHardwareBitmap(skSrcBitmap)); + if (!skBitmap.get()) { + return nullptr; + } + gainmap->bitmap = std::move(skBitmap); + return gainmap; +} + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/Gainmap.h b/libs/hwui/Gainmap.h index 765f98a257b8..3bc183ab854f 100644 --- a/libs/hwui/Gainmap.h +++ b/libs/hwui/Gainmap.h @@ -27,6 +27,7 @@ class Gainmap : public LightRefBase<Gainmap> { public: SkGainmapInfo info; sk_sp<Bitmap> bitmap; + static sp<Gainmap> allocateHardwareGainmap(const sp<Gainmap>& srcGainmap); }; } // namespace android::uirenderer 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/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 3f9c4bd2aeff..6ee7576651f2 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -386,15 +386,10 @@ static jobject Bitmap_copy(JNIEnv* env, jobject, jlong srcHandle, jint dstConfig return NULL; } if (hasGainmap) { - auto gainmap = sp<uirenderer::Gainmap>::make(); - gainmap->info = original.gainmap()->info; - const SkBitmap skSrcBitmap = original.gainmap()->bitmap->getSkBitmap(); - sk_sp<Bitmap> skBitmap(Bitmap::allocateHardwareBitmap(skSrcBitmap)); - if (!skBitmap.get()) { - return NULL; + auto gm = uirenderer::Gainmap::allocateHardwareGainmap(original.gainmap()); + if (gm) { + bitmap->setGainmap(std::move(gm)); } - gainmap->bitmap = std::move(skBitmap); - bitmap->setGainmap(std::move(gainmap)); } return createBitmap(env, bitmap.release(), getPremulBitmapCreateFlags(isMutable)); } diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp index 571ab833f053..c57e6f09347a 100644 --- a/libs/hwui/jni/BitmapFactory.cpp +++ b/libs/hwui/jni/BitmapFactory.cpp @@ -637,7 +637,10 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, return nullObjectReturn("Failed to allocate a hardware bitmap"); } if (hasGainmap) { - hardwareBitmap->setGainmap(std::move(gainmap)); + auto gm = uirenderer::Gainmap::allocateHardwareGainmap(gainmap); + if (gm) { + hardwareBitmap->setGainmap(std::move(gm)); + } } return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags, diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp index f93be038cc36..aeaa17198be5 100644 --- a/libs/hwui/jni/BitmapRegionDecoder.cpp +++ b/libs/hwui/jni/BitmapRegionDecoder.cpp @@ -334,7 +334,10 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in if (isHardware) { sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(bitmap); if (hasGainmap) { - hardwareBitmap->setGainmap(std::move(gainmap)); + auto gm = uirenderer::Gainmap::allocateHardwareGainmap(gainmap); + if (gm) { + hardwareBitmap->setGainmap(std::move(gm)); + } } return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags); } 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/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp index fda7080f2864..ad80460da5a9 100644 --- a/libs/hwui/jni/ImageDecoder.cpp +++ b/libs/hwui/jni/ImageDecoder.cpp @@ -469,8 +469,10 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong if (hwBitmap) { hwBitmap->setImmutable(); if (nativeBitmap->hasGainmap()) { - // TODO: Also convert to a HW gainmap image - hwBitmap->setGainmap(nativeBitmap->gainmap()); + auto gm = uirenderer::Gainmap::allocateHardwareGainmap(nativeBitmap->gainmap()); + if (gm) { + hwBitmap->setGainmap(std::move(gm)); + } } return bitmap::createBitmap(env, hwBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets); 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/location/java/android/location/GnssCapabilities.java b/location/java/android/location/GnssCapabilities.java index c6f32c2cd387..88f00dc38cb3 100644 --- a/location/java/android/location/GnssCapabilities.java +++ b/location/java/android/location/GnssCapabilities.java @@ -123,6 +123,21 @@ public final class GnssCapabilities implements Parcelable { @Retention(RetentionPolicy.SOURCE) public @interface SubHalPowerCapabilityFlags {} + /** The capability is unknown to be supported or not. */ + public static final int CAPABILITY_UNKNOWN = 0; + /** The capability is supported. */ + public static final int CAPABILITY_SUPPORTED = 1; + /** The capability is not supported. */ + public static final int CAPABILITY_UNSUPPORTED = 2; + + /** @hide */ + @IntDef(flag = true, prefix = {"CAPABILITY_"}, value = {CAPABILITY_UNKNOWN, + CAPABILITY_SUPPORTED, + CAPABILITY_UNSUPPORTED}) + @Retention(RetentionPolicy.SOURCE) + public @interface CapabilitySupportType {} + + /** * Returns an empty GnssCapabilities object. * @@ -375,30 +390,25 @@ public final class GnssCapabilities implements Parcelable { } /** - * Returns {@code true} if GNSS chipset supports accumulated delta range, {@code false} - * otherwise. - * - * <p>The value is only known if {@link #isAccumulatedDeltaRangeCapabilityKnown()} is - * true. + * Returns {@link #CAPABILITY_SUPPORTED} if GNSS chipset supports accumulated delta + * range, {@link #CAPABILITY_UNSUPPORTED} if GNSS chipset does not support accumulated + * delta range, and {@link #CAPABILITY_UNKNOWN} if it is unknown, which means GNSS + * chipset may or may not support accumulated delta range. * * <p>The accumulated delta range information can be queried in * {@link android.location.GnssMeasurement#getAccumulatedDeltaRangeState()}, * {@link android.location.GnssMeasurement#getAccumulatedDeltaRangeMeters()}, and * {@link android.location.GnssMeasurement#getAccumulatedDeltaRangeUncertaintyMeters()}. */ - public boolean hasAccumulatedDeltaRange() { + public @CapabilitySupportType int hasAccumulatedDeltaRange() { if (!mIsAdrCapabilityKnown) { - throw new IllegalStateException("Accumulated delta range capability is unknown."); + return CAPABILITY_UNKNOWN; + } + if ((mTopFlags & TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE) != 0) { + return CAPABILITY_SUPPORTED; + } else { + return CAPABILITY_UNSUPPORTED; } - return (mTopFlags & TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE) != 0; - } - - /** - * Returns {@code true} if {@link #hasAccumulatedDeltaRange()} is known, {@code false} - * otherwise. - */ - public boolean isAccumulatedDeltaRangeCapabilityKnown() { - return mIsAdrCapabilityKnown; } /** @@ -597,9 +607,9 @@ public final class GnssCapabilities implements Parcelable { if (hasMeasurementCorrectionsForDriving()) { builder.append("MEASUREMENT_CORRECTIONS_FOR_DRIVING "); } - if (mIsAdrCapabilityKnown && hasAccumulatedDeltaRange()) { + if (hasAccumulatedDeltaRange() == CAPABILITY_SUPPORTED) { builder.append("ACCUMULATED_DELTA_RANGE "); - } else if (!mIsAdrCapabilityKnown) { + } else if (hasAccumulatedDeltaRange() == CAPABILITY_UNKNOWN) { builder.append("ACCUMULATED_DELTA_RANGE(unknown) "); } if (hasMeasurementCorrectionsLosSats()) { @@ -795,19 +805,17 @@ public final class GnssCapabilities implements Parcelable { /** * Sets accumulated delta range capability. */ - public @NonNull Builder setHasAccumulatedDeltaRange(boolean capable) { - mIsAdrCapabilityKnown = true; - mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE, - capable); - return this; - } - - /** - * Clears accumulated delta range capability and sets it as unknown. - */ - public @NonNull Builder clearIsAccumulatedDeltaRangeCapabilityKnown() { - mIsAdrCapabilityKnown = false; - mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE, false); + public @NonNull Builder setHasAccumulatedDeltaRange(@CapabilitySupportType int capable) { + if (capable == CAPABILITY_UNKNOWN) { + mIsAdrCapabilityKnown = false; + mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE, false); + } else if (capable == CAPABILITY_SUPPORTED) { + mIsAdrCapabilityKnown = true; + mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE, true); + } else if (capable == CAPABILITY_UNSUPPORTED) { + mIsAdrCapabilityKnown = true; + mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE, false); + } return this; } 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..16827da39b14 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,71 +87,89 @@ 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) }, + ) } } ProviderActivityState.READY_TO_LAUNCH -> { @@ -145,9 +178,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 +197,7 @@ fun CreateCredentialScreen( fun PasskeyIntroCard( onConfirm: () -> Unit, onLearnMore: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { SheetContainerCard { item { @@ -223,6 +262,7 @@ fun PasskeyIntroCard( ) } } + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_PASSKEY_INTRO) } @Composable @@ -234,6 +274,7 @@ fun ProviderSelectionCard( onOptionSelected: (ActiveEntry) -> Unit, onDisabledProvidersSelected: () -> Unit, onMoreOptionsSelected: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { SheetContainerCard { item { HeadlineIcon(bitmap = requestDisplayInfo.typeIcon.toBitmap().asImageBitmap()) } @@ -297,21 +338,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 +374,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 +415,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 +463,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 +562,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 +599,13 @@ fun ExternalOnlySelectionCard( ) } } + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_EXTERNAL_ONLY_SELECTION) } @Composable fun MoreAboutPasskeysIntroCard( onBackPasskeyIntroButtonSelected: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { SheetContainerCard( topAppBar = { @@ -582,6 +641,7 @@ fun MoreAboutPasskeysIntroCard( BodyMediumText(text = stringResource(R.string.seamless_transition_detail)) } } + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO) } @Composable @@ -620,6 +680,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..96798a33a16b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -62,6 +62,8 @@ import com.android.credentialmanager.common.ui.CredentialListSectionHeader import com.android.credentialmanager.common.ui.Snackbar import com.android.credentialmanager.common.ui.setTransparentSystemBarsColor import com.android.credentialmanager.common.ui.setBottomSheetSystemBarsColor +import com.android.credentialmanager.logging.GetCredentialEvent +import com.android.internal.logging.UiEventLogger.UiEventEnum @Composable fun GetCredentialScreen( @@ -75,7 +77,9 @@ fun GetCredentialScreen( RemoteCredentialSnackBarScreen( onClick = viewModel::getFlowOnMoreOptionOnSnackBarSelected, onCancel = viewModel::onUserCancel, + onLog = { viewModel.logUiEvent(it) }, ) + viewModel.uiMetrics.log(GetCredentialEvent.CREDMAN_GET_CRED_SCREEN_REMOTE_ONLY) } else if (getCredentialUiState.currentScreenState == GetScreenState.UNLOCKED_AUTH_ENTRIES_ONLY) { setTransparentSystemBarsColor(sysUiController) @@ -84,7 +88,10 @@ fun GetCredentialScreen( getCredentialUiState.providerDisplayInfo.authenticationEntryList, onCancel = viewModel::silentlyFinishActivity, onLastLockedAuthEntryNotFound = viewModel::onLastLockedAuthEntryNotFoundError, + onLog = { viewModel.logUiEvent(it) }, ) + viewModel.uiMetrics.log(GetCredentialEvent + .CREDMAN_GET_CRED_SCREEN_UNLOCKED_AUTH_ENTRIES_ONLY) } else { setBottomSheetSystemBarsColor(sysUiController) ModalBottomSheet( @@ -104,7 +111,10 @@ fun GetCredentialScreen( onEntrySelected = viewModel::getFlowOnEntrySelected, onConfirm = viewModel::getFlowOnConfirmEntrySelected, onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected, + onLog = { viewModel.logUiEvent(it) }, ) + viewModel.uiMetrics.log(GetCredentialEvent + .CREDMAN_GET_CRED_SCREEN_PRIMARY_SELECTION) } else { AllSignInOptionCard( providerInfoList = getCredentialUiState.providerInfoList, @@ -114,7 +124,10 @@ fun GetCredentialScreen( viewModel::getFlowOnBackToPrimarySelectionScreen, onCancel = viewModel::onUserCancel, isNoAccount = getCredentialUiState.isNoAccount, + onLog = { viewModel.logUiEvent(it) }, ) + viewModel.uiMetrics.log(GetCredentialEvent + .CREDMAN_GET_CRED_SCREEN_ALL_SIGN_IN_OPTIONS) } } ProviderActivityState.READY_TO_LAUNCH -> { @@ -123,9 +136,13 @@ fun GetCredentialScreen( LaunchedEffect(viewModel.uiState.providerActivityState) { viewModel.launchProviderUi(providerActivityLauncher) } + viewModel.uiMetrics.log(GetCredentialEvent + .CREDMAN_GET_CRED_PROVIDER_ACTIVITY_READY_TO_LAUNCH) } ProviderActivityState.PENDING -> { // Hide our content when the provider activity is active. + viewModel.uiMetrics.log(GetCredentialEvent + .CREDMAN_GET_CRED_PROVIDER_ACTIVITY_PENDING) } } }, @@ -144,6 +161,7 @@ fun PrimarySelectionCard( onEntrySelected: (BaseEntry) -> Unit, onConfirm: () -> Unit, onMoreOptionSelected: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList @@ -182,12 +200,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 +215,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 +230,7 @@ fun PrimarySelectionCard( CredentialEntryRow( credentialEntryInfo = it.sortedCredentialEntryList.first(), onEntrySelected = onEntrySelected, + enforceOneLine = true, ) } } @@ -243,6 +266,7 @@ fun PrimarySelectionCard( ) } } + onLog(GetCredentialEvent.CREDMAN_GET_CRED_PRIMARY_SELECTION_CARD) } /** Draws the secondary credential selection page, where all sign-in options are listed. */ @@ -254,6 +278,7 @@ fun AllSignInOptionCard( onBackButtonClicked: () -> Unit, onCancel: () -> Unit, isNoAccount: Boolean, + onLog: @Composable (UiEventEnum) -> Unit, ) { val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList @@ -290,13 +315,6 @@ fun AllSignInOptionCard( ) } } - item { - Divider( - thickness = 1.dp, - color = Color.LightGray, - modifier = Modifier.padding(top = 16.dp) - ) - } // Manage sign-ins (action chips) item { ActionChips( @@ -305,6 +323,7 @@ fun AllSignInOptionCard( ) } } + onLog(GetCredentialEvent.CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD) } // TODO: create separate rows for primary and secondary pages. @@ -402,10 +421,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 +436,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 +454,7 @@ fun CredentialEntryRow( fun AuthenticationEntryRow( authenticationEntryInfo: AuthenticationEntryInfo, onEntrySelected: (BaseEntry) -> Unit, + enforceOneLine: Boolean = false, ) { Entry( onClick = { onEntrySelected(authenticationEntryInfo) }, @@ -442,6 +466,7 @@ fun AuthenticationEntryRow( else R.string.locked_credential_entry_label_subtext_tap_to_unlock ), isLockedAuthEntry = !authenticationEntryInfo.isUnlockedAndEmpty, + enforceOneLine = enforceOneLine, ) } @@ -462,6 +487,7 @@ fun ActionEntryRow( fun RemoteCredentialSnackBarScreen( onClick: (Boolean) -> Unit, onCancel: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { Snackbar( action = { @@ -478,6 +504,7 @@ fun RemoteCredentialSnackBarScreen( onDismiss = onCancel, contentText = stringResource(R.string.get_dialog_use_saved_passkey_for), ) + onLog(GetCredentialEvent.CREDMAN_GET_CRED_REMOTE_CRED_SNACKBAR_SCREEN) } @Composable @@ -485,6 +512,7 @@ fun EmptyAuthEntrySnackBarScreen( authenticationEntryList: List<AuthenticationEntryInfo>, onCancel: () -> Unit, onLastLockedAuthEntryNotFound: () -> Unit, + onLog: @Composable (UiEventEnum) -> Unit, ) { val lastLocked = authenticationEntryList.firstOrNull({ it.isLastUnlocked }) if (lastLocked == null) { @@ -496,4 +524,5 @@ fun EmptyAuthEntrySnackBarScreen( onDismiss = onCancel, contentText = stringResource(R.string.no_sign_in_info_in, lastLocked.providerDisplayName), ) + onLog(GetCredentialEvent.CREDMAN_GET_CRED_SCREEN_EMPTY_AUTH_SNACKBAR_SCREEN) }
\ No newline at end of file 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/GetCredentialEvent.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt new file mode 100644 index 000000000000..8de8895e8ffc --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt @@ -0,0 +1,62 @@ +/* + * 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 GetCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum { + + @UiEvent(doc = "The The snackbar only page when there's no account but only a remoteEntry " + + "visible on the screen.") + CREDMAN_GET_CRED_SCREEN_REMOTE_ONLY(1332), + + @UiEvent(doc = "The snackbar when there are only auth entries and all of them are empty.") + CREDMAN_GET_CRED_SCREEN_UNLOCKED_AUTH_ENTRIES_ONLY(1333), + + @UiEvent(doc = "The primary credential selection page is displayed on screen.") + CREDMAN_GET_CRED_SCREEN_PRIMARY_SELECTION(1334), + + @UiEvent(doc = "The secondary credential selection page, where all sign-in options are " + + "listed is displayed on the screen.") + CREDMAN_GET_CRED_SCREEN_ALL_SIGN_IN_OPTIONS(1335), + + @UiEvent(doc = "The provider activity is not active nor is any ready for launch on the screen.") + CREDMAN_GET_CRED_PROVIDER_ACTIVITY_NOT_APPLICABLE(1336), + + @UiEvent(doc = "The provider activity is ready to be launched on the screen.") + CREDMAN_GET_CRED_PROVIDER_ACTIVITY_READY_TO_LAUNCH(1337), + + @UiEvent(doc = "The provider activity is launched and we are waiting for its result. " + + "Contents Hidden.") + CREDMAN_GET_CRED_PROVIDER_ACTIVITY_PENDING(1338), + + @UiEvent(doc = "The remote credential snackbar screen is visible.") + CREDMAN_GET_CRED_REMOTE_CRED_SNACKBAR_SCREEN(1339), + + @UiEvent(doc = "The empty auth snackbar screen is visible.") + CREDMAN_GET_CRED_SCREEN_EMPTY_AUTH_SNACKBAR_SCREEN(1340), + + @UiEvent(doc = "The primary selection card is visible on screen.") + CREDMAN_GET_CRED_PRIMARY_SELECTION_CARD(1341), + + @UiEvent(doc = "The all sign in option card is visible on screen.") + CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD(1342); + + 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/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 63bb2fe2aa76..a3d632cfb82a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -210,7 +210,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> synchronized (mProfileLock) { if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile - || profile instanceof HearingAidProfile) { + || profile instanceof HearingAidProfile || profile instanceof LeAudioProfile) { setProfileConnectedStatus(profile.getProfileId(), false); switch (newProfileState) { case BluetoothProfile.STATE_CONNECTED: @@ -228,7 +228,20 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> case BluetoothProfile.STATE_DISCONNECTED: if (mHandler.hasMessages(profile.getProfileId())) { mHandler.removeMessages(profile.getProfileId()); - setProfileConnectedStatus(profile.getProfileId(), true); + if (profile.getConnectionPolicy(mDevice) > + BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { + /* + * If we received state DISCONNECTED and previous state was + * CONNECTING and connection policy is FORBIDDEN or UNKNOWN + * then it's not really a failure to connect. + * + * Connection profile is considered as failed when connection + * policy indicates that profile should be connected + * but it got disconnected. + */ + Log.w(TAG, "onProfileStateChanged(): Failed to connect profile"); + setProfileConnectedStatus(profile.getProfileId(), true); + } } break; default: @@ -1205,6 +1218,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } private boolean isProfileConnectedFail() { + Log.d(TAG, "anonymizedAddress=" + mDevice.getAnonymizedAddress() + + " mIsA2dpProfileConnectedFail=" + mIsA2dpProfileConnectedFail + + " mIsHearingAidProfileConnectedFail=" + mIsHearingAidProfileConnectedFail + + " mIsLeAudioProfileConnectedFail=" + mIsLeAudioProfileConnectedFail + + " mIsHeadsetProfileConnectedFail=" + mIsHeadsetProfileConnectedFail + + " isConnectedSapDevice()=" + isConnectedSapDevice()); + return mIsA2dpProfileConnectedFail || mIsHearingAidProfileConnectedFail || (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail) || mIsLeAudioProfileConnectedFail; 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/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index 77c19a1d903e..1c179f838586 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -97,15 +97,76 @@ public class CachedBluetoothDeviceTest { mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); when(mDevice.getAddress()).thenReturn(DEVICE_ADDRESS); when(mHfpProfile.isProfileReady()).thenReturn(true); + when(mHfpProfile.getProfileId()).thenReturn(BluetoothProfile.HEADSET); when(mA2dpProfile.isProfileReady()).thenReturn(true); + when(mA2dpProfile.getProfileId()).thenReturn(BluetoothProfile.A2DP); when(mPanProfile.isProfileReady()).thenReturn(true); + when(mPanProfile.getProfileId()).thenReturn(BluetoothProfile.PAN); when(mHearingAidProfile.isProfileReady()).thenReturn(true); + when(mHearingAidProfile.getProfileId()).thenReturn(BluetoothProfile.HEARING_AID); + when(mLeAudioProfile.isProfileReady()).thenReturn(true); + when(mLeAudioProfile.getProfileId()).thenReturn(BluetoothProfile.LE_AUDIO); mCachedDevice = spy(new CachedBluetoothDevice(mContext, mProfileManager, mDevice)); mSubCachedDevice = spy(new CachedBluetoothDevice(mContext, mProfileManager, mSubDevice)); doAnswer((invocation) -> mBatteryLevel).when(mCachedDevice).getBatteryLevel(); doAnswer((invocation) -> mBatteryLevel).when(mSubCachedDevice).getBatteryLevel(); } + private void testTransitionFromConnectingToDisconnected( + LocalBluetoothProfile connectingProfile, LocalBluetoothProfile connectedProfile, + int connectionPolicy, String expectedSummary) { + // Arrange: + // At least one profile has to be connected + updateProfileStatus(connectedProfile, BluetoothProfile.STATE_CONNECTED); + // Set profile under test to CONNECTING + updateProfileStatus(connectingProfile, BluetoothProfile.STATE_CONNECTING); + // Set connection policy + when(connectingProfile.getConnectionPolicy(mDevice)).thenReturn(connectionPolicy); + + // Act & Assert: + // Get the expected connection summary. + updateProfileStatus(connectingProfile, BluetoothProfile.STATE_DISCONNECTED); + assertThat(mCachedDevice.getConnectionSummary()).isEqualTo(expectedSummary); + } + + @Test + public void onProfileStateChanged_testConnectingToDisconnected_policyAllowed_problem() { + String connectTimeoutString = mContext.getString(R.string.profile_connect_timeout_subtext); + + testTransitionFromConnectingToDisconnected(mA2dpProfile, mLeAudioProfile, + BluetoothProfile.CONNECTION_POLICY_ALLOWED, connectTimeoutString); + testTransitionFromConnectingToDisconnected(mHearingAidProfile, mLeAudioProfile, + BluetoothProfile.CONNECTION_POLICY_ALLOWED, connectTimeoutString); + testTransitionFromConnectingToDisconnected(mHfpProfile, mLeAudioProfile, + BluetoothProfile.CONNECTION_POLICY_ALLOWED, connectTimeoutString); + testTransitionFromConnectingToDisconnected(mLeAudioProfile, mA2dpProfile, + BluetoothProfile.CONNECTION_POLICY_ALLOWED, connectTimeoutString); + } + + @Test + public void onProfileStateChanged_testConnectingToDisconnected_policyForbidden_noProblem() { + testTransitionFromConnectingToDisconnected(mA2dpProfile, mLeAudioProfile, + BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, null); + testTransitionFromConnectingToDisconnected(mHearingAidProfile, mLeAudioProfile, + BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, null); + testTransitionFromConnectingToDisconnected(mHfpProfile, mLeAudioProfile, + BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, null); + testTransitionFromConnectingToDisconnected(mLeAudioProfile, mA2dpProfile, + BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, null); + } + + @Test + public void onProfileStateChanged_testConnectingToDisconnected_policyUnknown_noProblem() { + testTransitionFromConnectingToDisconnected(mA2dpProfile, mLeAudioProfile, + BluetoothProfile.CONNECTION_POLICY_UNKNOWN, null); + testTransitionFromConnectingToDisconnected(mHearingAidProfile, mLeAudioProfile, + BluetoothProfile.CONNECTION_POLICY_UNKNOWN, null); + testTransitionFromConnectingToDisconnected(mHfpProfile, mLeAudioProfile, + BluetoothProfile.CONNECTION_POLICY_UNKNOWN, null); + testTransitionFromConnectingToDisconnected(mLeAudioProfile, mA2dpProfile, + BluetoothProfile.CONNECTION_POLICY_UNKNOWN, null); + } + @Test public void getConnectionSummary_testProfilesInactive_returnPairing() { // Arrange: 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/GenerationRegistry.java b/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java index 7f3b0ff8c838..db6cc1a39ef1 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java @@ -16,6 +16,7 @@ package com.android.providers.settings; +import android.annotation.NonNull; import android.os.Bundle; import android.provider.Settings; import android.util.ArrayMap; @@ -59,6 +60,10 @@ final class GenerationRegistry { // Maximum size of an individual backing store static final int MAX_BACKING_STORE_SIZE = MemoryIntArray.getMaxSize(); + // Use an empty string to track the generation number of all non-predefined, unset settings + // The generation number is only increased when a new non-predefined setting is inserted + private static final String DEFAULT_MAP_KEY_FOR_UNSET_SETTINGS = ""; + public GenerationRegistry(Object lock) { mLock = lock; } @@ -72,6 +77,10 @@ final class GenerationRegistry { (SettingsState.getTypeFromKey(key) == SettingsState.SETTINGS_TYPE_CONFIG); // Only store the prefix if the mutated setting is a config final String indexMapKey = isConfig ? (name.split("/")[0] + "/") : name; + incrementGenerationInternal(key, indexMapKey); + } + + private void incrementGenerationInternal(int key, @NonNull String indexMapKey) { synchronized (mLock) { final MemoryIntArray backingStore = getBackingStoreLocked(key, /* createIfNotExist= */ false); @@ -87,7 +96,8 @@ final class GenerationRegistry { final int generation = backingStore.get(index) + 1; backingStore.set(index, generation); if (DEBUG) { - Slog.i(LOG_TAG, "Incremented generation for setting:" + indexMapKey + Slog.i(LOG_TAG, "Incremented generation for " + + (indexMapKey.isEmpty() ? "unset settings" : "setting:" + indexMapKey) + " key:" + SettingsState.keyToString(key) + " at index:" + index); } @@ -98,6 +108,18 @@ final class GenerationRegistry { } } + // A new, non-predefined setting has been inserted, increment the tracking number for all unset + // settings + public void incrementGenerationForUnsetSettings(int key) { + final boolean isConfig = + (SettingsState.getTypeFromKey(key) == SettingsState.SETTINGS_TYPE_CONFIG); + if (isConfig) { + // No need to track new settings for configs + return; + } + incrementGenerationInternal(key, DEFAULT_MAP_KEY_FOR_UNSET_SETTINGS); + } + /** * Return the backing store's reference, the index and the current generation number * of a cached setting. If it was not in the backing store, first create the entry in it before @@ -124,8 +146,8 @@ final class GenerationRegistry { bundle.putInt(Settings.CALL_METHOD_GENERATION_KEY, backingStore.get(index)); if (DEBUG) { - Slog.i(LOG_TAG, "Exported index:" + index - + " for setting:" + indexMapKey + Slog.i(LOG_TAG, "Exported index:" + index + " for " + + (indexMapKey.isEmpty() ? "unset settings" : "setting:" + indexMapKey) + " key:" + SettingsState.keyToString(key)); } } catch (IOException e) { @@ -135,6 +157,10 @@ final class GenerationRegistry { } } + public void addGenerationDataForUnsetSettings(Bundle bundle, int key) { + addGenerationData(bundle, key, /* indexMapKey= */ DEFAULT_MAP_KEY_FOR_UNSET_SETTINGS); + } + public void onUserRemoved(int userId) { final int secureKey = SettingsState.makeKey( SettingsState.SETTINGS_TYPE_SECURE, userId); 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/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index ba275ebca168..27c8cdfe98f3 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -2327,11 +2327,15 @@ public class SettingsProvider extends ContentProvider { result.putString(Settings.NameValueTable.VALUE, (setting != null && !setting.isNull()) ? setting.getValue() : null); - if ((setting != null && !setting.isNull()) || isSettingPreDefined(name, type)) { - // Don't track generation for non-existent settings unless the name is predefined - synchronized (mLock) { + synchronized (mLock) { + if ((setting != null && !setting.isNull()) || isSettingPreDefined(name, type)) { + // Individual generation tracking for predefined settings even if they are unset mSettingsRegistry.mGenerationRegistry.addGenerationData(result, SettingsState.makeKey(type, userId), name); + } else { + // All non-predefined, unset settings are tracked using the same generation number + mSettingsRegistry.mGenerationRegistry.addGenerationDataForUnsetSettings(result, + SettingsState.makeKey(type, userId)); } } return result; @@ -2345,7 +2349,8 @@ public class SettingsProvider extends ContentProvider { } else if (type == SETTINGS_TYPE_SYSTEM) { return sAllSystemSettings.contains(name); } else { - return false; + // Consider all config settings predefined because they are used by system apps only + return type == SETTINGS_TYPE_CONFIG; } } @@ -2354,14 +2359,13 @@ public class SettingsProvider extends ContentProvider { Bundle result = new Bundle(); result.putSerializable(Settings.NameValueTable.VALUE, keyValues); if (trackingGeneration) { - // Track generation even if the namespace is empty because this is for system apps synchronized (mLock) { + // Track generation even if namespace is empty because this is for system apps only mSettingsRegistry.mGenerationRegistry.addGenerationData(result, - mSettingsRegistry.getSettingsLocked(SETTINGS_TYPE_CONFIG, - UserHandle.USER_SYSTEM).mKey, prefix); + SettingsState.makeKey(SETTINGS_TYPE_CONFIG, UserHandle.USER_SYSTEM), + prefix); } } - return result; } @@ -3052,10 +3056,15 @@ public class SettingsProvider extends ContentProvider { final int key = makeKey(type, userId); boolean success = false; + boolean wasUnsetNonPredefinedSetting = false; SettingsState settingsState = peekSettingsStateLocked(key); if (settingsState != null) { + if (!isSettingPreDefined(name, type) && !settingsState.hasSetting(name)) { + wasUnsetNonPredefinedSetting = true; + } success = settingsState.insertSettingLocked(name, value, - tag, makeDefault, forceNonSystemPackage, packageName, overrideableByRestore); + tag, makeDefault, forceNonSystemPackage, packageName, + overrideableByRestore); } if (success && criticalSettings != null && criticalSettings.contains(name)) { @@ -3064,6 +3073,11 @@ public class SettingsProvider extends ContentProvider { if (forceNotify || success) { notifyForSettingsChange(key, name); + if (wasUnsetNonPredefinedSetting) { + // Increment the generation number for all non-predefined, unset settings, + // because a new non-predefined setting has been inserted + mGenerationRegistry.incrementGenerationForUnsetSettings(key); + } } if (success) { logSettingChanged(userId, name, type, CHANGE_TYPE_INSERT); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index c3888268a61d..4d8705f135af 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -759,6 +759,12 @@ final class SettingsState { mPackageToMemoryUsage.put(packageName, newSize); } + public boolean hasSetting(String name) { + synchronized (mLock) { + return hasSettingLocked(name); + } + } + @GuardedBy("mLock") private boolean hasSettingLocked(String name) { return mSettings.indexOfKey(name) >= 0; diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/GenerationRegistryTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/GenerationRegistryTest.java index d34fe6943153..6ec8146baee0 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/GenerationRegistryTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/GenerationRegistryTest.java @@ -151,6 +151,26 @@ public class GenerationRegistryTest { checkBundle(b, 0, 1, false); } + @Test + public void testUnsetSettings() throws IOException { + final GenerationRegistry generationRegistry = new GenerationRegistry(new Object()); + final int secureKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_SECURE, 0); + final String testSecureSetting = "test_secure_setting"; + Bundle b = new Bundle(); + generationRegistry.addGenerationData(b, secureKey, testSecureSetting); + checkBundle(b, 0, 1, false); + generationRegistry.addGenerationDataForUnsetSettings(b, secureKey); + checkBundle(b, 1, 1, false); + generationRegistry.addGenerationDataForUnsetSettings(b, secureKey); + // Test that unset settings always have the same index + checkBundle(b, 1, 1, false); + generationRegistry.incrementGenerationForUnsetSettings(secureKey); + // Test that the generation number of the unset settings have increased + generationRegistry.addGenerationDataForUnsetSettings(b, secureKey); + checkBundle(b, 1, 2, false); + } + + private void checkBundle(Bundle b, int expectedIndex, int expectedGeneration, boolean isNull) throws IOException { final MemoryIntArray array = getArray(b); 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..459a38e9318c --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 java.util.regex.Pattern +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) { + // Annotations having int bugId field + if (node.qualifiedName in DEMOTING_ANNOTATION_BUG_ID) { + 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) + } + } + // @Ignore has a String field for reason + if (node.qualifiedName == DEMOTING_ANNOTATION_IGNORE) { + val reason = node.findAttributeValue("value")!!.evaluate() as String + val bugPattern = Pattern.compile("b/\\d+") + if (!bugPattern.matcher(reason).find()) { + val location = context.getLocation(node) + val message = "Please attach a bug (e.g. b/123) to track demoted test" + context.report(ISSUE, node, location, message) + } + } + } + } + } + + companion object { + val DEMOTING_ANNOTATION_BUG_ID = + listOf( + "androidx.test.filters.FlakyTest", + "android.platform.test.annotations.FlakyTest", + "android.platform.test.rule.PlatinumRule.Platinum", + ) + + const val DEMOTING_ANNOTATION_IGNORE = "org.junit.Ignore" + + @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..63eb2632979c --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt @@ -0,0 +1,313 @@ +/* + * 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 + """ + ) + } + + @Test + fun testExcludeDevices_withBugId() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.platform.test.rule.PlatinumRule.Platinum; + + @Platinum(devices = "foo,bar", bugId = 123) + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testExcludeDevices_withoutBugId() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.platform.test.rule.PlatinumRule.Platinum; + + @Platinum(devices = "foo,bar") + 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] + @Platinum(devices = "foo,bar") + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testIgnore_withBug() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import org.junit.Ignore; + + @Ignore("Blocked by b/123.") + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testIgnore_withoutBug() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import org.junit.Ignore; + + @Ignore + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:4: Warning: Please attach a bug (e.g. b/123) to track demoted test [DemotingTestWithoutBug] + @Ignore + ~~~~~~~ + 0 errors, 1 warnings + """ + ) + + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import org.junit.Ignore; + + @Ignore("Not ready") + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:4: Warning: Please attach a bug (e.g. b/123) to track demoted test [DemotingTestWithoutBug] + @Ignore("Not ready") + ~~~~~~~~~~~~~~~~~~~~ + 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 annotationsPlatinumStub: TestFile = + java( + """ + package android.platform.test.rule; + + public class PlatinumRule { + public @interface Platinum { + String devices(); + int bugId() default -1; + } + } + """ + ) + private val annotationsIgnoreStub: TestFile = + java( + """ + package org.junit; + + public @interface Ignore { + String value() default ""; + } + """ + ) + private val stubs = + arrayOf( + filtersFlakyTestStub, + annotationsFlakyTestStub, + annotationsPlatinumStub, + annotationsIgnoreStub + ) +} diff --git a/packages/SystemUI/res-keyguard/values-land/dimens.xml b/packages/SystemUI/res-keyguard/values-land/dimens.xml index f1aa54412b3b..a4e7a5f12db4 100644 --- a/packages/SystemUI/res-keyguard/values-land/dimens.xml +++ b/packages/SystemUI/res-keyguard/values-land/dimens.xml @@ -27,6 +27,4 @@ <integer name="scaled_password_text_size">26</integer> <dimen name="bouncer_user_switcher_y_trans">@dimen/status_bar_height</dimen> - <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> - <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> </resources> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index b6a78f56ec5f..1f44f0532ead 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> @@ -127,9 +127,7 @@ <dimen name="bouncer_user_switcher_item_padding_vertical">10dp</dimen> <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen> <dimen name="bouncer_user_switcher_header_padding_end">44dp</dimen> - <dimen name="bouncer_user_switcher_y_trans">0dp</dimen> - <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> - <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> + <dimen name="bouncer_user_switcher_y_trans">80dp</dimen> <!-- 2 * the margin + size should equal the plus_margin --> <dimen name="user_switcher_icon_large_margin">16dp</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-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 4f38e6058723..908aac4a7b7f 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -66,4 +66,8 @@ <dimen name="controls_header_horizontal_padding">12dp</dimen> <dimen name="controls_content_margin_horizontal">16dp</dimen> + + <!-- Bouncer user switcher margins --> + <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml index b98165fb08f0..ca62d286f4ee 100644 --- a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml @@ -21,6 +21,6 @@ <!-- Space between status view and notification shelf --> <dimen name="keyguard_status_view_bottom_margin">70dp</dimen> <dimen name="keyguard_clock_top_margin">80dp</dimen> - <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">186dp</dimen> - <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">110dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">155dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">85dp</dimen> </resources> 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..095e09045daa 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> @@ -1749,4 +1746,9 @@ it is long-pressed. --> <dimen name="keyguard_long_press_settings_popup_vertical_offset">96dp</dimen> + + + <!-- Bouncer user switcher margins --> + <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 4d989a67bffd..ccdd71cbacd2 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]--> @@ -2427,7 +2431,7 @@ <!-- Shows in a dialog presented to the user to authorize this app to display a Device controls panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=NONE] --> - <string name="controls_panel_authorization">When you add <xliff:g id="appName" example="My app">%s</xliff:g>, it can add controls and content to this panel. In some apps, you can choose which controls show up here.</string> + <string name="controls_panel_authorization"><xliff:g id="appName" example="My app">%s</xliff:g>can choose which controls and content show here.</string> <!-- Shows in a dialog presented to the user to authorize this app removal from a Device controls panel [CHAR LIMIT=NONE] --> @@ -2487,7 +2491,7 @@ <!-- Title of the dialog to control certain devices from lock screen without auth [CHAR LIMIT=NONE] --> <string name="controls_settings_trivial_controls_dialog_title">Control devices from lock screen?</string> <!-- Message of the dialog to control certain devices from lock screen without auth [CHAR LIMIT=NONE] --> - <string name="controls_settings_trivial_controls_dialog_message">You can control some devices without unlocking your phone or tablet.\n\nYour device app determines which devices can be controlled in this way.</string> + <string name="controls_settings_trivial_controls_dialog_message">You can control some devices without unlocking your phone or tablet. Your device app determines which devices can be controlled in this way.</string> <!-- Neutral button title of the controls dialog [CHAR LIMIT=NONE] --> <string name="controls_settings_dialog_neutral_button">No thanks</string> <!-- Positive button title of the controls dialog [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt index 9a581aaa9b2c..482158e80d0f 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt @@ -91,6 +91,22 @@ constructor( val sampledRegion = calculateSampledRegion(sampledView) val regions = ArrayList<RectF>() val sampledRegionWithOffset = convertBounds(sampledRegion) + + if ( + sampledRegionWithOffset.left < 0.0 || + sampledRegionWithOffset.right > 1.0 || + sampledRegionWithOffset.top < 0.0 || + sampledRegionWithOffset.bottom > 1.0 + ) { + android.util.Log.e( + "RegionSampler", + "view out of bounds: $sampledRegion | " + + "screen width: ${displaySize.x}, screen height: ${displaySize.y}", + Exception() + ) + return + } + regions.add(sampledRegionWithOffset) wallpaperManager?.removeOnColorsChangedListener(this) 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-debug/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt index 8323d0971ad7..f005bab55de6 100644 --- a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt +++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt @@ -23,6 +23,8 @@ import com.android.systemui.util.settings.SettingsUtilModule import dagger.Binds import dagger.Module import dagger.Provides +import dagger.multibindings.IntoSet +import javax.inject.Named @Module(includes = [ FeatureFlagsDebugStartableModule::class, @@ -35,7 +37,8 @@ abstract class FlagsModule { abstract fun bindsFeatureFlagDebug(impl: FeatureFlagsDebug): FeatureFlags @Binds - abstract fun bindsRestarter(debugRestarter: FeatureFlagsDebugRestarter): Restarter + @IntoSet + abstract fun bindsScreenIdleCondition(impl: ScreenIdleCondition): ConditionalRestarter.Condition @Module companion object { @@ -44,5 +47,10 @@ abstract class FlagsModule { fun provideFlagManager(context: Context, @Main handler: Handler): FlagManager { return FlagManager(context, handler) } + + @JvmStatic + @Provides + @Named(ConditionalRestarter.RESTART_DELAY) + fun provideRestartDelaySec(): Long = 1 } } diff --git a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt index 87beff76290d..927d4604b823 100644 --- a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt +++ b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt @@ -18,6 +18,9 @@ package com.android.systemui.flags import dagger.Binds import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import javax.inject.Named @Module(includes = [ FeatureFlagsReleaseStartableModule::class, @@ -29,5 +32,18 @@ abstract class FlagsModule { abstract fun bindsFeatureFlagRelease(impl: FeatureFlagsRelease): FeatureFlags @Binds - abstract fun bindsRestarter(debugRestarter: FeatureFlagsReleaseRestarter): Restarter + @IntoSet + abstract fun bindsScreenIdleCondition(impl: ScreenIdleCondition): ConditionalRestarter.Condition + + @Binds + @IntoSet + abstract fun bindsPluggedInCondition(impl: PluggedInCondition): ConditionalRestarter.Condition + + @Module + companion object { + @JvmStatic + @Provides + @Named(ConditionalRestarter.RESTART_DELAY) + fun provideRestartDelaySec(): Long = 30 + } } diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 92ee37310130..4aaa566eb852 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.res.Resources +import android.graphics.Rect import android.text.format.DateFormat import android.util.TypedValue import android.view.View @@ -119,10 +120,6 @@ constructor( private val mLayoutChangedListener = object : View.OnLayoutChangeListener { - private var currentSmallClockView: View? = null - private var currentLargeClockView: View? = null - private var currentSmallClockLocation = IntArray(2) - private var currentLargeClockLocation = IntArray(2) override fun onLayoutChange( view: View?, @@ -135,6 +132,8 @@ constructor( oldRight: Int, oldBottom: Int ) { + view?.removeOnLayoutChangeListener(this) + val parent = (view?.parent) as FrameLayout // don't pass in negative bounds when clocks are in transition state @@ -142,31 +141,12 @@ constructor( return } - // SMALL CLOCK - if (parent.id == R.id.lockscreen_clock_view) { - // view bounds have changed due to clock size changing (i.e. different character - // widths) - // AND/OR the view has been translated when transitioning between small and - // large clock - if ( - view != currentSmallClockView || - !view.locationOnScreen.contentEquals(currentSmallClockLocation) - ) { - currentSmallClockView = view - currentSmallClockLocation = view.locationOnScreen - updateRegionSampler(view) - } - } - // LARGE CLOCK - else if (parent.id == R.id.lockscreen_clock_view_large) { - if ( - view != currentLargeClockView || - !view.locationOnScreen.contentEquals(currentLargeClockLocation) - ) { - currentLargeClockView = view - currentLargeClockLocation = view.locationOnScreen - updateRegionSampler(view) - } + val currentViewRect = Rect(left, top, right, bottom) + val oldViewRect = Rect(oldLeft, oldTop, oldRight, oldBottom) + + if (currentViewRect.width() != oldViewRect.width() || + currentViewRect.height() != oldViewRect.height()) { + updateRegionSampler(view) } } } 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/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java index 67e3400670ba..03947542d21e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -217,9 +217,11 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { private void animate(float progress) { Interpolator standardDecelerate = Interpolators.STANDARD_DECELERATE; Interpolator legacyDecelerate = Interpolators.LEGACY_DECELERATE; + float standardProgress = standardDecelerate.getInterpolation(progress); mBouncerMessageView.setTranslationY( - mYTrans - mYTrans * standardDecelerate.getInterpolation(progress)); + mYTrans - mYTrans * standardProgress); + mBouncerMessageView.setAlpha(standardProgress); for (int i = 0; i < mViews.length; i++) { View[] row = mViews[i]; @@ -236,7 +238,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { view.setAlpha(scaledProgress); int yDistance = mYTrans + mYTransOffset * i; view.setTranslationY( - yDistance - (yDistance * standardDecelerate.getInterpolation(progress))); + yDistance - (yDistance * standardProgress)); if (view instanceof NumPadAnimationListener) { ((NumPadAnimationListener) view).setProgress(scaledProgress); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 29496169e04f..2f937a949e78 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() { @@ -1224,8 +1220,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { constraintSet.connect(rightElement, LEFT, leftElement, RIGHT); constraintSet.connect(rightElement, RIGHT, PARENT_ID, RIGHT); constraintSet.connect(mUserSwitcherViewGroup.getId(), TOP, PARENT_ID, TOP); - constraintSet.connect(mUserSwitcherViewGroup.getId(), BOTTOM, PARENT_ID, BOTTOM, - yTrans); + constraintSet.connect(mUserSwitcherViewGroup.getId(), BOTTOM, PARENT_ID, BOTTOM); constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP); constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM); constraintSet.setHorizontalChainStyle(mUserSwitcherViewGroup.getId(), CHAIN_SPREAD); 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..64fe645b9c28 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; @@ -716,6 +717,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab if (mKeyguardGoingAway) { updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onKeyguardGoingAway(); + } + } } updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } @@ -876,7 +883,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 +1872,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 +2539,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 +2918,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 +2967,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) { @@ -3666,7 +3686,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Register to receive notifications about general keyguard information * (see {@link KeyguardUpdateMonitorCallback}. * - * @param callback The callback to register + * @param callback The callback to register. Stay away from passing anonymous instances + * as they will likely be dereferenced. Ensure that the callback is a class + * field to persist it. */ public void registerCallback(KeyguardUpdateMonitorCallback callback) { Assert.isMainThread(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index 0d4889a4c39f..feff216310df 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -317,4 +317,9 @@ public class KeyguardUpdateMonitorCallback { * Called when the non-strong biometric state changed. */ public void onNonStrongBiometricAllowedChanged(int userId) { } + + /** + * Called when keyguard is going away or not going away. + */ + public void onKeyguardGoingAway() { } } 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/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index 3555d0a7e7fb..2d37c292a6b8 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -173,12 +173,6 @@ interface ControlsController : UserAwareController { fun removeFavorites(componentName: ComponentName): Boolean /** - * Checks if the favorites can be removed. You can't remove components from the preferred list. - * @param componentName the name of the service that provides the [Control] - */ - fun canRemoveFavorites(componentName: ComponentName): Boolean - - /** * Replaces the favorites for the given structure. * * Calling this method will eliminate the previous selection of favorites and replace it with a diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index a171f43013e0..ac1150e0fcb1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -38,6 +38,7 @@ import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.SysUISingleton @@ -57,16 +58,17 @@ import javax.inject.Inject @SysUISingleton class ControlsControllerImpl @Inject constructor ( - private val context: Context, - @Background private val executor: DelayableExecutor, - private val uiController: ControlsUiController, - private val bindingController: ControlsBindingController, - private val listingController: ControlsListingController, - private val userFileManager: UserFileManager, - private val userTracker: UserTracker, - private val authorizedPanelsRepository: AuthorizedPanelsRepository, - optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>, - dumpManager: DumpManager, + private val context: Context, + @Background private val executor: DelayableExecutor, + private val uiController: ControlsUiController, + private val selectedComponentRepository: SelectedComponentRepository, + private val bindingController: ControlsBindingController, + private val listingController: ControlsListingController, + private val userFileManager: UserFileManager, + private val userTracker: UserTracker, + private val authorizedPanelsRepository: AuthorizedPanelsRepository, + optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>, + dumpManager: DumpManager, ) : Dumpable, ControlsController { companion object { @@ -499,17 +501,14 @@ class ControlsControllerImpl @Inject constructor ( } } - override fun canRemoveFavorites(componentName: ComponentName): Boolean = - !authorizedPanelsRepository.getPreferredPackages().contains(componentName.packageName) - override fun removeFavorites(componentName: ComponentName): Boolean { if (!confirmAvailability()) return false - if (!canRemoveFavorites(componentName)) return false executor.execute { - Favorites.removeStructures(componentName) + if (Favorites.removeStructures(componentName)) { + persistenceWrapper.storeFavorites(Favorites.getAllStructures()) + } authorizedPanelsRepository.removeAuthorizedPanels(setOf(componentName.packageName)) - persistenceWrapper.storeFavorites(Favorites.getAllStructures()) } return true } @@ -576,7 +575,9 @@ class ControlsControllerImpl @Inject constructor ( } override fun setPreferredSelection(selectedItem: SelectedItem) { - uiController.updatePreferences(selectedItem) + selectedComponentRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(selectedItem) + ) } override fun dump(pw: PrintWriter, args: Array<out String>) { diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt index d949d1119222..2af49aa5fa1a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt @@ -36,6 +36,8 @@ import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.management.ControlsRequestDialog import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.panels.AuthorizedPanelsRepositoryImpl +import com.android.systemui.controls.panels.SelectedComponentRepository +import com.android.systemui.controls.panels.SelectedComponentRepositoryImpl import com.android.systemui.controls.settings.ControlsSettingsDialogManager import com.android.systemui.controls.settings.ControlsSettingsDialogManagerImpl import com.android.systemui.controls.ui.ControlActionCoordinator @@ -114,6 +116,11 @@ abstract class ControlsModule { repository: AuthorizedPanelsRepositoryImpl ): AuthorizedPanelsRepository + @Binds + abstract fun providePreferredPanelRepository( + repository: SelectedComponentRepositoryImpl + ): SelectedComponentRepository + @BindsOptionalOf abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt index e51e8326c0a5..5c2402ba4149 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt @@ -20,6 +20,8 @@ package com.android.systemui.controls.panels import android.content.Context import android.content.SharedPreferences import com.android.systemui.R +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl @@ -30,7 +32,8 @@ class AuthorizedPanelsRepositoryImpl constructor( private val context: Context, private val userFileManager: UserFileManager, - private val userTracker: UserTracker + private val userTracker: UserTracker, + private val featureFlags: FeatureFlags, ) : AuthorizedPanelsRepository { override fun getAuthorizedPanels(): Set<String> { @@ -71,8 +74,18 @@ constructor( userTracker.userId, ) - // If we've never run this (i.e., the key doesn't exist), add the default packages - if (sharedPref.getStringSet(KEY, null) == null) { + // We should add default packages in two cases: + // 1) We've never run this + // 2) APP_PANELS_REMOVE_APPS_ALLOWED got disabled after user removed all apps + val needToSetup = + if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED)) { + sharedPref.getStringSet(KEY, null) == null + } else { + // There might be an empty set that need to be overridden after the feature has been + // turned off after being turned on + sharedPref.getStringSet(KEY, null).isNullOrEmpty() + } + if (needToSetup) { sharedPref.edit().putStringSet(KEY, getPreferredPackages()).apply() } return sharedPref diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt new file mode 100644 index 000000000000..5bb6eece9098 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.controls.panels + +import android.content.ComponentName +import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.ui.SelectedItem +import com.android.systemui.flags.Flags + +/** Stores user-selected preferred component. */ +interface SelectedComponentRepository { + + /** + * Returns currently set preferred component, or null when nothing is set. Consider using + * [ControlsUiController.getPreferredSelectedItem] to get domain specific data + */ + fun getSelectedComponent(): SelectedComponent? + + /** Sets preferred component. Use [getSelectedComponent] to get current one */ + fun setSelectedComponent(selectedComponent: SelectedComponent) + + /** Clears current preferred component. [getSelectedComponent] will return null afterwards */ + fun removeSelectedComponent() + + /** + * Return true when default preferred component should be set up and false the otherwise. This + * is always true when [Flags.APP_PANELS_REMOVE_APPS_ALLOWED] is disabled + */ + fun shouldAddDefaultComponent(): Boolean + + /** + * Sets if default component should be added. This is ignored when + * [Flags.APP_PANELS_REMOVE_APPS_ALLOWED] is disabled + */ + fun setShouldAddDefaultComponent(shouldAdd: Boolean) + + data class SelectedComponent( + val name: String, + val componentName: ComponentName?, + val isPanel: Boolean, + ) { + constructor( + selectedItem: SelectedItem + ) : this( + name = selectedItem.name.toString(), + componentName = selectedItem.componentName, + isPanel = selectedItem is SelectedItem.PanelItem, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt new file mode 100644 index 000000000000..0fb5b66ef93c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt @@ -0,0 +1,95 @@ +/* + * 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.controls.panels + +import android.content.ComponentName +import android.content.Context +import android.content.SharedPreferences +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import javax.inject.Inject + +@SysUISingleton +class SelectedComponentRepositoryImpl +@Inject +constructor( + private val userFileManager: UserFileManager, + private val userTracker: UserTracker, + private val featureFlags: FeatureFlags, +) : SelectedComponentRepository { + + private companion object { + const val PREF_COMPONENT = "controls_component" + const val PREF_STRUCTURE_OR_APP_NAME = "controls_structure" + const val PREF_IS_PANEL = "controls_is_panel" + const val SHOULD_ADD_DEFAULT_PANEL = "should_add_default_panel" + } + + private val sharedPreferences: SharedPreferences + get() = + userFileManager.getSharedPreferences( + fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + mode = Context.MODE_PRIVATE, + userId = userTracker.userId + ) + + override fun getSelectedComponent(): SelectedComponentRepository.SelectedComponent? { + with(sharedPreferences) { + val componentString = getString(PREF_COMPONENT, null) ?: return null + return SelectedComponentRepository.SelectedComponent( + name = getString(PREF_STRUCTURE_OR_APP_NAME, "")!!, + componentName = ComponentName.unflattenFromString(componentString), + isPanel = getBoolean(PREF_IS_PANEL, false) + ) + } + } + + override fun setSelectedComponent( + selectedComponent: SelectedComponentRepository.SelectedComponent + ) { + sharedPreferences + .edit() + .putString(PREF_COMPONENT, selectedComponent.componentName?.flattenToString()) + .putString(PREF_STRUCTURE_OR_APP_NAME, selectedComponent.name) + .putBoolean(PREF_IS_PANEL, selectedComponent.isPanel) + .apply() + } + + override fun removeSelectedComponent() { + sharedPreferences + .edit() + .remove(PREF_COMPONENT) + .remove(PREF_STRUCTURE_OR_APP_NAME) + .remove(PREF_IS_PANEL) + .apply() + } + + override fun shouldAddDefaultComponent(): Boolean = + if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED)) { + sharedPreferences.getBoolean(SHOULD_ADD_DEFAULT_PANEL, true) + } else { + true + } + + override fun setShouldAddDefaultComponent(shouldAdd: Boolean) { + sharedPreferences.edit().putBoolean(SHOULD_ADD_DEFAULT_PANEL, shouldAdd).apply() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt index 9d99253de741..3a4a00c0ccd3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt @@ -18,17 +18,16 @@ package com.android.systemui.controls.start import android.content.Context -import android.content.res.Resources import android.os.UserHandle import com.android.systemui.CoreStartable -import com.android.systemui.R import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.UserTracker import java.util.concurrent.Executor import javax.inject.Inject @@ -37,7 +36,7 @@ import javax.inject.Inject * Started with SystemUI to perform early operations for device controls subsystem (only if enabled) * * In particular, it will perform the following: - * * If there is no preferred selection for provider and at least one of the preferred packages + * * If there is no preferred selection for provider and at least one of the preferred packages * provides a panel, it will select the first one that does. * * If the preferred selection provides a panel, it will bind to that service (to reduce latency on * displaying the panel). @@ -48,10 +47,11 @@ import javax.inject.Inject class ControlsStartable @Inject constructor( - @Main private val resources: Resources, - @Background private val executor: Executor, - private val controlsComponent: ControlsComponent, - private val userTracker: UserTracker + @Background private val executor: Executor, + private val controlsComponent: ControlsComponent, + private val userTracker: UserTracker, + private val authorizedPanelsRepository: AuthorizedPanelsRepository, + private val selectedComponentRepository: SelectedComponentRepository, ) : CoreStartable { // These two controllers can only be accessed after `start` method once we've checked if the @@ -85,12 +85,15 @@ constructor( } private fun selectDefaultPanelIfNecessary() { + if (!selectedComponentRepository.shouldAddDefaultComponent()) { + return + } val currentSelection = controlsController.getPreferredSelection() if (currentSelection == SelectedItem.EMPTY_SELECTION) { val availableServices = controlsListingController.getCurrentServices() val panels = availableServices.filter { it.panelActivity != null } - resources - .getStringArray(R.array.config_controlsPreferredPackages) + authorizedPanelsRepository + .getPreferredPackages() // Looking for the first element in the string array such that there is one package // that has a panel. It will return null if there are no packages in the array, // or if no packages in the array have a panel associated with it. diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index 58673bb6f567..0d5311752ab9 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -64,8 +64,6 @@ interface ControlsUiController { * This element will be the one that appears when the user first opens the controls activity. */ fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem - - fun updatePreferences(selectedItem: SelectedItem) } sealed class SelectedItem { diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index c61dad6fc075..5da86de933e6 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -64,6 +64,7 @@ import com.android.systemui.controls.management.ControlsFavoritingActivity import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -73,9 +74,7 @@ import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.globalactions.GlobalActionsPopupMenu import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker -import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.asIndenting import com.android.systemui.util.concurrency.DelayableExecutor @@ -84,7 +83,7 @@ import com.android.wm.shell.TaskViewFactory import dagger.Lazy import java.io.PrintWriter import java.text.Collator -import java.util.* +import java.util.Optional import java.util.function.Consumer import javax.inject.Inject @@ -98,25 +97,22 @@ class ControlsUiControllerImpl @Inject constructor ( @Main val uiExecutor: DelayableExecutor, @Background val bgExecutor: DelayableExecutor, val controlsListingController: Lazy<ControlsListingController>, - val controlActionCoordinator: ControlActionCoordinator, + private val controlActionCoordinator: ControlActionCoordinator, private val activityStarter: ActivityStarter, private val iconCache: CustomIconCache, private val controlsMetricsLogger: ControlsMetricsLogger, private val keyguardStateController: KeyguardStateController, - private val userFileManager: UserFileManager, private val userTracker: UserTracker, private val taskViewFactory: Optional<TaskViewFactory>, private val controlsSettingsRepository: ControlsSettingsRepository, private val authorizedPanelsRepository: AuthorizedPanelsRepository, + private val selectedComponentRepository: SelectedComponentRepository, private val featureFlags: FeatureFlags, private val dialogsFactory: ControlsDialogsFactory, dumpManager: DumpManager ) : ControlsUiController, Dumpable { companion object { - private const val PREF_COMPONENT = "controls_component" - private const val PREF_STRUCTURE_OR_APP_NAME = "controls_structure" - private const val PREF_IS_PANEL = "controls_is_panel" private const val FADE_IN_MILLIS = 200L @@ -138,12 +134,6 @@ class ControlsUiControllerImpl @Inject constructor ( private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow) private var retainCache = false private var lastSelections = emptyList<SelectionItem>() - private val sharedPreferences - get() = userFileManager.getSharedPreferences( - fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, - mode = 0, - userId = userTracker.userId - ) private var taskViewController: PanelTaskViewController? = null @@ -341,20 +331,18 @@ class ControlsUiControllerImpl @Inject constructor ( if (!controlsController.get().removeFavorites(componentName)) { return@createRemoveAppDialog } - if ( - sharedPreferences.getString(PREF_COMPONENT, "") == - componentName.flattenToString() - ) { - sharedPreferences - .edit() - .remove(PREF_COMPONENT) - .remove(PREF_STRUCTURE_OR_APP_NAME) - .remove(PREF_IS_PANEL) - .commit() + + if (selectedComponentRepository.getSelectedComponent()?.componentName == + componentName) { + selectedComponentRepository.removeSelectedComponent() } - allStructures = controlsController.get().getFavorites() - selectedItem = getPreferredSelectedItem(allStructures) + val selectedItem = getPreferredSelectedItem(controlsController.get().getFavorites()) + if (selectedItem == SelectedItem.EMPTY_SELECTION) { + // User removed the last panel. In this case we start app selection flow and don't + // want to auto-add it again + selectedComponentRepository.setShouldAddDefaultComponent(false) + } reload(parent) }.apply { show() } } @@ -522,8 +510,7 @@ class ControlsUiControllerImpl @Inject constructor ( ADD_APP_ID )) } - if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED) && - controlsController.get().canRemoveFavorites(selectedItem.componentName)) { + if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED)) { add(OverflowMenuAdapter.MenuItem( context.getText(R.string.controls_menu_remove), REMOVE_APP_ID, @@ -569,7 +556,7 @@ class ControlsUiControllerImpl @Inject constructor ( ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure) EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure) REMOVE_APP_ID -> startRemovingApp( - selectedStructure.componentName, selectionItem.appName + selectionItem.componentName, selectionItem.appName ) } dismiss() @@ -714,29 +701,22 @@ class ControlsUiControllerImpl @Inject constructor ( } override fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem { - val sp = sharedPreferences - - val component = sp.getString(PREF_COMPONENT, null)?.let { - ComponentName.unflattenFromString(it) - } ?: EMPTY_COMPONENT - val name = sp.getString(PREF_STRUCTURE_OR_APP_NAME, "")!! - val isPanel = sp.getBoolean(PREF_IS_PANEL, false) - return if (isPanel) { - SelectedItem.PanelItem(name, component) + val preferredPanel = selectedComponentRepository.getSelectedComponent() + val component = preferredPanel?.componentName ?: EMPTY_COMPONENT + return if (preferredPanel?.isPanel == true) { + SelectedItem.PanelItem(preferredPanel.name, component) } else { if (structures.isEmpty()) return SelectedItem.EMPTY_SELECTION SelectedItem.StructureItem(structures.firstOrNull { - component == it.componentName && name == it.structure - } ?: structures.get(0)) + component == it.componentName && preferredPanel?.name == it.structure + } ?: structures[0]) } } - override fun updatePreferences(selectedItem: SelectedItem) { - sharedPreferences.edit() - .putString(PREF_COMPONENT, selectedItem.componentName.flattenToString()) - .putString(PREF_STRUCTURE_OR_APP_NAME, selectedItem.name.toString()) - .putBoolean(PREF_IS_PANEL, selectedItem is SelectedItem.PanelItem) - .apply() + private fun updatePreferences(selectedItem: SelectedItem) { + selectedComponentRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(selectedItem) + ) } private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean { 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/ConditionalRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt new file mode 100644 index 000000000000..b20e33a63776 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt @@ -0,0 +1,103 @@ +/* + * 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.flags + +import android.util.Log +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** Restarts the process after all passed in [Condition]s are true. */ +class ConditionalRestarter +@Inject +constructor( + private val systemExitRestarter: SystemExitRestarter, + private val conditions: Set<@JvmSuppressWildcards Condition>, + @Named(RESTART_DELAY) private val restartDelaySec: Long, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : Restarter { + + private var restartJob: Job? = null + private var pendingReason = "" + private var androidRestartRequested = false + + override fun restartSystemUI(reason: String) { + Log.d(FeatureFlagsDebug.TAG, "SystemUI Restart requested. Restarting when idle.") + scheduleRestart(reason) + } + + override fun restartAndroid(reason: String) { + Log.d(FeatureFlagsDebug.TAG, "Android Restart requested. Restarting when idle.") + androidRestartRequested = true + scheduleRestart(reason) + } + + private fun scheduleRestart(reason: String = "") { + pendingReason = if (reason.isEmpty()) pendingReason else reason + + if (conditions.all { c -> c.canRestartNow(this::scheduleRestart) }) { + if (restartJob == null) { + restartJob = + applicationScope.launch(backgroundDispatcher) { + delay(TimeUnit.SECONDS.toMillis(restartDelaySec)) + restartNow() + } + } + } else { + restartJob?.cancel() + restartJob = null + } + } + + private fun restartNow() { + if (androidRestartRequested) { + systemExitRestarter.restartAndroid(pendingReason) + } else { + systemExitRestarter.restartSystemUI(pendingReason) + } + } + + interface Condition { + /** + * Should return true if the system is ready to restart. + * + * A call to this function means that we want to restart and are waiting for this condition + * to return true. + * + * retryFn should be cached if it is _not_ ready to restart, and later called when it _is_ + * ready to restart. At that point, this method will be called again to verify that the + * system is ready. + * + * Multiple calls to an instance of this method may happen for a single restart attempt if + * multiple [Condition]s are being checked. If any one [Condition] returns false, all the + * [Condition]s will need to be rechecked on the next restart attempt. + */ + fun canRestartNow(retryFn: () -> Unit): Boolean + } + + companion object { + const val RESTART_DELAY = "restarter_restart_delay" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt deleted file mode 100644 index a6956a443e46..000000000000 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.flags - -import android.util.Log -import com.android.systemui.keyguard.WakefulnessLifecycle -import javax.inject.Inject - -/** Restarts SystemUI when the screen is locked. */ -class FeatureFlagsDebugRestarter -@Inject -constructor( - private val wakefulnessLifecycle: WakefulnessLifecycle, - private val systemExitRestarter: SystemExitRestarter, -) : Restarter { - - private var androidRestartRequested = false - private var pendingReason = "" - - val observer = - object : WakefulnessLifecycle.Observer { - override fun onFinishedGoingToSleep() { - Log.d(FeatureFlagsDebug.TAG, "Restarting due to systemui flag change") - restartNow() - } - } - - override fun restartSystemUI(reason: String) { - Log.d(FeatureFlagsDebug.TAG, "SystemUI Restart requested. Restarting on next screen off.") - Log.i(FeatureFlagsDebug.TAG, reason) - scheduleRestart(reason) - } - - override fun restartAndroid(reason: String) { - Log.d(FeatureFlagsDebug.TAG, "Android Restart requested. Restarting on next screen off.") - androidRestartRequested = true - scheduleRestart(reason) - } - - fun scheduleRestart(reason: String) { - pendingReason = reason - if (wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP) { - restartNow() - } else { - wakefulnessLifecycle.addObserver(observer) - } - } - - private fun restartNow() { - if (androidRestartRequested) { - systemExitRestarter.restartAndroid(pendingReason) - } else { - systemExitRestarter.restartSystemUI(pendingReason) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt deleted file mode 100644 index c08266caf147..000000000000 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.flags - -import android.util.Log -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP -import com.android.systemui.statusbar.policy.BatteryController -import com.android.systemui.util.concurrency.DelayableExecutor -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -/** Restarts SystemUI when the device appears idle. */ -class FeatureFlagsReleaseRestarter -@Inject -constructor( - private val wakefulnessLifecycle: WakefulnessLifecycle, - private val batteryController: BatteryController, - @Background private val bgExecutor: DelayableExecutor, - private val systemExitRestarter: SystemExitRestarter -) : Restarter { - var listenersAdded = false - var pendingRestart: Runnable? = null - private var pendingReason = "" - var androidRestartRequested = false - - val observer = - object : WakefulnessLifecycle.Observer { - override fun onFinishedGoingToSleep() { - scheduleRestart(pendingReason) - } - } - - val batteryCallback = - object : BatteryController.BatteryStateChangeCallback { - override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { - scheduleRestart(pendingReason) - } - } - - override fun restartSystemUI(reason: String) { - Log.d( - FeatureFlagsDebug.TAG, - "SystemUI Restart requested. Restarting when plugged in and idle." - ) - scheduleRestart(reason) - } - - override fun restartAndroid(reason: String) { - Log.d( - FeatureFlagsDebug.TAG, - "Android Restart requested. Restarting when plugged in and idle." - ) - androidRestartRequested = true - scheduleRestart(reason) - } - - private fun scheduleRestart(reason: String) { - // Don't bother adding listeners twice. - pendingReason = reason - if (!listenersAdded) { - listenersAdded = true - wakefulnessLifecycle.addObserver(observer) - batteryController.addCallback(batteryCallback) - } - if ( - wakefulnessLifecycle.wakefulness == WAKEFULNESS_ASLEEP && batteryController.isPluggedIn - ) { - if (pendingRestart == null) { - pendingRestart = bgExecutor.executeDelayed(this::restartNow, 30L, TimeUnit.SECONDS) - } - } else if (pendingRestart != null) { - pendingRestart?.run() - pendingRestart = null - } - } - - private fun restartNow() { - Log.d(FeatureFlagsRelease.TAG, "Restarting due to systemui flag change") - if (androidRestartRequested) { - systemExitRestarter.restartAndroid(pendingReason) - } else { - systemExitRestarter.restartSystemUI(pendingReason) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 3608b91aa91a..131ad5591203 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 = @@ -383,7 +391,10 @@ object Flags { val SHOW_LOWLIGHT_ON_DIRECT_BOOT = releasedFlag(1003, "show_lowlight_on_direct_boot") @JvmField - val ENABLE_LOW_LIGHT_CLOCK_UNDOCKED = unreleasedFlag(1004, "enable_low_light_clock_undocked") + // TODO(b/271428141): Tracking Bug + val ENABLE_LOW_LIGHT_CLOCK_UNDOCKED = unreleasedFlag( + 1004, + "enable_low_light_clock_undocked", teamfood = true) // 1100 - windowing @Keep @@ -438,7 +449,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 +474,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/flags/FlagsCommonModule.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt index 0054d266c283..3c5012559a89 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.flags +import dagger.Binds import dagger.Module import dagger.Provides import javax.inject.Named @@ -22,6 +23,8 @@ import javax.inject.Named /** Module containing shared code for all FeatureFlag implementations. */ @Module interface FlagsCommonModule { + @Binds fun bindsRestarter(impl: ConditionalRestarter): Restarter + companion object { const val ALL_FLAGS = "all_flags" diff --git a/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt b/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt new file mode 100644 index 000000000000..3120638cb17f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import com.android.systemui.statusbar.policy.BatteryController +import javax.inject.Inject + +/** Returns true when the device is plugged in. */ +class PluggedInCondition +@Inject +constructor( + private val batteryController: BatteryController, +) : ConditionalRestarter.Condition { + + var listenersAdded = false + var retryFn: (() -> Unit)? = null + + val batteryCallback = + object : BatteryController.BatteryStateChangeCallback { + override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { + retryFn?.invoke() + } + } + + override fun canRestartNow(retryFn: () -> Unit): Boolean { + if (!listenersAdded) { + listenersAdded = true + batteryController.addCallback(batteryCallback) + } + + this.retryFn = retryFn + + return batteryController.isPluggedIn + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/ScreenIdleCondition.kt b/packages/SystemUI/src/com/android/systemui/flags/ScreenIdleCondition.kt new file mode 100644 index 000000000000..49e61afbdcd6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/ScreenIdleCondition.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import com.android.systemui.keyguard.WakefulnessLifecycle +import javax.inject.Inject + +/** Returns true when the device is "asleep" as defined by the [WakefullnessLifecycle]. */ +class ScreenIdleCondition +@Inject +constructor( + private val wakefulnessLifecycle: WakefulnessLifecycle, +) : ConditionalRestarter.Condition { + + var listenersAdded = false + var retryFn: (() -> Unit)? = null + + val observer = + object : WakefulnessLifecycle.Observer { + override fun onFinishedGoingToSleep() { + retryFn?.invoke() + } + } + + override fun canRestartNow(retryFn: () -> Unit): Boolean { + if (!listenersAdded) { + listenersAdded = true + wakefulnessLifecycle.addObserver(observer) + } + + this.retryFn = retryFn + + return wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP + } +} 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..c42e5028e18c 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,10 +140,10 @@ 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 + /** Whether or not quick settings or quick quick settings are showing. */ + val isQuickSettingsVisible: Flow<Boolean> = repository.isQuickSettingsVisible /** * Observable for [BiometricUnlockModel] when biometrics like face or any fingerprint (rear, * side, under display) is used to unlock the device. 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..70648276365e 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, ) { @@ -94,8 +96,9 @@ constructor( quickAffordanceAlwaysVisible(position), keyguardInteractor.isDozing, keyguardInteractor.isKeyguardShowing, - ) { affordance, isDozing, isKeyguardShowing -> - if (!isDozing && isKeyguardShowing) { + keyguardInteractor.isQuickSettingsVisible + ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible -> + if (!isDozing && isKeyguardShowing && !isQuickSettingsVisible) { affordance } else { KeyguardQuickAffordanceModel.Hidden @@ -122,10 +125,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 +144,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 +197,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/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt index edd28972c05e..d092337adfcb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt @@ -120,21 +120,24 @@ constructor( val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 } val sideFpsShowing: Flow<Boolean> = repository.sideFpsShowing + /** + * This callback needs to be a class field so it does not get garbage collected. + */ + val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { + override fun onBiometricRunningStateChanged( + running: Boolean, + biometricSourceType: BiometricSourceType? + ) { + updateSideFpsVisibility() + } + + override fun onStrongAuthStateChanged(userId: Int) { + updateSideFpsVisibility() + } + } + init { - keyguardUpdateMonitor.registerCallback( - object : KeyguardUpdateMonitorCallback() { - override fun onBiometricRunningStateChanged( - running: Boolean, - biometricSourceType: BiometricSourceType? - ) { - updateSideFpsVisibility() - } - - override fun onStrongAuthStateChanged(userId: Int) { - updateSideFpsVisibility() - } - } - ) + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) } // TODO(b/243685699): Move isScrimmed logic to data layer. 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/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 72c4aabd44b9..4cc041074a8d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -1346,13 +1346,9 @@ class MediaDataManager( fun onNotificationRemoved(key: String) { Assert.isMainThread() val removed = mediaEntries.remove(key) ?: return - val isEligibleForResume = - removed.isLocalSession() || - (mediaFlags.isRemoteResumeAllowed() && - removed.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) - } else if (useMediaResumption && removed.resumeAction != null && isEligibleForResume) { + } else if (isAbleToResume(removed)) { convertToResumePlayer(key, removed) } else if (mediaFlags.isRetainingPlayersEnabled()) { handlePossibleRemoval(key, removed, notificationRemoved = true) @@ -1372,6 +1368,14 @@ class MediaDataManager( handlePossibleRemoval(key, updated) } + private fun isAbleToResume(data: MediaData): Boolean { + val isEligibleForResume = + data.isLocalSession() || + (mediaFlags.isRemoteResumeAllowed() && + data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) + return useMediaResumption && data.resumeAction != null && isEligibleForResume + } + /** * Convert to resume state if the player is no longer valid and active, then notify listeners * that the data was updated. Does not convert to resume state if the player is still valid, or @@ -1394,8 +1398,9 @@ class MediaDataManager( if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key") mediaEntries.put(key, removed) notifyMediaDataLoaded(key, key, removed) - } else if (removed.active) { - // This player was still active - it didn't last long enough to time out: remove + } else if (removed.active && !isAbleToResume(removed)) { + // This player was still active - it didn't last long enough to time out, + // and its app doesn't normally support resume: remove if (DEBUG) Log.d(TAG, "Removing still-active player $key") notifyMediaDataRemoved(key) logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt index 92e0c851a462..b0389b50cd7d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt @@ -239,6 +239,8 @@ constructor( data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) if (data.resumeAction == null && !data.hasCheckedForResume && isEligibleForResume) { // TODO also check for a media button receiver intended for restarting (b/154127084) + // Set null action to prevent additional attempts to connect + mediaDataManager.setResumeAction(key, null) Log.d(TAG, "Checking for service component for " + data.packageName) val pm = context.packageManager val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE) @@ -249,9 +251,6 @@ constructor( backgroundExecutor.execute { tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName) } - } else { - // No service found - mediaDataManager.setResumeAction(key, null) } } } @@ -263,8 +262,6 @@ constructor( */ private fun tryUpdateResumptionList(key: String, componentName: ComponentName) { Log.d(TAG, "Testing if we can connect to $componentName") - // Set null action to prevent additional attempts to connect - mediaDataManager.setResumeAction(key, null) mediaBrowser = mediaBrowserFactory.create( object : ResumeMediaBrowser.Callback() { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java index 3493b2453fd6..d460b5b5d782 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java @@ -85,16 +85,13 @@ public class ResumeMediaBrowser { * ResumeMediaBrowser#disconnect will be called automatically with this function. */ public void findRecentMedia() { - disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = mBrowserFactory.create( + MediaBrowser browser = mBrowserFactory.create( mComponentName, mConnectionCallback, rootHints); - updateMediaController(); - mLogger.logConnection(mComponentName, "findRecentMedia"); - mMediaBrowser.connect(); + connectBrowser(browser, "findRecentMedia"); } private final MediaBrowser.SubscriptionCallback mSubscriptionCallback = @@ -202,6 +199,21 @@ public class ResumeMediaBrowser { }; /** + * Connect using a new media browser. Disconnects the existing browser first, if it exists. + * @param browser media browser to connect + * @param reason Reason to log for connection + */ + private void connectBrowser(MediaBrowser browser, String reason) { + mLogger.logConnection(mComponentName, reason); + disconnect(); + mMediaBrowser = browser; + if (browser != null) { + browser.connect(); + } + updateMediaController(); + } + + /** * Disconnect the media browser. This should be done after callbacks have completed to * disconnect from the media browser service. */ @@ -222,10 +234,9 @@ public class ResumeMediaBrowser { * getting a media update from the app */ public void restart() { - disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = mBrowserFactory.create(mComponentName, + MediaBrowser browser = mBrowserFactory.create(mComponentName, new MediaBrowser.ConnectionCallback() { @Override public void onConnected() { @@ -265,9 +276,7 @@ public class ResumeMediaBrowser { disconnect(); } }, rootHints); - updateMediaController(); - mLogger.logConnection(mComponentName, "restart"); - mMediaBrowser.connect(); + connectBrowser(browser, "restart"); } @VisibleForTesting @@ -305,16 +314,13 @@ public class ResumeMediaBrowser { * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed. */ public void testConnection() { - disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = mBrowserFactory.create( + MediaBrowser browser = mBrowserFactory.create( mComponentName, mConnectionCallback, rootHints); - updateMediaController(); - mLogger.logConnection(mComponentName, "testConnection"); - mMediaBrowser.connect(); + connectBrowser(browser, "testConnection"); } /** Updates mMediaController based on our current browser values. */ 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..e4351d2c412c 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -25,11 +25,6 @@ import static android.app.StatusBarManager.WindowType; import static android.app.StatusBarManager.WindowVisibleState; import static android.app.StatusBarManager.windowStateToString; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; -import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES; -import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT; -import static android.view.InsetsState.ITYPE_LEFT_GESTURES; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_RIGHT_GESTURES; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; @@ -292,6 +287,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final DeadZone mDeadZone; private boolean mImeVisible; private final Rect mSamplingBounds = new Rect(); + private final Binder mInsetsSourceOwner = new Binder(); /** * When quickswitching between apps of different orientations, we draw a secondary home handle @@ -1709,65 +1705,46 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } private InsetsFrameProvider[] getInsetsFrameProvider(int insetsHeight, Context userContext) { - final InsetsFrameProvider navBarProvider; + final InsetsFrameProvider navBarProvider = + new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.navigationBars()) + .setInsetsSizeOverrides(new InsetsFrameProvider.InsetsSizeOverride[] { + new InsetsFrameProvider.InsetsSizeOverride( + TYPE_INPUT_METHOD, null)}); if (insetsHeight != -1 && !mIsButtonForceVisible) { - navBarProvider = new InsetsFrameProvider( - ITYPE_NAVIGATION_BAR, Insets.of(0, 0, 0, insetsHeight)); - // Use window frame for IME. - navBarProvider.insetsSizeOverrides = new InsetsFrameProvider.InsetsSizeOverride[] { - new InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, null) - }; - } else { - navBarProvider = new InsetsFrameProvider(ITYPE_NAVIGATION_BAR); - navBarProvider.insetsSizeOverrides = new InsetsFrameProvider.InsetsSizeOverride[]{ - new InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, null) - }; + navBarProvider.setInsetsSize(Insets.of(0, 0, 0, insetsHeight)); } + + final InsetsFrameProvider tappableElementProvider = new InsetsFrameProvider( + mInsetsSourceOwner, 0, WindowInsets.Type.tappableElement()); final boolean navBarTapThrough = userContext.getResources().getBoolean( com.android.internal.R.bool.config_navBarTapThrough); - final InsetsFrameProvider bottomTappableProvider; if (navBarTapThrough) { - bottomTappableProvider = new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT, - Insets.of(0, 0, 0, 0)); - } else { - 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 - }; - } + tappableElementProvider.setInsetsSize(Insets.NONE); + } + + 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, + tappableElementProvider, + new InsetsFrameProvider( + mInsetsSourceOwner, 0, WindowInsets.Type.mandatorySystemGestures()) + .setInsetsSize(Insets.of(0, 0, 0, gestureHeight)), + new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.systemGestures()) + .setSource(InsetsFrameProvider.SOURCE_DISPLAY) + .setInsetsSize(Insets.of(gestureInsetsLeft, 0, 0, 0)), + new InsetsFrameProvider(mInsetsSourceOwner, 1, WindowInsets.Type.systemGestures()) + .setSource(InsetsFrameProvider.SOURCE_DISPLAY) + .setInsetsSize(Insets.of(0, 0, gestureInsetsRight, 0)) + }; } 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/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 8721d71897f7..557e95c64443 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -419,6 +419,10 @@ public class ScreenshotController { return; } + mScreenBitmap = screenshot.getBitmap(); + String oldPackageName = mPackageName; + mPackageName = screenshot.getPackageNameString(); + if (!isUserSetupComplete(Process.myUserHandle())) { Log.w(TAG, "User setup not complete, displaying toast only"); // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing @@ -433,10 +437,6 @@ public class ScreenshotController { mScreenshotTakenInPortrait = mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; - String oldPackageName = mPackageName; - mPackageName = screenshot.getPackageNameString(); - - mScreenBitmap = screenshot.getBitmap(); // Optimizations mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); 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..5a8dec1865f9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -17,6 +17,7 @@ package com.android.systemui.shade; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; +import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; @@ -305,6 +306,7 @@ public final class NotificationPanelViewController implements Dumpable { */ public final boolean mAnimateBack; + private final boolean mTrackpadGestureBack; /** * The minimum scale to "squish" the Shade and associated elements down to, for Back gesture */ @@ -849,6 +851,7 @@ public final class NotificationPanelViewController implements Dumpable { mLayoutInflater = layoutInflater; mFeatureFlags = featureFlags; mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE); + mTrackpadGestureBack = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_BACK); mFalsingCollector = falsingCollector; mPowerManager = powerManager; mWakeUpCoordinator = coordinator; @@ -4758,6 +4761,9 @@ public final class NotificationPanelViewController implements Dumpable { addMovement(event); break; case MotionEvent.ACTION_POINTER_UP: + if (isTrackpadMotionEvent(event)) { + break; + } final int upPointer = event.getPointerId(event.getActionIndex()); if (mTrackingPointer == upPointer) { // gesture is ongoing, find a new pointer to track @@ -4771,7 +4777,8 @@ public final class NotificationPanelViewController implements Dumpable { mShadeLog.logMotionEventStatusBarState(event, mStatusBarStateController.getState(), "onInterceptTouchEvent: pointer down action"); - if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { + if (!isTrackpadMotionEvent(event) + && mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { mMotionAborted = true; mVelocityTracker.clear(); } @@ -4974,6 +4981,9 @@ public final class NotificationPanelViewController implements Dumpable { break; case MotionEvent.ACTION_POINTER_UP: + if (isTrackpadMotionEvent(event)) { + break; + } final int upPointer = event.getPointerId(event.getActionIndex()); if (mTrackingPointer == upPointer) { // gesture is ongoing, find a new pointer to track @@ -4990,7 +5000,8 @@ public final class NotificationPanelViewController implements Dumpable { mShadeLog.logMotionEventStatusBarState(event, mStatusBarStateController.getState(), "handleTouch: pointer down action"); - if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { + if (!isTrackpadMotionEvent(event) + && mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { mMotionAborted = true; endMotionEvent(event, x, y, true /* forceCancel */); return false; @@ -5064,6 +5075,11 @@ public final class NotificationPanelViewController implements Dumpable { } return !mGestureWaitForTouchSlop || mTracking; } + + private boolean isTrackpadMotionEvent(MotionEvent ev) { + return mTrackpadGestureBack + && ev.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE; + } } static class SplitShadeTransitionAdapter extends Transition { 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/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 82c5ee64b046..826e28955afc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -501,8 +501,10 @@ constructor( private fun updateTextColorFromRegionSampler() { smartspaceViews.forEach { - val textColor = regionSamplers.getValue(it).currentForegroundColor() - it.setPrimaryTextColor(textColor) + val textColor = regionSamplers.get(it)?.currentForegroundColor() + if (textColor != null) { + it.setPrimaryTextColor(textColor) + } } } 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..4156fc152602 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 @@ -51,8 +51,8 @@ constructor( ) } - fun logOnLost(network: Network) { - LoggerHelper.logOnLost(buffer, TAG, network) + fun logOnLost(network: Network, isDefaultNetworkCallback: Boolean) { + LoggerHelper.logOnLost(buffer, TAG, network, isDefaultNetworkCallback) } fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) { @@ -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/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt index 85729c12cd4c..19f0242040fd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt @@ -24,9 +24,11 @@ import android.telephony.TelephonyManager.DATA_HANDOVER_IN_PROGRESS import android.telephony.TelephonyManager.DATA_SUSPENDED import android.telephony.TelephonyManager.DATA_UNKNOWN import android.telephony.TelephonyManager.DataState +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger /** Internal enum representation of the telephony data connection states */ -enum class DataConnectionState { +enum class DataConnectionState : Diffable<DataConnectionState> { Connected, Connecting, Disconnected, @@ -34,7 +36,17 @@ enum class DataConnectionState { Suspended, HandoverInProgress, Unknown, - Invalid, + Invalid; + + override fun logDiffs(prevVal: DataConnectionState, row: TableRowLogger) { + if (prevVal != this) { + row.logChange(COL_CONNECTION_STATE, name) + } + } + + companion object { + private const val COL_CONNECTION_STATE = "connectionState" + } } fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt deleted file mode 100644 index ed7f60b50bb9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.mobile.data.model - -import android.annotation.IntRange -import android.telephony.CellSignalStrength -import android.telephony.TelephonyCallback.CarrierNetworkListener -import android.telephony.TelephonyCallback.DataActivityListener -import android.telephony.TelephonyCallback.DataConnectionStateListener -import android.telephony.TelephonyCallback.DisplayInfoListener -import android.telephony.TelephonyCallback.ServiceStateListener -import android.telephony.TelephonyCallback.SignalStrengthsListener -import android.telephony.TelephonyDisplayInfo -import android.telephony.TelephonyManager -import androidx.annotation.VisibleForTesting -import com.android.systemui.log.table.Diffable -import com.android.systemui.log.table.TableRowLogger -import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected -import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel - -/** - * Data class containing all of the relevant information for a particular line of service, known as - * a Subscription in the telephony world. These models are the result of a single telephony listener - * which has many callbacks which each modify some particular field on this object. - * - * The design goal here is to de-normalize fields from the system into our model fields below. So - * any new field that needs to be tracked should be copied into this data class rather than - * threading complex system objects through the pipeline. - */ -data class MobileConnectionModel( - /** Fields below are from [ServiceStateListener.onServiceStateChanged] */ - val isEmergencyOnly: Boolean = false, - val isRoaming: Boolean = false, - /** - * See [android.telephony.ServiceState.getOperatorAlphaShort], this value is defined as the - * current registered operator name in short alphanumeric format. In some cases this name might - * be preferred over other methods of calculating the network name - */ - val operatorAlphaShort: String? = null, - - /** - * TODO (b/263167683): Clarify this field - * - * This check comes from [com.android.settingslib.Utils.isInService]. It is intended to be a - * mapping from a ServiceState to a notion of connectivity. Notably, it will consider a - * connection to be in-service if either the voice registration state is IN_SERVICE or the data - * registration state is IN_SERVICE and NOT IWLAN. - */ - val isInService: Boolean = false, - - /** Fields below from [SignalStrengthsListener.onSignalStrengthsChanged] */ - val isGsm: Boolean = false, - @IntRange(from = 0, to = 4) - val cdmaLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, - @IntRange(from = 0, to = 4) - val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, - - /** Fields below from [DataConnectionStateListener.onDataConnectionStateChanged] */ - val dataConnectionState: DataConnectionState = Disconnected, - - /** - * Fields below from [DataActivityListener.onDataActivity]. See [TelephonyManager] for the - * values - */ - val dataActivityDirection: DataActivityModel = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = false, - ), - - /** Fields below from [CarrierNetworkListener.onCarrierNetworkChange] */ - val carrierNetworkChangeActive: Boolean = false, - - /** Fields below from [DisplayInfoListener.onDisplayInfoChanged]. */ - - /** - * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or - * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon - */ - val resolvedNetworkType: ResolvedNetworkType = ResolvedNetworkType.UnknownNetworkType, -) : Diffable<MobileConnectionModel> { - override fun logDiffs(prevVal: MobileConnectionModel, row: TableRowLogger) { - if (prevVal.dataConnectionState != dataConnectionState) { - row.logChange(COL_CONNECTION_STATE, dataConnectionState.name) - } - - if (prevVal.isEmergencyOnly != isEmergencyOnly) { - row.logChange(COL_EMERGENCY, isEmergencyOnly) - } - - if (prevVal.isRoaming != isRoaming) { - row.logChange(COL_ROAMING, isRoaming) - } - - if (prevVal.operatorAlphaShort != operatorAlphaShort) { - row.logChange(COL_OPERATOR, operatorAlphaShort) - } - - if (prevVal.isInService != isInService) { - row.logChange(COL_IS_IN_SERVICE, isInService) - } - - if (prevVal.isGsm != isGsm) { - row.logChange(COL_IS_GSM, isGsm) - } - - if (prevVal.cdmaLevel != cdmaLevel) { - row.logChange(COL_CDMA_LEVEL, cdmaLevel) - } - - if (prevVal.primaryLevel != primaryLevel) { - row.logChange(COL_PRIMARY_LEVEL, primaryLevel) - } - - if (prevVal.dataActivityDirection.hasActivityIn != dataActivityDirection.hasActivityIn) { - row.logChange(COL_ACTIVITY_DIRECTION_IN, dataActivityDirection.hasActivityIn) - } - - if (prevVal.dataActivityDirection.hasActivityOut != dataActivityDirection.hasActivityOut) { - row.logChange(COL_ACTIVITY_DIRECTION_OUT, dataActivityDirection.hasActivityOut) - } - - if (prevVal.carrierNetworkChangeActive != carrierNetworkChangeActive) { - row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive) - } - - if (prevVal.resolvedNetworkType != resolvedNetworkType) { - row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString()) - } - } - - override fun logFull(row: TableRowLogger) { - row.logChange(COL_CONNECTION_STATE, dataConnectionState.name) - row.logChange(COL_EMERGENCY, isEmergencyOnly) - row.logChange(COL_ROAMING, isRoaming) - row.logChange(COL_OPERATOR, operatorAlphaShort) - row.logChange(COL_IS_IN_SERVICE, isInService) - row.logChange(COL_IS_GSM, isGsm) - row.logChange(COL_CDMA_LEVEL, cdmaLevel) - row.logChange(COL_PRIMARY_LEVEL, primaryLevel) - row.logChange(COL_ACTIVITY_DIRECTION_IN, dataActivityDirection.hasActivityIn) - row.logChange(COL_ACTIVITY_DIRECTION_OUT, dataActivityDirection.hasActivityOut) - row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive) - row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString()) - } - - @VisibleForTesting - companion object { - const val COL_EMERGENCY = "EmergencyOnly" - const val COL_ROAMING = "Roaming" - const val COL_OPERATOR = "OperatorName" - const val COL_IS_IN_SERVICE = "IsInService" - const val COL_IS_GSM = "IsGsm" - const val COL_CDMA_LEVEL = "CdmaLevel" - const val COL_PRIMARY_LEVEL = "PrimaryLevel" - const val COL_CONNECTION_STATE = "ConnectionState" - const val COL_ACTIVITY_DIRECTION_IN = "DataActivity.In" - const val COL_ACTIVITY_DIRECTION_OUT = "DataActivity.Out" - const val COL_CARRIER_NETWORK_CHANGE = "CarrierNetworkChangeActive" - const val COL_RESOLVED_NETWORK_TYPE = "NetworkType" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt index 5562e73f0478..cf7a313a4cb1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt @@ -17,8 +17,12 @@ package com.android.systemui.statusbar.pipeline.mobile.data.model import android.telephony.Annotation.NetworkType +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy /** @@ -26,11 +30,19 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy * on whether or not the display info contains an override type, we may have to call different * methods on [MobileMappingsProxy] to generate an icon lookup key. */ -sealed interface ResolvedNetworkType { +sealed interface ResolvedNetworkType : Diffable<ResolvedNetworkType> { val lookupKey: String + override fun logDiffs(prevVal: ResolvedNetworkType, row: TableRowLogger) { + if (prevVal != this) { + row.logChange(COL_NETWORK_TYPE, this.toString()) + } + } + object UnknownNetworkType : ResolvedNetworkType { - override val lookupKey: String = "unknown" + override val lookupKey: String = MobileMappings.toIconKey(NETWORK_TYPE_UNKNOWN) + + override fun toString(): String = "Unknown" } data class DefaultNetworkType( @@ -47,5 +59,11 @@ sealed interface ResolvedNetworkType { override val lookupKey: String = "cwf" val iconGroupOverride: SignalIcon.MobileIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI + + override fun toString(): String = "CarrierMerged" + } + + companion object { + private const val COL_NETWORK_TYPE = "networkType" } } 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/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 6187f64e011d..90c32dc08045 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -17,11 +17,12 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.telephony.SubscriptionInfo -import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.flow.StateFlow /** @@ -45,11 +46,57 @@ interface MobileConnectionRepository { */ val tableLogBuffer: TableLogBuffer + /** True if the [android.telephony.ServiceState] says this connection is emergency calls only */ + val isEmergencyOnly: StateFlow<Boolean> + + /** True if [android.telephony.ServiceState] says we are roaming */ + val isRoaming: StateFlow<Boolean> + + /** + * See [android.telephony.ServiceState.getOperatorAlphaShort], this value is defined as the + * current registered operator name in short alphanumeric format. In some cases this name might + * be preferred over other methods of calculating the network name + */ + val operatorAlphaShort: StateFlow<String?> + + /** + * TODO (b/263167683): Clarify this field + * + * This check comes from [com.android.settingslib.Utils.isInService]. It is intended to be a + * mapping from a ServiceState to a notion of connectivity. Notably, it will consider a + * connection to be in-service if either the voice registration state is IN_SERVICE or the data + * registration state is IN_SERVICE and NOT IWLAN. + */ + val isInService: StateFlow<Boolean> + + /** True if [android.telephony.SignalStrength] told us that this connection is using GSM */ + val isGsm: StateFlow<Boolean> + + /** + * There is still specific logic in the pipeline that calls out CDMA level explicitly. This + * field is not completely orthogonal to [primaryLevel], because CDMA could be primary. + */ + // @IntRange(from = 0, to = 4) + val cdmaLevel: StateFlow<Int> + + /** [android.telephony.SignalStrength]'s concept of the overall signal level */ + // @IntRange(from = 0, to = 4) + val primaryLevel: StateFlow<Int> + + /** The current data connection state. See [DataConnectionState] */ + val dataConnectionState: StateFlow<DataConnectionState> + + /** The current data activity direction. See [DataActivityModel] */ + val dataActivityDirection: StateFlow<DataActivityModel> + + /** True if there is currently a carrier network change in process */ + val carrierNetworkChangeActive: StateFlow<Boolean> + /** - * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single - * listener + model. + * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or + * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon */ - val connectionInfo: StateFlow<MobileConnectionModel> + val resolvedNetworkType: StateFlow<ResolvedNetworkType> /** The total number of levels. Used with [SignalDrawable]. */ val numberOfLevels: StateFlow<Int> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt new file mode 100644 index 000000000000..809772eec2f0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt @@ -0,0 +1,231 @@ +/* + * 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.data.repository.demo + +import android.telephony.CellSignalStrength +import android.telephony.TelephonyManager +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_CARRIER_NETWORK_CHANGE +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_CDMA_LEVEL +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_EMERGENCY +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_GSM +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_IN_SERVICE +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_ROAMING +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn + +/** + * Demo version of [MobileConnectionRepository]. Note that this class shares all of its flows using + * [SharingStarted.WhileSubscribed()] to give the same semantics as using a regular + * [MutableStateFlow] while still logging all of the inputs in the same manor as the production + * repos. + */ +class DemoMobileConnectionRepository( + override val subId: Int, + override val tableLogBuffer: TableLogBuffer, + val scope: CoroutineScope, +) : MobileConnectionRepository { + private val _isEmergencyOnly = MutableStateFlow(false) + override val isEmergencyOnly = + _isEmergencyOnly + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_EMERGENCY, + _isEmergencyOnly.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _isEmergencyOnly.value) + + private val _isRoaming = MutableStateFlow(false) + override val isRoaming = + _isRoaming + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_ROAMING, + _isRoaming.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _isRoaming.value) + + private val _operatorAlphaShort: MutableStateFlow<String?> = MutableStateFlow(null) + override val operatorAlphaShort = + _operatorAlphaShort + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_OPERATOR, + _operatorAlphaShort.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _operatorAlphaShort.value) + + private val _isInService = MutableStateFlow(false) + override val isInService = + _isInService + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_IS_IN_SERVICE, + _isInService.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _isInService.value) + + private val _isGsm = MutableStateFlow(false) + override val isGsm = + _isGsm + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_IS_GSM, + _isGsm.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _isGsm.value) + + private val _cdmaLevel = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + override val cdmaLevel = + _cdmaLevel + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_CDMA_LEVEL, + _cdmaLevel.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _cdmaLevel.value) + + private val _primaryLevel = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + override val primaryLevel = + _primaryLevel + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_PRIMARY_LEVEL, + _primaryLevel.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _primaryLevel.value) + + private val _dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected) + override val dataConnectionState = + _dataConnectionState + .logDiffsForTable(tableLogBuffer, columnPrefix = "", _dataConnectionState.value) + .stateIn(scope, SharingStarted.WhileSubscribed(), _dataConnectionState.value) + + private val _dataActivityDirection = + MutableStateFlow( + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ) + ) + override val dataActivityDirection = + _dataActivityDirection + .logDiffsForTable(tableLogBuffer, columnPrefix = "", _dataActivityDirection.value) + .stateIn(scope, SharingStarted.WhileSubscribed(), _dataActivityDirection.value) + + private val _carrierNetworkChangeActive = MutableStateFlow(false) + override val carrierNetworkChangeActive = + _carrierNetworkChangeActive + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_CARRIER_NETWORK_CHANGE, + _carrierNetworkChangeActive.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _carrierNetworkChangeActive.value) + + private val _resolvedNetworkType: MutableStateFlow<ResolvedNetworkType> = + MutableStateFlow(ResolvedNetworkType.UnknownNetworkType) + override val resolvedNetworkType = + _resolvedNetworkType + .logDiffsForTable(tableLogBuffer, columnPrefix = "", _resolvedNetworkType.value) + .stateIn(scope, SharingStarted.WhileSubscribed(), _resolvedNetworkType.value) + + override val numberOfLevels = MutableStateFlow(MobileConnectionRepository.DEFAULT_NUM_LEVELS) + + override val dataEnabled = MutableStateFlow(true) + + override val cdmaRoaming = MutableStateFlow(false) + + override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived("demo network")) + + /** + * Process a new demo mobile event. Note that [resolvedNetworkType] must be passed in separately + * from the event, due to the requirement to reverse the mobile mappings lookup in the top-level + * repository. + */ + fun processDemoMobileEvent( + event: FakeNetworkEventModel.Mobile, + resolvedNetworkType: ResolvedNetworkType, + ) { + // This is always true here, because we split out disabled states at the data-source level + dataEnabled.value = true + networkName.value = NetworkNameModel.IntentDerived(event.name) + + cdmaRoaming.value = event.roaming + _isRoaming.value = event.roaming + // TODO(b/261029387): not yet supported + _isEmergencyOnly.value = false + _operatorAlphaShort.value = event.name + _isInService.value = (event.level ?: 0) > 0 + // TODO(b/261029387): not yet supported + _isGsm.value = false + _cdmaLevel.value = event.level ?: 0 + _primaryLevel.value = event.level ?: 0 + // TODO(b/261029387): not yet supported + _dataConnectionState.value = DataConnectionState.Connected + _dataActivityDirection.value = + (event.activity ?: TelephonyManager.DATA_ACTIVITY_NONE).toMobileDataActivityModel() + _carrierNetworkChangeActive.value = event.carrierNetworkChange + _resolvedNetworkType.value = resolvedNetworkType + } + + fun processCarrierMergedEvent(event: FakeWifiEventModel.CarrierMerged) { + // This is always true here, because we split out disabled states at the data-source level + dataEnabled.value = true + networkName.value = NetworkNameModel.IntentDerived(CARRIER_MERGED_NAME) + numberOfLevels.value = event.numberOfLevels + cdmaRoaming.value = false + _primaryLevel.value = event.level + _cdmaLevel.value = event.level + _dataActivityDirection.value = event.activity.toMobileDataActivityModel() + + // These fields are always the same for carrier-merged networks + _resolvedNetworkType.value = ResolvedNetworkType.CarrierMergedNetworkType + _dataConnectionState.value = DataConnectionState.Connected + _isRoaming.value = false + _isEmergencyOnly.value = false + _operatorAlphaShort.value = null + _isInService.value = true + _isGsm.value = false + _carrierNetworkChangeActive.value = false + } + + companion object { + private const val CARRIER_MERGED_NAME = "Carrier Merged Network" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt index e92483232186..3cafb7377260 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -18,30 +18,22 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo import android.content.Context import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID -import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE import android.util.Log import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory -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.MobileConnectivityModel -import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel 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.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled -import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.CarrierMergedConnectionRepository.Companion.createCarrierMergedConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE -import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import javax.inject.Inject @@ -183,7 +175,7 @@ constructor( private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer { val tableLogBuffer = logFactory.getOrCreate( - "DemoMobileConnectionLog [$subId]", + "DemoMobileConnectionLog[$subId]", MOBILE_CONNECTION_BUFFER_SIZE, ) @@ -191,6 +183,7 @@ constructor( DemoMobileConnectionRepository( subId, tableLogBuffer, + scope, ) return CacheContainer(repo, lastMobileState = null) } @@ -237,23 +230,18 @@ constructor( } } - private fun processEnabledMobileState(state: Mobile) { + private fun processEnabledMobileState(event: Mobile) { // get or create the connection repo, and set its values - val subId = state.subId ?: DEFAULT_SUB_ID + val subId = event.subId ?: DEFAULT_SUB_ID maybeCreateSubscription(subId) val connection = getRepoForSubId(subId) - connectionRepoCache[subId]?.lastMobileState = state + connectionRepoCache[subId]?.lastMobileState = event // TODO(b/261029387): until we have a command, use the most recent subId defaultDataSubId.value = subId - // This is always true here, because we split out disabled states at the data-source level - connection.dataEnabled.value = true - connection.networkName.value = NetworkNameModel.IntentDerived(state.name) - - connection.cdmaRoaming.value = state.roaming - connection.connectionInfo.value = state.toMobileConnectionModel() + connection.processDemoMobileEvent(event, event.dataType.toResolvedNetworkType()) } private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) { @@ -272,13 +260,7 @@ constructor( defaultDataSubId.value = subId val connection = getRepoForSubId(subId) - // This is always true here, because we split out disabled states at the data-source level - connection.dataEnabled.value = true - connection.networkName.value = NetworkNameModel.IntentDerived(CARRIER_MERGED_NAME) - connection.numberOfLevels.value = event.numberOfLevels - connection.cdmaRoaming.value = false - connection.connectionInfo.value = event.toMobileConnectionModel() - Log.e("CCS", "output connection info = ${connection.connectionInfo.value}") + connection.processCarrierMergedEvent(event) } private fun maybeRemoveSubscription(subId: Int?) { @@ -332,29 +314,6 @@ constructor( private fun subIdsString(): String = _subscriptions.value.joinToString(",") { it.subscriptionId.toString() } - private fun Mobile.toMobileConnectionModel(): MobileConnectionModel { - return MobileConnectionModel( - isEmergencyOnly = false, // TODO(b/261029387): not yet supported - isRoaming = roaming, - isInService = (level ?: 0) > 0, - isGsm = false, // TODO(b/261029387): not yet supported - cdmaLevel = level ?: 0, - primaryLevel = level ?: 0, - dataConnectionState = - DataConnectionState.Connected, // TODO(b/261029387): not yet supported - dataActivityDirection = (activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel(), - carrierNetworkChangeActive = carrierNetworkChange, - resolvedNetworkType = dataType.toResolvedNetworkType() - ) - } - - private fun FakeWifiEventModel.CarrierMerged.toMobileConnectionModel(): MobileConnectionModel { - return createCarrierMergedConnectionModel( - this.level, - activity.toMobileDataActivityModel(), - ) - } - private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType { val key = mobileMappingsReverseLookup.value[this] ?: "dis" return DefaultNetworkType(key) @@ -364,8 +323,6 @@ constructor( private const val TAG = "DemoMobileConnectionsRepo" private const val DEFAULT_SUB_ID = 1 - - private const val CARRIER_MERGED_NAME = "Carrier Merged Network" } } @@ -374,18 +331,3 @@ class CacheContainer( /** The last received [Mobile] event. Used when switching from carrier merged back to mobile. */ var lastMobileState: Mobile?, ) - -class DemoMobileConnectionRepository( - override val subId: Int, - override val tableLogBuffer: TableLogBuffer, -) : MobileConnectionRepository { - override val connectionInfo = MutableStateFlow(MobileConnectionModel()) - - override val numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS) - - override val dataEnabled = MutableStateFlow(true) - - override val cdmaRoaming = MutableStateFlow(false) - - override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived("demo network")) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt index 8f6a87b089f2..94d6d0b1db44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt @@ -16,18 +16,17 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod +import android.telephony.CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN import android.telephony.TelephonyManager import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBuffer 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 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType 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.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import javax.inject.Inject @@ -94,16 +93,6 @@ class CarrierMergedConnectionRepository( } } - override val connectionInfo: StateFlow<MobileConnectionModel> = - combine(network, wifiRepository.wifiActivity) { network, activity -> - if (network == null) { - MobileConnectionModel() - } else { - createCarrierMergedConnectionModel(network.level, activity) - } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel()) - override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(ROAMING).asStateFlow() override val networkName: StateFlow<NetworkNameModel> = @@ -129,34 +118,54 @@ class CarrierMergedConnectionRepository( } .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS) - override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled + override val primaryLevel = + network + .map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN } + .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) - companion object { - /** - * Creates an instance of [MobileConnectionModel] that represents a carrier merged network - * with the given [level] and [activity]. - */ - fun createCarrierMergedConnectionModel( - level: Int, - activity: DataActivityModel, - ): MobileConnectionModel { - return MobileConnectionModel( - primaryLevel = level, - cdmaLevel = level, - dataActivityDirection = activity, - // Here and below: These values are always the same for every carrier-merged - // connection. - resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType, - dataConnectionState = DataConnectionState.Connected, - isRoaming = ROAMING, - isEmergencyOnly = false, - operatorAlphaShort = null, - isInService = true, - isGsm = false, - carrierNetworkChangeActive = false, + override val cdmaLevel = + network + .map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN } + .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + + override val dataActivityDirection = wifiRepository.wifiActivity + + override val resolvedNetworkType = + network + .map { + if (it != null) { + ResolvedNetworkType.CarrierMergedNetworkType + } else { + ResolvedNetworkType.UnknownNetworkType + } + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + ResolvedNetworkType.UnknownNetworkType ) - } + override val dataConnectionState = + network + .map { + if (it != null) { + DataConnectionState.Connected + } else { + DataConnectionState.Disconnected + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), DataConnectionState.Disconnected) + + override val isRoaming = MutableStateFlow(false).asStateFlow() + override val isEmergencyOnly = MutableStateFlow(false).asStateFlow() + override val operatorAlphaShort = MutableStateFlow(null).asStateFlow() + override val isInService = MutableStateFlow(true).asStateFlow() + override val isGsm = MutableStateFlow(false).asStateFlow() + override val carrierNetworkChangeActive = MutableStateFlow(false).asStateFlow() + + override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled + + companion object { // Carrier merged is never roaming private const val ROAMING = false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt index a39ea0abce5a..b3737ecd1e0b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -114,15 +114,147 @@ class FullMobileConnectionRepository( .flatMapLatest { it.cdmaRoaming } .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaRoaming.value) - override val connectionInfo = + override val isEmergencyOnly = activeRepo - .flatMapLatest { it.connectionInfo } + .flatMapLatest { it.isEmergencyOnly } .logDiffsForTable( tableLogBuffer, columnPrefix = "", - initialValue = activeRepo.value.connectionInfo.value, + columnName = COL_EMERGENCY, + activeRepo.value.isEmergencyOnly.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.isEmergencyOnly.value + ) + + override val isRoaming = + activeRepo + .flatMapLatest { it.isRoaming } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_ROAMING, + activeRepo.value.isRoaming.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isRoaming.value) + + override val operatorAlphaShort = + activeRepo + .flatMapLatest { it.operatorAlphaShort } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_OPERATOR, + activeRepo.value.operatorAlphaShort.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.operatorAlphaShort.value + ) + + override val isInService = + activeRepo + .flatMapLatest { it.isInService } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_IS_IN_SERVICE, + activeRepo.value.isInService.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isInService.value) + + override val isGsm = + activeRepo + .flatMapLatest { it.isGsm } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_IS_GSM, + activeRepo.value.isGsm.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isGsm.value) + + override val cdmaLevel = + activeRepo + .flatMapLatest { it.cdmaLevel } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_CDMA_LEVEL, + activeRepo.value.cdmaLevel.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaLevel.value) + + override val primaryLevel = + activeRepo + .flatMapLatest { it.primaryLevel } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_PRIMARY_LEVEL, + activeRepo.value.primaryLevel.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.primaryLevel.value) + + override val dataConnectionState = + activeRepo + .flatMapLatest { it.dataConnectionState } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + activeRepo.value.dataConnectionState.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.dataConnectionState.value + ) + + override val dataActivityDirection = + activeRepo + .flatMapLatest { it.dataActivityDirection } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + activeRepo.value.dataActivityDirection.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.dataActivityDirection.value + ) + + override val carrierNetworkChangeActive = + activeRepo + .flatMapLatest { it.carrierNetworkChangeActive } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_CARRIER_NETWORK_CHANGE, + activeRepo.value.carrierNetworkChangeActive.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.carrierNetworkChangeActive.value + ) + + override val resolvedNetworkType = + activeRepo + .flatMapLatest { it.resolvedNetworkType } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + activeRepo.value.resolvedNetworkType.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.resolvedNetworkType.value ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.connectionInfo.value) override val dataEnabled = activeRepo @@ -187,4 +319,15 @@ class FullMobileConnectionRepository( fun tableBufferLogName(subId: Int): String = "MobileConnectionLog[$subId]" } } + + companion object { + const val COL_EMERGENCY = "emergencyOnly" + const val COL_ROAMING = "roaming" + const val COL_OPERATOR = "operatorName" + const val COL_IS_IN_SERVICE = "isInService" + const val COL_IS_GSM = "isGsm" + const val COL_CDMA_LEVEL = "cdmaLevel" + const val COL_PRIMARY_LEVEL = "primaryLevel" + const val COL_CARRIER_NETWORK_CHANGE = "carrierNetworkChangeActive" + } } 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..f1fc3868d690 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 @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod import android.content.Context import android.content.IntentFilter import android.telephony.CellSignalStrength +import android.telephony.CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN import android.telephony.CellSignalStrengthCdma import android.telephony.ServiceState import android.telephony.SignalStrength @@ -36,7 +37,8 @@ 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.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType @@ -47,8 +49,8 @@ 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.DataActivityModel import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -62,10 +64,10 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn @@ -165,83 +167,100 @@ class MobileConnectionRepositoryImpl( } .shareIn(scope, SharingStarted.WhileSubscribed()) - private fun updateConnectionState( - prevState: MobileConnectionModel, - callbackEvent: CallbackEvent, - ): MobileConnectionModel = - when (callbackEvent) { - is CallbackEvent.OnServiceStateChanged -> { - val serviceState = callbackEvent.serviceState - prevState.copy( - isEmergencyOnly = serviceState.isEmergencyOnly, - isRoaming = serviceState.roaming, - operatorAlphaShort = serviceState.operatorAlphaShort, - isInService = Utils.isInService(serviceState), - ) - } - is CallbackEvent.OnSignalStrengthChanged -> { - val signalStrength = callbackEvent.signalStrength - val cdmaLevel = - signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java).let { - strengths -> - if (!strengths.isEmpty()) { - strengths[0].level - } else { - CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN - } - } + override val isEmergencyOnly = + callbackEvents + .filterIsInstance<CallbackEvent.OnServiceStateChanged>() + .map { it.serviceState.isEmergencyOnly } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) - val primaryLevel = signalStrength.level + override val isRoaming = + callbackEvents + .filterIsInstance<CallbackEvent.OnServiceStateChanged>() + .map { it.serviceState.roaming } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) - prevState.copy( - cdmaLevel = cdmaLevel, - primaryLevel = primaryLevel, - isGsm = signalStrength.isGsm, - ) - } - is CallbackEvent.OnDataConnectionStateChanged -> { - prevState.copy(dataConnectionState = callbackEvent.dataState.toDataConnectionType()) - } - is CallbackEvent.OnDataActivity -> { - prevState.copy( - dataActivityDirection = callbackEvent.direction.toMobileDataActivityModel() - ) - } - is CallbackEvent.OnCarrierNetworkChange -> { - prevState.copy(carrierNetworkChangeActive = callbackEvent.active) - } - is CallbackEvent.OnDisplayInfoChanged -> { - val telephonyDisplayInfo = callbackEvent.telephonyDisplayInfo - val networkType = - if (telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) { - UnknownNetworkType - } else if ( - telephonyDisplayInfo.overrideNetworkType == OVERRIDE_NETWORK_TYPE_NONE - ) { - DefaultNetworkType( - mobileMappingsProxy.toIconKey(telephonyDisplayInfo.networkType) - ) + override val operatorAlphaShort = + callbackEvents + .filterIsInstance<CallbackEvent.OnServiceStateChanged>() + .map { it.serviceState.operatorAlphaShort } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val isInService = + callbackEvents + .filterIsInstance<CallbackEvent.OnServiceStateChanged>() + .map { Utils.isInService(it.serviceState) } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val isGsm = + callbackEvents + .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>() + .map { it.signalStrength.isGsm } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val cdmaLevel = + callbackEvents + .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>() + .map { + it.signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java).let { + strengths -> + if (strengths.isNotEmpty()) { + strengths[0].level } else { - OverrideNetworkType( - mobileMappingsProxy.toIconKeyOverride( - telephonyDisplayInfo.overrideNetworkType - ) - ) + CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN } - prevState.copy(resolvedNetworkType = networkType) - } - is CallbackEvent.OnDataEnabledChanged -> { - // Not part of this object, handled in a separate flow - prevState + } } - } + .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) - override val connectionInfo = run { - val initial = MobileConnectionModel() + override val primaryLevel = callbackEvents - .scan(initial, ::updateConnectionState) - .stateIn(scope, SharingStarted.WhileSubscribed(), initial) - } + .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>() + .map { it.signalStrength.level } + .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + + override val dataConnectionState = + callbackEvents + .filterIsInstance<CallbackEvent.OnDataConnectionStateChanged>() + .map { it.dataState.toDataConnectionType() } + .stateIn(scope, SharingStarted.WhileSubscribed(), Disconnected) + + override val dataActivityDirection = + callbackEvents + .filterIsInstance<CallbackEvent.OnDataActivity>() + .map { it.direction.toMobileDataActivityModel() } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + DataActivityModel(hasActivityIn = false, hasActivityOut = false) + ) + + override val carrierNetworkChangeActive = + callbackEvents + .filterIsInstance<CallbackEvent.OnCarrierNetworkChange>() + .map { it.active } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val resolvedNetworkType = + callbackEvents + .filterIsInstance<CallbackEvent.OnDisplayInfoChanged>() + .map { + if (it.telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) { + UnknownNetworkType + } else if ( + it.telephonyDisplayInfo.overrideNetworkType == OVERRIDE_NETWORK_TYPE_NONE + ) { + DefaultNetworkType( + mobileMappingsProxy.toIconKey(it.telephonyDisplayInfo.networkType) + ) + } else { + OverrideNetworkType( + mobileMappingsProxy.toIconKeyOverride( + it.telephonyDisplayInfo.overrideNetworkType + ) + ) + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), UnknownNetworkType) override val numberOfLevels = systemUiCarrierConfig.shouldInflateSignalStrength 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..b7da3f27c70a 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 @@ -266,6 +266,7 @@ constructor( val callback = object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { override fun onLost(network: Network) { + logger.logOnLost(network, isDefaultNetworkCallback = true) // Send a disconnected model when lost. Maybe should create a sealed // type or null here? trySend(MobileConnectivityModel()) @@ -275,6 +276,11 @@ constructor( network: Network, caps: NetworkCapabilities ) { + logger.logOnCapabilitiesChanged( + network, + caps, + isDefaultNetworkCallback = true, + ) trySend( MobileConnectivityModel( isConnected = caps.hasTransport(TRANSPORT_CELLULAR), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 4caf2b09a3f2..7df6764fda1a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -133,11 +134,9 @@ class MobileIconInteractorImpl( override val isForceHidden: Flow<Boolean>, connectionRepository: MobileConnectionRepository, ) : MobileIconInteractor { - private val connectionInfo = connectionRepository.connectionInfo - override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer - override val activity = connectionInfo.mapLatest { it.dataActivityDirection } + override val activity = connectionRepository.dataActivityDirection override val isConnected: Flow<Boolean> = defaultMobileConnectivity.mapLatest { it.isConnected } @@ -155,11 +154,11 @@ class MobileIconInteractorImpl( override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled override val networkName = - combine(connectionInfo, connectionRepository.networkName) { connection, networkName -> - if ( - networkName is NetworkNameModel.Default && connection.operatorAlphaShort != null - ) { - NetworkNameModel.IntentDerived(connection.operatorAlphaShort) + combine(connectionRepository.operatorAlphaShort, connectionRepository.networkName) { + operatorAlphaShort, + networkName -> + if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) { + NetworkNameModel.IntentDerived(operatorAlphaShort) } else { networkName } @@ -173,19 +172,19 @@ class MobileIconInteractorImpl( /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ override val networkTypeIconGroup: StateFlow<MobileIconGroup> = combine( - connectionInfo, + connectionRepository.resolvedNetworkType, defaultMobileIconMapping, defaultMobileIconGroup, isDefault, - ) { info, mapping, defaultGroup, isDefault -> + ) { resolvedNetworkType, mapping, defaultGroup, isDefault -> if (!isDefault) { return@combine NOT_DEFAULT_DATA } - when (info.resolvedNetworkType) { + when (resolvedNetworkType) { is ResolvedNetworkType.CarrierMergedNetworkType -> - info.resolvedNetworkType.iconGroupOverride - else -> mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup + resolvedNetworkType.iconGroupOverride + else -> mapping[resolvedNetworkType.lookupKey] ?: defaultGroup } } .distinctUntilChanged() @@ -200,17 +199,19 @@ class MobileIconInteractorImpl( } .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value) - override val isEmergencyOnly: StateFlow<Boolean> = - connectionInfo - .mapLatest { it.isEmergencyOnly } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isEmergencyOnly = connectionRepository.isEmergencyOnly override val isRoaming: StateFlow<Boolean> = - combine(connectionInfo, connectionRepository.cdmaRoaming) { connection, cdmaRoaming -> - if (connection.carrierNetworkChangeActive) { + combine( + connectionRepository.carrierNetworkChangeActive, + connectionRepository.isGsm, + connectionRepository.isRoaming, + connectionRepository.cdmaRoaming, + ) { carrierNetworkChangeActive, isGsm, isRoaming, cdmaRoaming -> + if (carrierNetworkChangeActive) { false - } else if (connection.isGsm) { - connection.isRoaming + } else if (isGsm) { + isRoaming } else { cdmaRoaming } @@ -218,12 +219,17 @@ class MobileIconInteractorImpl( .stateIn(scope, SharingStarted.WhileSubscribed(), false) override val level: StateFlow<Int> = - combine(connectionInfo, alwaysUseCdmaLevel) { connection, alwaysUseCdmaLevel -> + combine( + connectionRepository.isGsm, + connectionRepository.primaryLevel, + connectionRepository.cdmaLevel, + alwaysUseCdmaLevel, + ) { isGsm, primaryLevel, cdmaLevel, alwaysUseCdmaLevel -> when { // GSM connections should never use the CDMA level - connection.isGsm -> connection.primaryLevel - alwaysUseCdmaLevel -> connection.cdmaLevel - else -> connection.primaryLevel + isGsm -> primaryLevel + alwaysUseCdmaLevel -> cdmaLevel + else -> primaryLevel } } .stateIn(scope, SharingStarted.WhileSubscribed(), 0) @@ -236,12 +242,9 @@ class MobileIconInteractorImpl( ) override val isDataConnected: StateFlow<Boolean> = - connectionInfo - .mapLatest { connection -> connection.dataConnectionState == Connected } + connectionRepository.dataConnectionState + .map { it == Connected } .stateIn(scope, SharingStarted.WhileSubscribed(), false) - override val isInService = - connectionRepository.connectionInfo - .mapLatest { it.isInService } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isInService = connectionRepository.isInService } 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/LoggerHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt index 6f29e33b5a17..a96e8ff20dd4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt @@ -38,11 +38,24 @@ object LoggerHelper { int1 = network.getNetId() str1 = networkCapabilities.toString() }, - { "onCapabilitiesChanged[default=$bool1]: net=$int1 capabilities=$str1" } + { "on${if (bool1) "Default" else ""}CapabilitiesChanged: net=$int1 capabilities=$str1" } ) } - fun logOnLost(buffer: LogBuffer, tag: String, network: Network) { - buffer.log(tag, LogLevel.INFO, { int1 = network.getNetId() }, { "onLost: net=$int1" }) + fun logOnLost( + buffer: LogBuffer, + tag: String, + network: Network, + isDefaultNetworkCallback: Boolean, + ) { + buffer.log( + tag, + LogLevel.INFO, + { + int1 = network.getNetId() + bool1 = isDefaultNetworkCallback + }, + { "on${if (bool1) "Default" else ""}Lost: net=$int1" } + ) } } 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/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index ee58160a7d3b..b5e7b7a13505 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -128,6 +128,7 @@ constructor( } override fun onLost(network: Network) { + logger.logOnLost(network, isDefaultNetworkCallback = true) // The system no longer has a default network, so wifi is definitely not // default. trySend(false) @@ -179,7 +180,7 @@ constructor( } override fun onLost(network: Network) { - logger.logOnLost(network) + logger.logOnLost(network, isDefaultNetworkCallback = false) wifiNetworkChangeEvents.tryEmit(Unit) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt index a32e47592355..bb0b166f7aba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt @@ -48,8 +48,8 @@ constructor( ) } - fun logOnLost(network: Network) { - LoggerHelper.logOnLost(buffer, TAG, network) + fun logOnLost(network: Network, isDefaultNetworkCallback: Boolean) { + LoggerHelper.logOnLost(buffer, TAG, network, isDefaultNetworkCallback) } fun logIntent(intentName: String) { 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/statusbar/window/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java index 853de7b5ecab..bcf3b0cbfc86 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.window; -import static android.view.InsetsState.ITYPE_STATUS_BAR; -import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES; -import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT; +import static android.view.WindowInsets.Type.mandatorySystemGestures; +import static android.view.WindowInsets.Type.statusBars; +import static android.view.WindowInsets.Type.tappableElement; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR; @@ -85,6 +85,7 @@ public class StatusBarWindowController { private final ViewGroup mLaunchAnimationContainer; private WindowManager.LayoutParams mLp; private final WindowManager.LayoutParams mLpChanged; + private final Binder mInsetsSourceOwner = new Binder(); @Inject public StatusBarWindowController( @@ -231,16 +232,16 @@ public class StatusBarWindowController { lp.packageName = mContext.getPackageName(); lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; final InsetsFrameProvider gestureInsetsProvider = - new InsetsFrameProvider(ITYPE_TOP_MANDATORY_GESTURES); + new InsetsFrameProvider(mInsetsSourceOwner, 0, mandatorySystemGestures()); final int safeTouchRegionHeight = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.display_cutout_touchable_region_size); if (safeTouchRegionHeight > 0) { - gestureInsetsProvider.minimalInsetsSizeInDisplayCutoutSafe = - Insets.of(0, safeTouchRegionHeight, 0, 0); + gestureInsetsProvider.setMinimalInsetsSizeInDisplayCutoutSafe( + Insets.of(0, safeTouchRegionHeight, 0, 0)); } lp.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_STATUS_BAR), - new InsetsFrameProvider(ITYPE_TOP_TAPPABLE_ELEMENT), + new InsetsFrameProvider(mInsetsSourceOwner, 0, statusBars()), + new InsetsFrameProvider(mInsetsSourceOwner, 0, tappableElement()), gestureInsetsProvider }; return lp; 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/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 433642b7366c..94dd1b309436 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -33,6 +33,8 @@ import android.provider.Settings import android.util.Log import com.android.internal.logging.UiEventLogger import com.android.internal.util.UserIcons +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.R import com.android.systemui.SystemUISecondaryUserService import com.android.systemui.animation.Expandable @@ -93,6 +95,7 @@ constructor( @Application private val applicationScope: CoroutineScope, telephonyInteractor: TelephonyInteractor, broadcastDispatcher: BroadcastDispatcher, + keyguardUpdateMonitor: KeyguardUpdateMonitor, @Background private val backgroundDispatcher: CoroutineDispatcher, private val activityManager: ActivityManager, private val refreshUsersScheduler: RefreshUsersScheduler, @@ -290,6 +293,12 @@ constructor( val isSimpleUserSwitcher: Boolean get() = repository.isSimpleUserSwitcher() + val keyguardUpdateMonitorCallback = + object : KeyguardUpdateMonitorCallback() { + override fun onKeyguardGoingAway() { + dismissDialog() + } + } init { refreshUsersScheduler.refreshIfNotPaused() @@ -320,6 +329,7 @@ constructor( onBroadcastReceived(intent, previousSelectedUser) } .launchIn(applicationScope) + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) } fun addCallback(callback: UserCallback) { 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/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 531006da8210..565fc57e81b7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -263,9 +263,6 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { assertThat(viewFlipperConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID); assertThat(userSwitcherConstraint.layout.topToTop).isEqualTo(PARENT_ID); assertThat(userSwitcherConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID); - assertThat(userSwitcherConstraint.layout.bottomMargin).isEqualTo( - getContext().getResources().getDimensionPixelSize( - R.dimen.bouncer_user_switcher_y_trans)); assertThat(viewFlipperConstraint.layout.horizontalChainStyle).isEqualTo(CHAIN_SPREAD); assertThat(userSwitcherConstraint.layout.horizontalChainStyle).isEqualTo(CHAIN_SPREAD); assertThat(viewFlipperConstraint.layout.mHeight).isEqualTo(MATCH_CONSTRAINT); 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/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt index 28e80057a672..c98d5374311d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.FakeSelectedComponentRepository import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dump.DumpManager import com.android.systemui.settings.UserFileManager @@ -56,7 +57,6 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.`when` import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.inOrder @@ -66,6 +66,7 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import java.io.File import java.util.* @@ -108,6 +109,8 @@ class ControlsControllerImplTest : SysuiTestCase() { private lateinit var listingCallbackCaptor: ArgumentCaptor<ControlsListingController.ControlsListingCallback> + private val preferredPanelRepository = FakeSelectedComponentRepository() + private lateinit var delayableExecutor: FakeExecutor private lateinit var controller: ControlsControllerImpl private lateinit var canceller: DidRunRunnable @@ -168,6 +171,7 @@ class ControlsControllerImplTest : SysuiTestCase() { wrapper, delayableExecutor, uiController, + preferredPanelRepository, bindingController, listingController, userFileManager, @@ -221,6 +225,7 @@ class ControlsControllerImplTest : SysuiTestCase() { mContext, delayableExecutor, uiController, + preferredPanelRepository, bindingController, listingController, userFileManager, @@ -240,6 +245,7 @@ class ControlsControllerImplTest : SysuiTestCase() { mContext, delayableExecutor, uiController, + preferredPanelRepository, bindingController, listingController, userFileManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt index 7ac1953ee495..272f5895390c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt @@ -22,6 +22,8 @@ import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.util.FakeSharedPreferences @@ -40,6 +42,8 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker + private val featureFlags = FakeFeatureFlags() + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -48,6 +52,7 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { arrayOf<String>() ) whenever(userTracker.userId).thenReturn(0) + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, true) } @Test @@ -127,8 +132,25 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { assertThat(sharedPrefs.getStringSet(KEY, null)).isEmpty() } + @Test + fun testSetAuthorizedPackageAfterFeatureDisabled() { + mContext.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf(TEST_PACKAGE) + ) + val sharedPrefs = FakeSharedPreferences() + val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs)) + val repository = createRepository(fileManager) + + repository.removeAuthorizedPanels(setOf(TEST_PACKAGE)) + + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, false) + + assertThat(repository.getAuthorizedPanels()).isEqualTo(setOf(TEST_PACKAGE)) + } + private fun createRepository(userFileManager: UserFileManager): AuthorizedPanelsRepositoryImpl { - return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker) + return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker, featureFlags) } private class FakeUserFileManager(private val sharedPrefs: Map<Int, SharedPreferences>) : diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt new file mode 100644 index 000000000000..a7677cca9f29 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt @@ -0,0 +1,42 @@ +/* + * 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.controls.panels + +class FakeSelectedComponentRepository : SelectedComponentRepository { + + private var selectedComponent: SelectedComponentRepository.SelectedComponent? = null + private var shouldAddDefaultPanel: Boolean = true + + override fun getSelectedComponent(): SelectedComponentRepository.SelectedComponent? = + selectedComponent + + override fun setSelectedComponent( + selectedComponent: SelectedComponentRepository.SelectedComponent + ) { + this.selectedComponent = selectedComponent + } + + override fun removeSelectedComponent() { + selectedComponent = null + } + + override fun shouldAddDefaultComponent(): Boolean = shouldAddDefaultPanel + + override fun setShouldAddDefaultComponent(shouldAdd: Boolean) { + shouldAddDefaultPanel = shouldAdd + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt new file mode 100644 index 000000000000..0c7b9cb82b94 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt @@ -0,0 +1,163 @@ +/* + * 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.controls.panels + +import android.content.ComponentName +import android.content.SharedPreferences +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class SelectedComponentRepositoryTest : SysuiTestCase() { + + private companion object { + val COMPONENT_A = + SelectedComponentRepository.SelectedComponent( + name = "a", + componentName = ComponentName.unflattenFromString("pkg/.cls_a"), + isPanel = false, + ) + val COMPONENT_B = + SelectedComponentRepository.SelectedComponent( + name = "b", + componentName = ComponentName.unflattenFromString("pkg/.cls_b"), + isPanel = false, + ) + } + + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var userFileManager: UserFileManager + + private val featureFlags = FakeFeatureFlags() + private val sharedPreferences: SharedPreferences = FakeSharedPreferences() + + // under test + private lateinit var repository: SelectedComponentRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(userFileManager.getSharedPreferences(any(), any(), any())) + .thenReturn(sharedPreferences) + + repository = SelectedComponentRepositoryImpl(userFileManager, userTracker, featureFlags) + } + + @Test + fun testUnsetIsNull() { + assertThat(repository.getSelectedComponent()).isNull() + } + + @Test + fun testGetReturnsSet() { + repository.setSelectedComponent(COMPONENT_A) + + assertThat(repository.getSelectedComponent()).isEqualTo(COMPONENT_A) + } + + @Test + fun testSetOverrides() { + repository.setSelectedComponent(COMPONENT_A) + repository.setSelectedComponent(COMPONENT_B) + + assertThat(repository.getSelectedComponent()).isEqualTo(COMPONENT_B) + } + + @Test + fun testRemove() { + repository.setSelectedComponent(COMPONENT_A) + + repository.removeSelectedComponent() + + assertThat(repository.getSelectedComponent()).isNull() + } + + @Test + fun testFeatureEnabled_shouldAddDefaultPanelDefaultsToTrue() { + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, true) + + assertThat(repository.shouldAddDefaultComponent()).isTrue() + } + + @Test + fun testFeatureDisabled_shouldAddDefaultPanelDefaultsToTrue() { + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, false) + + assertThat(repository.shouldAddDefaultComponent()).isTrue() + } + + @Test + fun testFeatureEnabled_shouldAddDefaultPanelChecked() { + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, true) + repository.setShouldAddDefaultComponent(false) + + assertThat(repository.shouldAddDefaultComponent()).isFalse() + } + + @Test + fun testFeatureDisabled_shouldAlwaysAddDefaultPanelAlwaysTrue() { + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, false) + repository.setShouldAddDefaultComponent(false) + + assertThat(repository.shouldAddDefaultComponent()).isTrue() + } + + @Test + fun testGetPreferredStructure_differentUserId() { + sharedPreferences.savePanel(COMPONENT_A) + whenever( + userFileManager.getSharedPreferences( + DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + 0, + 1, + ) + ) + .thenReturn(FakeSharedPreferences().also { it.savePanel(COMPONENT_B) }) + + val previousPreferredStructure = repository.getSelectedComponent() + whenever(userTracker.userId).thenReturn(1) + val currentPreferredStructure = repository.getSelectedComponent() + + assertThat(previousPreferredStructure).isEqualTo(COMPONENT_A) + assertThat(currentPreferredStructure).isNotEqualTo(previousPreferredStructure) + assertThat(currentPreferredStructure).isEqualTo(COMPONENT_B) + } + + private fun SharedPreferences.savePanel(panel: SelectedComponentRepository.SelectedComponent) { + edit() + .putString("controls_component", panel.componentName?.flattenToString()) + .putString("controls_structure", panel.name) + .putBoolean("controls_is_panel", panel.isPanel) + .commit() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt index 7ecaca6c36d0..9d8084d4f2f4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt @@ -23,17 +23,19 @@ import android.content.pm.ApplicationInfo import android.content.pm.ServiceInfo import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.FakeSelectedComponentRepository import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import java.util.Optional import org.junit.Before @@ -53,16 +55,16 @@ class ControlsStartableTest : SysuiTestCase() { @Mock private lateinit var controlsController: ControlsController @Mock private lateinit var controlsListingController: ControlsListingController @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository + + private val preferredPanelsRepository = FakeSelectedComponentRepository() private lateinit var fakeExecutor: FakeExecutor @Before fun setUp() { MockitoAnnotations.initMocks(this) - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf<String>() - ) + whenever(authorizedPanelsRepository.getPreferredPackages()).thenReturn(setOf()) fakeExecutor = FakeExecutor(FakeSystemClock()) } @@ -87,10 +89,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testPreferredPackagesNotInstalled_noNewSelection() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf(TEST_PACKAGE_PANEL) - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) `when`(controlsListingController.getCurrentServices()).thenReturn(emptyList()) @@ -101,10 +101,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testPreferredPackageNotPanel_noNewSelection() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf(TEST_PACKAGE_PANEL) - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "not panel", hasPanel = false)) `when`(controlsListingController.getCurrentServices()).thenReturn(listings) @@ -116,10 +114,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testExistingSelection_noNewSelection() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf(TEST_PACKAGE_PANEL) - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()) .thenReturn(mock<SelectedItem.PanelItem>()) val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) @@ -132,10 +128,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testPanelAdded() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf(TEST_PACKAGE_PANEL) - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) `when`(controlsListingController.getCurrentServices()).thenReturn(listings) @@ -147,10 +141,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testMultiplePreferredOnlyOnePanel_panelAdded() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf("other_package", TEST_PACKAGE_PANEL) - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) val listings = listOf( @@ -166,10 +158,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testMultiplePreferredMultiplePanels_firstPreferredAdded() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf(TEST_PACKAGE_PANEL, "other_package") - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) val listings = listOf( @@ -217,6 +207,20 @@ class ControlsStartableTest : SysuiTestCase() { verify(controlsController, never()).bindComponentForPanel(any()) } + @Test + fun testAlreadyAddedPanel_noNewSelection() { + preferredPanelsRepository.setShouldAddDefaultComponent(false) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) + `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) + val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) + `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + + createStartable(enabled = true).start() + + verify(controlsController, never()).setPreferredSelection(any()) + } + private fun createStartable(enabled: Boolean): ControlsStartable { val component: ControlsComponent = mock() { @@ -230,7 +234,13 @@ class ControlsStartableTest : SysuiTestCase() { `when`(getControlsListingController()).thenReturn(Optional.empty()) } } - return ControlsStartable(context.resources, fakeExecutor, component, userTracker) + return ControlsStartable( + fakeExecutor, + component, + userTracker, + authorizedPanelsRepository, + preferredPanelsRepository, + ) } private fun ControlsServiceInfo( diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 23faa99c0b9d..3f61bf75740a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -42,16 +42,14 @@ import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.FakeSelectedComponentRepository +import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.settings.FakeControlsSettingsRepository import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker -import com.android.systemui.shade.ShadeController -import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.FakeSystemUIDialogController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -64,20 +62,18 @@ import com.android.systemui.util.time.FakeSystemClock import com.android.wm.shell.TaskView import com.android.wm.shell.TaskViewFactory import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.function.Consumer import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.anyString import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import java.util.Optional -import java.util.function.Consumer @SmallTest @RunWith(AndroidTestingRunner::class) @@ -87,11 +83,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var controlsListingController: ControlsListingController @Mock lateinit var controlActionCoordinator: ControlActionCoordinator @Mock lateinit var activityStarter: ActivityStarter - @Mock lateinit var shadeController: ShadeController @Mock lateinit var iconCache: CustomIconCache @Mock lateinit var controlsMetricsLogger: ControlsMetricsLogger @Mock lateinit var keyguardStateController: KeyguardStateController - @Mock lateinit var userFileManager: UserFileManager @Mock lateinit var userTracker: UserTracker @Mock lateinit var taskViewFactory: TaskViewFactory @Mock lateinit var dumpManager: DumpManager @@ -99,7 +93,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var featureFlags: FeatureFlags @Mock lateinit var packageManager: PackageManager - private val sharedPreferences = FakeSharedPreferences() + private val preferredPanelRepository = FakeSelectedComponentRepository() private val fakeDialogController = FakeSystemUIDialogController() private val uiExecutor = FakeExecutor(FakeSystemClock()) private val bgExecutor = FakeExecutor(FakeSystemClock()) @@ -138,94 +132,30 @@ class ControlsUiControllerImplTest : SysuiTestCase() { iconCache, controlsMetricsLogger, keyguardStateController, - userFileManager, userTracker, Optional.of(taskViewFactory), controlsSettingsRepository, authorizedPanelsRepository, + preferredPanelRepository, featureFlags, ControlsDialogsFactory { fakeDialogController.dialog }, dumpManager, ) - `when`( - userFileManager.getSharedPreferences( - DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, - 0, - 0 - ) - ) - .thenReturn(sharedPreferences) - `when`(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())) - .thenReturn(sharedPreferences) `when`(userTracker.userId).thenReturn(0) `when`(userTracker.userHandle).thenReturn(UserHandle.of(0)) } @Test - fun testGetPreferredStructure() { - val structureInfo = mock<StructureInfo>() - underTest.getPreferredSelectedItem(listOf(structureInfo)) - verify(userFileManager) - .getSharedPreferences( - fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, - mode = 0, - userId = 0 - ) - } - - @Test - fun testGetPreferredStructure_differentUserId() { - val selectedItems = - listOf( - SelectedItem.StructureItem( - StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()) - ), - SelectedItem.StructureItem( - StructureInfo(ComponentName.unflattenFromString("pkg/.cls2"), "b", ArrayList()) - ), - ) - val structures = selectedItems.map { it.structure } - sharedPreferences - .edit() - .putString("controls_component", selectedItems[0].componentName.flattenToString()) - .putString("controls_structure", selectedItems[0].name.toString()) - .commit() - - val differentSharedPreferences = FakeSharedPreferences() - differentSharedPreferences - .edit() - .putString("controls_component", selectedItems[1].componentName.flattenToString()) - .putString("controls_structure", selectedItems[1].name.toString()) - .commit() - - val previousPreferredStructure = underTest.getPreferredSelectedItem(structures) - - `when`( - userFileManager.getSharedPreferences( - DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, - 0, - 1 - ) - ) - .thenReturn(differentSharedPreferences) - `when`(userTracker.userId).thenReturn(1) - - val currentPreferredStructure = underTest.getPreferredSelectedItem(structures) - - assertThat(previousPreferredStructure).isEqualTo(selectedItems[0]) - assertThat(currentPreferredStructure).isEqualTo(selectedItems[1]) - assertThat(currentPreferredStructure).isNotEqualTo(previousPreferredStructure) - } - - @Test fun testGetPreferredPanel() { val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) - sharedPreferences - .edit() - .putString("controls_component", panel.componentName.flattenToString()) - .putString("controls_structure", panel.appName.toString()) - .putBoolean("controls_is_panel", true) - .commit() + + preferredPanelRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent( + name = panel.appName.toString(), + componentName = panel.componentName, + isPanel = true, + ) + ) val selected = underTest.getPreferredSelectedItem(emptyList()) @@ -369,11 +299,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()) ), ) - sharedPreferences - .edit() - .putString("controls_component", selectedItems[0].componentName.flattenToString()) - .putString("controls_structure", selectedItems[0].name.toString()) - .commit() + preferredPanelRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(selectedItems[0]) + ) assertThat(underTest.resolveActivity()) .isEqualTo(ControlsProviderSelectorActivity::class.java) @@ -418,12 +346,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { val componentName = ComponentName(context, "cls") whenever(controlsController.removeFavorites(eq(componentName))).thenReturn(true) val panel = SelectedItem.PanelItem("App name", componentName) - sharedPreferences - .edit() - .putString("controls_component", panel.componentName.flattenToString()) - .putString("controls_structure", panel.appName.toString()) - .putBoolean("controls_is_panel", true) - .commit() + preferredPanelRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(panel) + ) underTest.show(parent, {}, context) underTest.startRemovingApp(componentName, "Test App") @@ -432,11 +357,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() { verify(controlsController).removeFavorites(eq(componentName)) assertThat(underTest.getPreferredSelectedItem(emptyList())) .isEqualTo(SelectedItem.EMPTY_SELECTION) - with(sharedPreferences) { - assertThat(contains("controls_component")).isFalse() - assertThat(contains("controls_structure")).isFalse() - assertThat(contains("controls_is_panel")).isFalse() - } + assertThat(preferredPanelRepository.shouldAddDefaultComponent()).isFalse() + assertThat(preferredPanelRepository.getSelectedComponent()).isNull() } @Test @@ -452,12 +374,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo { val activity = ComponentName(context, "activity") - sharedPreferences - .edit() - .putString("controls_component", panel.componentName.flattenToString()) - .putString("controls_structure", panel.appName.toString()) - .putBoolean("controls_is_panel", true) - .commit() + preferredPanelRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(panel) + ) return ControlsServiceInfo(panel.componentName, panel.appName, activity) } 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/flags/ConditionalRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/ConditionalRestarterTest.kt new file mode 100644 index 000000000000..0e14591c5f53 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/ConditionalRestarterTest.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.flags + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()! + */ +@SmallTest +class ConditionalRestarterTest : SysuiTestCase() { + private lateinit var restarter: ConditionalRestarter + + @Mock private lateinit var systemExitRestarter: SystemExitRestarter + + val restartDelayMs = 0L + val dispatcher = StandardTestDispatcher() + val testScope = TestScope(dispatcher) + + val conditionA = FakeCondition() + val conditionB = FakeCondition() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + restarter = + ConditionalRestarter( + systemExitRestarter, + setOf(conditionA, conditionB), + restartDelayMs, + testScope, + dispatcher + ) + } + + @Test + fun restart_ImmediatelySatisfied() = + testScope.runTest { + conditionA.canRestart = true + conditionB.canRestart = true + restarter.restartSystemUI("Restart for test") + advanceUntilIdle() + verify(systemExitRestarter).restartSystemUI(any()) + } + + @Test + fun restart_WaitsForConditionA() = + testScope.runTest { + conditionA.canRestart = false + conditionB.canRestart = true + + restarter.restartSystemUI("Restart for test") + advanceUntilIdle() + // No restart occurs yet. + verify(systemExitRestarter, never()).restartSystemUI(any()) + + conditionA.canRestart = true + conditionA.retryFn?.invoke() + advanceUntilIdle() + verify(systemExitRestarter).restartSystemUI(any()) + } + + @Test + fun restart_WaitsForConditionB() = + testScope.runTest { + conditionA.canRestart = true + conditionB.canRestart = false + + restarter.restartSystemUI("Restart for test") + advanceUntilIdle() + // No restart occurs yet. + verify(systemExitRestarter, never()).restartSystemUI(any()) + + conditionB.canRestart = true + conditionB.retryFn?.invoke() + advanceUntilIdle() + verify(systemExitRestarter).restartSystemUI(any()) + } + + @Test + fun restart_WaitsForAllConditions() = + testScope.runTest { + conditionA.canRestart = true + conditionB.canRestart = false + + restarter.restartSystemUI("Restart for test") + advanceUntilIdle() + // No restart occurs yet. + verify(systemExitRestarter, never()).restartSystemUI(any()) + + // B becomes true, but A is now false + conditionA.canRestart = false + conditionB.canRestart = true + conditionB.retryFn?.invoke() + advanceUntilIdle() + // No restart occurs yet. + verify(systemExitRestarter, never()).restartSystemUI(any()) + + conditionA.canRestart = true + conditionA.retryFn?.invoke() + advanceUntilIdle() + verify(systemExitRestarter).restartSystemUI(any()) + } + + class FakeCondition : ConditionalRestarter.Condition { + var retryFn: (() -> Unit)? = null + var canRestart = false + + override fun canRestartNow(retryFn: () -> Unit): Boolean { + this.retryFn = retryFn + + return canRestart + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt deleted file mode 100644 index 6060afe495f5..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.flags - -import android.test.suitebuilder.annotation.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP -import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE -import com.android.systemui.statusbar.policy.BatteryController -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.mockito.ArgumentCaptor -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever -import org.mockito.MockitoAnnotations - -/** - * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()! - */ -@SmallTest -class FeatureFlagsReleaseRestarterTest : SysuiTestCase() { - private lateinit var restarter: FeatureFlagsReleaseRestarter - - @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle - @Mock private lateinit var batteryController: BatteryController - @Mock private lateinit var systemExitRestarter: SystemExitRestarter - private val executor = FakeExecutor(FakeSystemClock()) - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - restarter = - FeatureFlagsReleaseRestarter( - wakefulnessLifecycle, - batteryController, - executor, - systemExitRestarter - ) - } - - @Test - fun testRestart_ScheduledWhenReady() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - whenever(batteryController.isPluggedIn).thenReturn(true) - - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - assertThat(executor.numPending()).isEqualTo(1) - } - - @Test - fun testRestart_RestartsWhenIdle() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - whenever(batteryController.isPluggedIn).thenReturn(true) - - restarter.restartSystemUI("Restart for test") - verify(systemExitRestarter, never()).restartSystemUI("Restart for test") - executor.advanceClockToLast() - executor.runAllReady() - verify(systemExitRestarter).restartSystemUI(any()) - } - - @Test - fun testRestart_NotScheduledWhenAwake() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) - whenever(batteryController.isPluggedIn).thenReturn(true) - - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - assertThat(executor.numPending()).isEqualTo(0) - } - - @Test - fun testRestart_NotScheduledWhenNotPluggedIn() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - whenever(batteryController.isPluggedIn).thenReturn(false) - - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - assertThat(executor.numPending()).isEqualTo(0) - } - - @Test - fun testRestart_NotDoubleSheduled() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - whenever(batteryController.isPluggedIn).thenReturn(true) - - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - restarter.restartSystemUI("Restart for test") - assertThat(executor.numPending()).isEqualTo(1) - } - - @Test - fun testWakefulnessLifecycle_CanRestart() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) - whenever(batteryController.isPluggedIn).thenReturn(true) - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - - val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java) - verify(wakefulnessLifecycle).addObserver(captor.capture()) - - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - - captor.value.onFinishedGoingToSleep() - assertThat(executor.numPending()).isEqualTo(1) - } - - @Test - fun testBatteryController_CanRestart() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - whenever(batteryController.isPluggedIn).thenReturn(false) - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - - val captor = - ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback::class.java) - verify(batteryController).addCallback(captor.capture()) - - whenever(batteryController.isPluggedIn).thenReturn(true) - - captor.value.onBatteryLevelChanged(0, true, true) - assertThat(executor.numPending()).isEqualTo(1) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/PluggedInConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/PluggedInConditionTest.kt new file mode 100644 index 000000000000..647b05a77b90 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/PluggedInConditionTest.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.flags + +import android.test.suitebuilder.annotation.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.policy.BatteryController +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +/** + * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()! + */ +@SmallTest +class PluggedInConditionTest : SysuiTestCase() { + private lateinit var condition: PluggedInCondition + + @Mock private lateinit var batteryController: BatteryController + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + condition = PluggedInCondition(batteryController) + } + + @Test + fun testCondition_unplugged() { + whenever(batteryController.isPluggedIn).thenReturn(false) + + assertThat(condition.canRestartNow({})).isFalse() + } + + @Test + fun testCondition_pluggedIn() { + whenever(batteryController.isPluggedIn).thenReturn(true) + + assertThat(condition.canRestartNow({})).isTrue() + } + + @Test + fun testCondition_invokesRetry() { + whenever(batteryController.isPluggedIn).thenReturn(false) + var retried = false + val retryFn = { retried = true } + + // No restart yet, but we do register a listener now. + assertThat(condition.canRestartNow(retryFn)).isFalse() + val captor = + ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback::class.java) + verify(batteryController).addCallback(captor.capture()) + + whenever(batteryController.isPluggedIn).thenReturn(true) + + captor.value.onBatteryLevelChanged(0, true, true) + assertThat(retried).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/ScreenIdleConditionTest.kt index 686782f59355..f7a773ea30ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/ScreenIdleConditionTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,11 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE -import com.android.systemui.util.mockito.any +import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.mockito.ArgumentCaptor import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -34,37 +33,45 @@ import org.mockito.MockitoAnnotations * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()! */ @SmallTest -class FeatureFlagsDebugRestarterTest : SysuiTestCase() { - private lateinit var restarter: FeatureFlagsDebugRestarter +class ScreenIdleConditionTest : SysuiTestCase() { + private lateinit var condition: ScreenIdleCondition @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle - @Mock private lateinit var systemExitRestarter: SystemExitRestarter @Before fun setup() { MockitoAnnotations.initMocks(this) - restarter = FeatureFlagsDebugRestarter(wakefulnessLifecycle, systemExitRestarter) + condition = ScreenIdleCondition(wakefulnessLifecycle) } @Test - fun testRestart_ImmediateWhenAsleep() { + fun testCondition_awake() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) + + assertThat(condition.canRestartNow {}).isFalse() + } + + @Test + fun testCondition_asleep() { whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - restarter.restartSystemUI("Restart for test") - verify(systemExitRestarter).restartSystemUI(any()) + + assertThat(condition.canRestartNow {}).isTrue() } @Test - fun testRestart_WaitsForSceenOff() { + fun testCondition_invokesRetry() { whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) + var retried = false + val retryFn = { retried = true } - restarter.restartSystemUI("Restart for test") - verify(systemExitRestarter, never()).restartSystemUI(any()) - + // No restart yet, but we do register a listener now. + assertThat(condition.canRestartNow(retryFn)).isFalse() val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java) verify(wakefulnessLifecycle).addObserver(captor.capture()) - captor.value.onFinishedGoingToSleep() + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - verify(systemExitRestarter).restartSystemUI(any()) + captor.value.onFinishedGoingToSleep() + assertThat(retried).isTrue() } } 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..503e0025ce5c 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, ) @@ -281,6 +284,24 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { } @Test + fun `quickAffordance - hidden when quick settings is visible`() = + testScope.runTest { + repository.setQuickSettingsVisible(true) + quickAccessWallet.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = ICON, + ) + ) + + val collectedValue = + collectLastValue( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) + ) + + assertThat(collectedValue()).isEqualTo(KeyguardQuickAffordanceModel.Hidden) + } + + @Test fun `quickAffordance - bottom start affordance hidden while dozing`() = testScope.runTest { repository.setDozing(true) 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/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index ab0669a28f04..d428db7b9dda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -2031,7 +2031,7 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test - fun testRetain_sessionPlayer_destroyedWhileActive_fullyRemoved() { + fun testRetain_sessionPlayer_destroyedWhileActive_noResume_fullyRemoved() { whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) addPlaybackStateAction() @@ -2051,6 +2051,40 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testRetain_sessionPlayer_canResume_destroyedWhileActive_setToResume() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control using session actions and that does allow resumption is added, + addNotificationAndLoad() + val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {}) + mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable) + + // And then the session is destroyed without timing out first + sessionCallbackCaptor.value.invoke(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test fun testSessionDestroyed_noNotificationKey_stillRemoved() { whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt index 4dfa6261b868..9ab728949e40 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt @@ -313,6 +313,25 @@ class MediaResumeListenerTest : SysuiTestCase() { } @Test + fun testOnLoadTwice_onlyChecksOnce() { + // When data is first loaded, + setUpMbsWithValidResolveInfo() + resumeListener.onMediaDataLoaded(KEY, null, data) + + // We notify the manager to set a null action + verify(mediaDataManager).setResumeAction(KEY, null) + + // If we then get another update from the app before the first check completes + assertThat(executor.numPending()).isEqualTo(1) + var dataWithCheck = data.copy(hasCheckedForResume = true) + resumeListener.onMediaDataLoaded(KEY, null, dataWithCheck) + + // We do not try to start another check + assertThat(executor.numPending()).isEqualTo(1) + verify(mediaDataManager).setResumeAction(KEY, null) + } + + @Test fun testOnUserUnlock_loadsTracks() { // Set up mock service to successfully find valid media val description = MediaDescription.Builder().setTitle(TITLE).build() @@ -392,7 +411,7 @@ class MediaResumeListenerTest : SysuiTestCase() { assertThat(result.size).isEqualTo(3) assertThat(result[2].toLong()).isEqualTo(currentTime) } - verify(sharedPrefsEditor, times(1)).apply() + verify(sharedPrefsEditor).apply() } @Test @@ -432,8 +451,8 @@ class MediaResumeListenerTest : SysuiTestCase() { resumeListener.userUnlockReceiver.onReceive(mockContext, intent) // We add its resume controls - verify(resumeBrowser, times(1)).findRecentMedia() - verify(mediaDataManager, times(1)) + verify(resumeBrowser).findRecentMedia() + verify(mediaDataManager) .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) } @@ -516,7 +535,7 @@ class MediaResumeListenerTest : SysuiTestCase() { assertThat(result.size).isEqualTo(3) assertThat(result[2].toLong()).isEqualTo(currentTime) } - verify(sharedPrefsEditor, times(1)).apply() + verify(sharedPrefsEditor).apply() } @Test 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..7c9351c8495d 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 @@ -47,14 +47,14 @@ class MobileInputLoggerTest : SysuiTestCase() { val expectedNetId = NET_1_ID.toString() val expectedCaps = NET_1_CAPS.toString() - assertThat(actualString).contains("true") + assertThat(actualString).contains("onDefaultCapabilitiesChanged") assertThat(actualString).contains(expectedNetId) assertThat(actualString).contains(expectedCaps) } @Test fun testLogOnLost_bufferHasNetIdOfLostNetwork() { - logger.logOnLost(NET_1) + logger.logOnLost(NET_1, isDefaultNetworkCallback = false) val stringWriter = StringWriter() buffer.dump(PrintWriter(stringWriter), tailLength = 0) @@ -62,6 +62,7 @@ class MobileInputLoggerTest : SysuiTestCase() { val expectedNetId = NET_1_ID.toString() + assertThat(actualString).contains("onLost") assertThat(actualString).contains(expectedNetId) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt deleted file mode 100644 index 45189cf8d432..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.mobile.data.model - -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.log.table.TableRowLogger -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION_IN -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION_OUT -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CARRIER_NETWORK_CHANGE -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CDMA_LEVEL -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CONNECTION_STATE -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_EMERGENCY -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_IS_GSM -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_OPERATOR -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_PRIMARY_LEVEL -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_RESOLVED_NETWORK_TYPE -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ROAMING -import com.google.common.truth.Truth.assertThat -import org.junit.Test - -@SmallTest -class MobileConnectionModelTest : SysuiTestCase() { - - @Test - fun `log diff - initial log contains all columns`() { - val logger = TestLogger() - val connection = MobileConnectionModel() - - connection.logFull(logger) - - assertThat(logger.changes) - .contains(Pair(COL_EMERGENCY, connection.isEmergencyOnly.toString())) - assertThat(logger.changes).contains(Pair(COL_ROAMING, connection.isRoaming.toString())) - assertThat(logger.changes) - .contains(Pair(COL_OPERATOR, connection.operatorAlphaShort.toString())) - assertThat(logger.changes).contains(Pair(COL_IS_GSM, connection.isGsm.toString())) - assertThat(logger.changes).contains(Pair(COL_CDMA_LEVEL, connection.cdmaLevel.toString())) - assertThat(logger.changes) - .contains(Pair(COL_PRIMARY_LEVEL, connection.primaryLevel.toString())) - assertThat(logger.changes) - .contains(Pair(COL_CONNECTION_STATE, connection.dataConnectionState.toString())) - assertThat(logger.changes) - .contains( - Pair( - COL_ACTIVITY_DIRECTION_IN, - connection.dataActivityDirection.hasActivityIn.toString(), - ) - ) - assertThat(logger.changes) - .contains( - Pair( - COL_ACTIVITY_DIRECTION_OUT, - connection.dataActivityDirection.hasActivityOut.toString(), - ) - ) - assertThat(logger.changes) - .contains( - Pair(COL_CARRIER_NETWORK_CHANGE, connection.carrierNetworkChangeActive.toString()) - ) - assertThat(logger.changes) - .contains(Pair(COL_RESOLVED_NETWORK_TYPE, connection.resolvedNetworkType.toString())) - } - - @Test - fun `log diff - primary level changes - only level is logged`() { - val logger = TestLogger() - val connectionOld = MobileConnectionModel(primaryLevel = 1) - - val connectionNew = MobileConnectionModel(primaryLevel = 2) - - connectionNew.logDiffs(connectionOld, logger) - - assertThat(logger.changes).isEqualTo(listOf(Pair(COL_PRIMARY_LEVEL, "2"))) - } - - private class TestLogger : TableRowLogger { - val changes = mutableListOf<Pair<String, String>>() - - override fun logChange(columnName: String, value: String?) { - changes.add(Pair(columnName, value.toString())) - } - - override fun logChange(columnName: String, value: Int) { - changes.add(Pair(columnName, value.toString())) - } - - override fun logChange(columnName: String, value: Boolean) { - changes.add(Pair(columnName, value.toString())) - } - } -} 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/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index 53cd71f1bdf9..44fbd5b99894 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -17,9 +17,11 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.flow.MutableStateFlow // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository @@ -27,8 +29,19 @@ class FakeMobileConnectionRepository( override val subId: Int, override val tableLogBuffer: TableLogBuffer, ) : MobileConnectionRepository { - private val _connectionInfo = MutableStateFlow(MobileConnectionModel()) - override val connectionInfo = _connectionInfo + override val isEmergencyOnly = MutableStateFlow(false) + override val isRoaming = MutableStateFlow(false) + override val operatorAlphaShort: MutableStateFlow<String?> = MutableStateFlow(null) + override val isInService = MutableStateFlow(false) + override val isGsm = MutableStateFlow(false) + override val cdmaLevel = MutableStateFlow(0) + override val primaryLevel = MutableStateFlow(0) + override val dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected) + override val dataActivityDirection = + MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + override val carrierNetworkChangeActive = MutableStateFlow(false) + override val resolvedNetworkType: MutableStateFlow<ResolvedNetworkType> = + MutableStateFlow(ResolvedNetworkType.UnknownNetworkType) override val numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS) @@ -40,10 +53,6 @@ class FakeMobileConnectionRepository( override val networkName = MutableStateFlow<NetworkNameModel>(NetworkNameModel.Default("default")) - fun setConnectionInfo(model: MobileConnectionModel) { - _connectionInfo.value = model - } - fun setDataEnabled(enabled: Boolean) { _dataEnabled.value = enabled } 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/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt index b072deedb9c9..37fac3458c83 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt @@ -25,7 +25,6 @@ import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBufferFactory 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 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel @@ -36,8 +35,11 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -123,34 +125,49 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC assertConnection(underTest, networkModel) } - private fun assertConnection( + private fun TestScope.startCollection(conn: DemoMobileConnectionRepository): Job { + val job = launch { + launch { conn.cdmaLevel.collect {} } + launch { conn.primaryLevel.collect {} } + launch { conn.dataActivityDirection.collect {} } + launch { conn.carrierNetworkChangeActive.collect {} } + launch { conn.isRoaming.collect {} } + launch { conn.networkName.collect {} } + launch { conn.isEmergencyOnly.collect {} } + launch { conn.dataConnectionState.collect {} } + } + return job + } + + private fun TestScope.assertConnection( conn: DemoMobileConnectionRepository, model: FakeNetworkEventModel ) { + val job = startCollection(underTest) when (model) { is FakeNetworkEventModel.Mobile -> { - val connectionInfo: MobileConnectionModel = conn.connectionInfo.value assertThat(conn.subId).isEqualTo(model.subId) - assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) - assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) - assertThat(connectionInfo.dataActivityDirection) + assertThat(conn.cdmaLevel.value).isEqualTo(model.level) + assertThat(conn.primaryLevel.value).isEqualTo(model.level) + assertThat(conn.dataActivityDirection.value) .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel()) - assertThat(connectionInfo.carrierNetworkChangeActive) + assertThat(conn.carrierNetworkChangeActive.value) .isEqualTo(model.carrierNetworkChange) - assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming) + assertThat(conn.isRoaming.value).isEqualTo(model.roaming) assertThat(conn.networkName.value) .isEqualTo(NetworkNameModel.IntentDerived(model.name)) // TODO(b/261029387): check these once we start handling them - assertThat(connectionInfo.isEmergencyOnly).isFalse() - assertThat(connectionInfo.isGsm).isFalse() - assertThat(connectionInfo.dataConnectionState) - .isEqualTo(DataConnectionState.Connected) + assertThat(conn.isEmergencyOnly.value).isFalse() + assertThat(conn.isGsm.value).isFalse() + assertThat(conn.dataConnectionState.value).isEqualTo(DataConnectionState.Connected) } // MobileDisabled isn't combinatorial in nature, and is tested in // DemoMobileConnectionsRepositoryTest.kt else -> {} } + + job.cancel() } /** Matches [FakeNetworkEventModel] */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt index f60d92bde202..0e45d8ea5563 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt @@ -26,7 +26,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.table.TableLogBufferFactory 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 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel @@ -40,9 +39,11 @@ import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import junit.framework.Assert import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -524,47 +525,65 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } - private fun assertConnection( + private fun TestScope.startCollection(conn: DemoMobileConnectionRepository): Job { + val job = launch { + launch { conn.cdmaLevel.collect {} } + launch { conn.primaryLevel.collect {} } + launch { conn.dataActivityDirection.collect {} } + launch { conn.carrierNetworkChangeActive.collect {} } + launch { conn.isRoaming.collect {} } + launch { conn.networkName.collect {} } + launch { conn.isEmergencyOnly.collect {} } + launch { conn.dataConnectionState.collect {} } + } + return job + } + + private fun TestScope.assertConnection( conn: DemoMobileConnectionRepository, - model: FakeNetworkEventModel + model: FakeNetworkEventModel, ) { + val job = startCollection(conn) + // Assert the fields using the `MutableStateFlow` so that we don't have to start up + // a collector for every field for every test when (model) { is FakeNetworkEventModel.Mobile -> { - val connectionInfo: MobileConnectionModel = conn.connectionInfo.value assertThat(conn.subId).isEqualTo(model.subId) - assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) - assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) - assertThat(connectionInfo.dataActivityDirection) + assertThat(conn.cdmaLevel.value).isEqualTo(model.level) + assertThat(conn.primaryLevel.value).isEqualTo(model.level) + assertThat(conn.dataActivityDirection.value) .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel()) - assertThat(connectionInfo.carrierNetworkChangeActive) + assertThat(conn.carrierNetworkChangeActive.value) .isEqualTo(model.carrierNetworkChange) - assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming) + assertThat(conn.isRoaming.value).isEqualTo(model.roaming) assertThat(conn.networkName.value) .isEqualTo(NetworkNameModel.IntentDerived(model.name)) // TODO(b/261029387) check these once we start handling them - assertThat(connectionInfo.isEmergencyOnly).isFalse() - assertThat(connectionInfo.isGsm).isFalse() - assertThat(connectionInfo.dataConnectionState) - .isEqualTo(DataConnectionState.Connected) + assertThat(conn.isEmergencyOnly.value).isFalse() + assertThat(conn.isGsm.value).isFalse() + assertThat(conn.dataConnectionState.value).isEqualTo(DataConnectionState.Connected) } else -> {} } + + job.cancel() } - private fun assertCarrierMergedConnection( + private fun TestScope.assertCarrierMergedConnection( conn: DemoMobileConnectionRepository, model: FakeWifiEventModel.CarrierMerged, ) { - val connectionInfo: MobileConnectionModel = conn.connectionInfo.value + val job = startCollection(conn) assertThat(conn.subId).isEqualTo(model.subscriptionId) - assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) - assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) - assertThat(connectionInfo.carrierNetworkChangeActive).isEqualTo(false) - assertThat(connectionInfo.isRoaming).isEqualTo(false) - assertThat(connectionInfo.isEmergencyOnly).isFalse() - assertThat(connectionInfo.isGsm).isFalse() - assertThat(connectionInfo.dataConnectionState).isEqualTo(DataConnectionState.Connected) + assertThat(conn.cdmaLevel.value).isEqualTo(model.level) + assertThat(conn.primaryLevel.value).isEqualTo(model.level) + assertThat(conn.carrierNetworkChangeActive.value).isEqualTo(false) + assertThat(conn.isRoaming.value).isEqualTo(false) + assertThat(conn.isEmergencyOnly.value).isFalse() + assertThat(conn.isGsm.value).isFalse() + assertThat(conn.dataConnectionState.value).isEqualTo(DataConnectionState.Connected) + job.cancel() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt index f0f213bc0d58..441186acb6b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt @@ -22,7 +22,6 @@ 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.model.DataConnectionState -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 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel @@ -75,36 +74,48 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { } @Test - fun connectionInfo_inactiveWifi_isDefault() = + fun inactiveWifi_isDefault() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestConnState: DataConnectionState? = null + var latestNetType: ResolvedNetworkType? = null + + val dataJob = + underTest.dataConnectionState.onEach { latestConnState = it }.launchIn(this) + val netJob = underTest.resolvedNetworkType.onEach { latestNetType = it }.launchIn(this) wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) - assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected) + assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) - job.cancel() + dataJob.cancel() + netJob.cancel() } @Test - fun connectionInfo_activeWifi_isDefault() = + fun activeWifi_isDefault() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestConnState: DataConnectionState? = null + var latestNetType: ResolvedNetworkType? = null + + val dataJob = + underTest.dataConnectionState.onEach { latestConnState = it }.launchIn(this) + val netJob = underTest.resolvedNetworkType.onEach { latestNetType = it }.launchIn(this) wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = NET_ID, level = 1)) - assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected) + assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) - job.cancel() + dataJob.cancel() + netJob.cancel() } @Test - fun connectionInfo_carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() = + fun carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Int? = null + val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) wifiRepository.setIsWifiEnabled(true) wifiRepository.setIsWifiDefault(true) @@ -117,34 +128,16 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) ) - val expected = - MobileConnectionModel( - primaryLevel = 3, - cdmaLevel = 3, - dataConnectionState = DataConnectionState.Connected, - dataActivityDirection = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = false, - ), - resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType, - isRoaming = false, - isEmergencyOnly = false, - operatorAlphaShort = null, - isInService = true, - isGsm = false, - carrierNetworkChangeActive = false, - ) - assertThat(latest).isEqualTo(expected) + assertThat(latest).isEqualTo(3) job.cancel() } @Test - fun connectionInfo_activity_comesFromWifiActivity() = + fun activity_comesFromWifiActivity() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataActivityModel? = null + val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this) wifiRepository.setIsWifiEnabled(true) wifiRepository.setIsWifiDefault(true) @@ -162,8 +155,8 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) ) - assertThat(latest!!.dataActivityDirection.hasActivityIn).isTrue() - assertThat(latest!!.dataActivityDirection.hasActivityOut).isFalse() + assertThat(latest!!.hasActivityIn).isTrue() + assertThat(latest!!.hasActivityOut).isFalse() wifiRepository.setWifiActivity( DataActivityModel( @@ -172,17 +165,19 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) ) - assertThat(latest!!.dataActivityDirection.hasActivityIn).isFalse() - assertThat(latest!!.dataActivityDirection.hasActivityOut).isTrue() + assertThat(latest!!.hasActivityIn).isFalse() + assertThat(latest!!.hasActivityOut).isTrue() job.cancel() } @Test - fun connectionInfo_carrierMergedWifi_wrongSubId_isDefault() = + fun carrierMergedWifi_wrongSubId_isDefault() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestLevel: Int? = null + var latestType: ResolvedNetworkType? = null + val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) + val typeJob = underTest.resolvedNetworkType.onEach { latestType = it }.launchIn(this) wifiRepository.setWifiNetwork( WifiNetworkModel.CarrierMerged( @@ -192,20 +187,19 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) ) - assertThat(latest).isEqualTo(MobileConnectionModel()) - assertThat(latest!!.primaryLevel).isNotEqualTo(3) - assertThat(latest!!.resolvedNetworkType) - .isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) + assertThat(latestLevel).isNotEqualTo(3) + assertThat(latestType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) - job.cancel() + levelJob.cancel() + typeJob.cancel() } // This scenario likely isn't possible, but write a test for it anyway @Test - fun connectionInfo_carrierMergedButNotEnabled_isDefault() = + fun carrierMergedButNotEnabled_isDefault() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Int? = null + val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) wifiRepository.setWifiNetwork( WifiNetworkModel.CarrierMerged( @@ -216,17 +210,17 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) wifiRepository.setIsWifiEnabled(false) - assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latest).isNotEqualTo(3) job.cancel() } // This scenario likely isn't possible, but write a test for it anyway @Test - fun connectionInfo_carrierMergedButWifiNotDefault_isDefault() = + fun carrierMergedButWifiNotDefault_isDefault() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Int? = null + val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) wifiRepository.setWifiNetwork( WifiNetworkModel.CarrierMerged( @@ -237,7 +231,7 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) wifiRepository.setIsWifiDefault(false) - assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latest).isNotEqualTo(3) job.cancel() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt index cd4d8472763f..db5a7d1ad84a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt @@ -24,13 +24,12 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_EMERGENCY -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_OPERATOR -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_PRIMARY_LEVEL import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_EMERGENCY +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.getTelephonyCallbackForType import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel @@ -94,16 +93,16 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { @Test fun startingIsCarrierMerged_usesCarrierMergedInitially() = testScope.runTest { - val carrierMergedConnectionInfo = - MobileConnectionModel( - operatorAlphaShort = "Carrier Merged Operator", - ) - carrierMergedRepo.setConnectionInfo(carrierMergedConnectionInfo) + val carrierMergedOperatorName = "Carrier Merged Operator" + val nonCarrierMergedName = "Non-carrier-merged" + + carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperatorName + mobileRepo.operatorAlphaShort.value = nonCarrierMergedName initializeRepo(startingIsCarrierMerged = true) assertThat(underTest.activeRepo.value).isEqualTo(carrierMergedRepo) - assertThat(underTest.connectionInfo.value).isEqualTo(carrierMergedConnectionInfo) + assertThat(underTest.operatorAlphaShort.value).isEqualTo(carrierMergedOperatorName) verify(mobileFactory, never()) .build( SUB_ID, @@ -116,16 +115,16 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { @Test fun startingNotCarrierMerged_usesTypicalInitially() = testScope.runTest { - val mobileConnectionInfo = - MobileConnectionModel( - operatorAlphaShort = "Typical Operator", - ) - mobileRepo.setConnectionInfo(mobileConnectionInfo) + val carrierMergedOperatorName = "Carrier Merged Operator" + val nonCarrierMergedName = "Typical Operator" + + carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperatorName + mobileRepo.operatorAlphaShort.value = nonCarrierMergedName initializeRepo(startingIsCarrierMerged = false) assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo) - assertThat(underTest.connectionInfo.value).isEqualTo(mobileConnectionInfo) + assertThat(underTest.operatorAlphaShort.value).isEqualTo(nonCarrierMergedName) verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer) } @@ -156,39 +155,40 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { testScope.runTest { initializeRepo(startingIsCarrierMerged = false) - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestName: String? = null + var latestLevel: Int? = null + + val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this) + val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) underTest.setIsCarrierMerged(true) - val info1 = - MobileConnectionModel( - operatorAlphaShort = "Carrier Merged Operator", - primaryLevel = 1, - ) - carrierMergedRepo.setConnectionInfo(info1) + val operator1 = "Carrier Merged Operator" + val level1 = 1 + carrierMergedRepo.operatorAlphaShort.value = operator1 + carrierMergedRepo.primaryLevel.value = level1 - assertThat(latest).isEqualTo(info1) + assertThat(latestName).isEqualTo(operator1) + assertThat(latestLevel).isEqualTo(level1) - val info2 = - MobileConnectionModel( - operatorAlphaShort = "Carrier Merged Operator #2", - primaryLevel = 2, - ) - carrierMergedRepo.setConnectionInfo(info2) + val operator2 = "Carrier Merged Operator #2" + val level2 = 2 + carrierMergedRepo.operatorAlphaShort.value = operator2 + carrierMergedRepo.primaryLevel.value = level2 - assertThat(latest).isEqualTo(info2) + assertThat(latestName).isEqualTo(operator2) + assertThat(latestLevel).isEqualTo(level2) - val info3 = - MobileConnectionModel( - operatorAlphaShort = "Carrier Merged Operator #3", - primaryLevel = 3, - ) - carrierMergedRepo.setConnectionInfo(info3) + val operator3 = "Carrier Merged Operator #3" + val level3 = 3 + carrierMergedRepo.operatorAlphaShort.value = operator3 + carrierMergedRepo.primaryLevel.value = level3 - assertThat(latest).isEqualTo(info3) + assertThat(latestName).isEqualTo(operator3) + assertThat(latestLevel).isEqualTo(level3) - job.cancel() + nameJob.cancel() + levelJob.cancel() } @Test @@ -196,39 +196,40 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { testScope.runTest { initializeRepo(startingIsCarrierMerged = false) - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestName: String? = null + var latestLevel: Int? = null + + val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this) + val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) underTest.setIsCarrierMerged(false) - val info1 = - MobileConnectionModel( - operatorAlphaShort = "Typical Merged Operator", - primaryLevel = 1, - ) - mobileRepo.setConnectionInfo(info1) + val operator1 = "Typical Merged Operator" + val level1 = 1 + mobileRepo.operatorAlphaShort.value = operator1 + mobileRepo.primaryLevel.value = level1 - assertThat(latest).isEqualTo(info1) + assertThat(latestName).isEqualTo(operator1) + assertThat(latestLevel).isEqualTo(level1) - val info2 = - MobileConnectionModel( - operatorAlphaShort = "Typical Merged Operator #2", - primaryLevel = 2, - ) - mobileRepo.setConnectionInfo(info2) + val operator2 = "Typical Merged Operator #2" + val level2 = 2 + mobileRepo.operatorAlphaShort.value = operator2 + mobileRepo.primaryLevel.value = level2 - assertThat(latest).isEqualTo(info2) + assertThat(latestName).isEqualTo(operator2) + assertThat(latestLevel).isEqualTo(level2) - val info3 = - MobileConnectionModel( - operatorAlphaShort = "Typical Merged Operator #3", - primaryLevel = 3, - ) - mobileRepo.setConnectionInfo(info3) + val operator3 = "Typical Merged Operator #3" + val level3 = 3 + mobileRepo.operatorAlphaShort.value = operator3 + mobileRepo.primaryLevel.value = level3 - assertThat(latest).isEqualTo(info3) + assertThat(latestName).isEqualTo(operator3) + assertThat(latestLevel).isEqualTo(level3) - job.cancel() + nameJob.cancel() + levelJob.cancel() } @Test @@ -236,57 +237,58 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { testScope.runTest { initializeRepo(startingIsCarrierMerged = false) - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestName: String? = null + var latestLevel: Int? = null - val carrierMergedInfo = - MobileConnectionModel( - operatorAlphaShort = "Carrier Merged Operator", - primaryLevel = 4, - ) - carrierMergedRepo.setConnectionInfo(carrierMergedInfo) + val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this) + val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) - val mobileInfo = - MobileConnectionModel( - operatorAlphaShort = "Typical Operator", - primaryLevel = 2, - ) - mobileRepo.setConnectionInfo(mobileInfo) + val carrierMergedOperator = "Carrier Merged Operator" + val carrierMergedLevel = 4 + carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperator + carrierMergedRepo.primaryLevel.value = carrierMergedLevel + + val mobileName = "Typical Operator" + val mobileLevel = 2 + mobileRepo.operatorAlphaShort.value = mobileName + mobileRepo.primaryLevel.value = mobileLevel // Start with the mobile info - assertThat(latest).isEqualTo(mobileInfo) + assertThat(latestName).isEqualTo(mobileName) + assertThat(latestLevel).isEqualTo(mobileLevel) // WHEN isCarrierMerged is set to true underTest.setIsCarrierMerged(true) // THEN the carrier merged info is used - assertThat(latest).isEqualTo(carrierMergedInfo) + assertThat(latestName).isEqualTo(carrierMergedOperator) + assertThat(latestLevel).isEqualTo(carrierMergedLevel) - val newCarrierMergedInfo = - MobileConnectionModel( - operatorAlphaShort = "New CM Operator", - primaryLevel = 0, - ) - carrierMergedRepo.setConnectionInfo(newCarrierMergedInfo) + val newCarrierMergedName = "New CM Operator" + val newCarrierMergedLevel = 0 + carrierMergedRepo.operatorAlphaShort.value = newCarrierMergedName + carrierMergedRepo.primaryLevel.value = newCarrierMergedLevel - assertThat(latest).isEqualTo(newCarrierMergedInfo) + assertThat(latestName).isEqualTo(newCarrierMergedName) + assertThat(latestLevel).isEqualTo(newCarrierMergedLevel) // WHEN isCarrierMerged is set to false underTest.setIsCarrierMerged(false) // THEN the typical info is used - assertThat(latest).isEqualTo(mobileInfo) + assertThat(latestName).isEqualTo(mobileName) + assertThat(latestLevel).isEqualTo(mobileLevel) - val newMobileInfo = - MobileConnectionModel( - operatorAlphaShort = "New Mobile Operator", - primaryLevel = 3, - ) - mobileRepo.setConnectionInfo(newMobileInfo) + val newMobileName = "New MobileOperator" + val newMobileLevel = 3 + mobileRepo.operatorAlphaShort.value = newMobileName + mobileRepo.primaryLevel.value = newMobileLevel - assertThat(latest).isEqualTo(newMobileInfo) + assertThat(latestName).isEqualTo(newMobileName) + assertThat(latestLevel).isEqualTo(newMobileLevel) - job.cancel() + nameJob.cancel() + levelJob.cancel() } @Test @@ -370,7 +372,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { initializeRepo(startingIsCarrierMerged = false) - val job = underTest.connectionInfo.launchIn(this) + val emergencyJob = underTest.isEmergencyOnly.launchIn(this) + val operatorJob = underTest.operatorAlphaShort.launchIn(this) // WHEN we set up some mobile connection info val serviceState = ServiceState() @@ -394,7 +397,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { assertThat(dumpBuffer()).contains("$COL_OPERATOR${BUFFER_SEPARATOR}OpDiff") assertThat(dumpBuffer()).contains("$COL_EMERGENCY${BUFFER_SEPARATOR}true") - job.cancel() + emergencyJob.cancel() + operatorJob.cancel() } @Test @@ -409,7 +413,7 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { initializeRepo(startingIsCarrierMerged = true) - val job = underTest.connectionInfo.launchIn(this) + val job = underTest.primaryLevel.launchIn(this) // WHEN we set up carrier merged info val networkId = 2 @@ -452,7 +456,7 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { initializeRepo(startingIsCarrierMerged = false) - val job = underTest.connectionInfo.launchIn(this) + val job = underTest.primaryLevel.launchIn(this) // WHEN we set up some mobile connection info val signalStrength = mock<SignalStrength>() @@ -502,12 +506,7 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { assertThat(bufferAfterCarrierMerged).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1") // WHEN the normal network is updated - val newMobileInfo = - MobileConnectionModel( - operatorAlphaShort = "Mobile Operator 2", - primaryLevel = 0, - ) - mobileRepo.setConnectionInfo(newMobileInfo) + mobileRepo.primaryLevel.value = 0 // THEN the new level is logged assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}0") @@ -529,7 +528,7 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { // WHEN isCarrierMerged = false initializeRepo(startingIsCarrierMerged = false) - val job = underTest.connectionInfo.launchIn(this) + val job = underTest.primaryLevel.launchIn(this) val signalStrength = mock<SignalStrength>() whenever(signalStrength.level).thenReturn(1) 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..f6e595924f58 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 @@ -50,12 +50,15 @@ import android.telephony.TelephonyManager.EXTRA_SHOW_SPN import android.telephony.TelephonyManager.EXTRA_SPN import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID import android.telephony.TelephonyManager.NETWORK_TYPE_LTE +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest +import com.android.settingslib.mobile.MobileMappings 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 +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType @@ -65,7 +68,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 @@ -135,235 +137,285 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test - fun testFlowForSubId_default() = + fun emergencyOnly() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) - - assertThat(latest).isEqualTo(MobileConnectionModel()) - - job.cancel() - } - - @Test - fun testFlowForSubId_emergencyOnly() = - runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this) val serviceState = ServiceState() serviceState.isEmergencyOnly = true getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState) - assertThat(latest?.isEmergencyOnly).isEqualTo(true) + assertThat(latest).isEqualTo(true) job.cancel() } @Test - fun testFlowForSubId_emergencyOnly_toggles() = + fun emergencyOnly_toggles() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<ServiceStateListener>() val serviceState = ServiceState() serviceState.isEmergencyOnly = true callback.onServiceStateChanged(serviceState) + assertThat(latest).isTrue() + serviceState.isEmergencyOnly = false callback.onServiceStateChanged(serviceState) - assertThat(latest?.isEmergencyOnly).isEqualTo(false) + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun cdmaLevelUpdates() = + runBlocking(IMMEDIATE) { + var latest: Int? = null + val job = underTest.cdmaLevel.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() + var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) + + assertThat(latest).isEqualTo(2) + + // gsmLevel updates, no change to cdmaLevel + strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true) + + assertThat(latest).isEqualTo(2) job.cancel() } @Test - fun testFlowForSubId_signalStrengths_levelsUpdate() = + fun gsmLevelUpdates() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Int? = null + val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() - val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) + var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) callback.onSignalStrengthsChanged(strength) - assertThat(latest?.isGsm).isEqualTo(true) - assertThat(latest?.primaryLevel).isEqualTo(1) - assertThat(latest?.cdmaLevel).isEqualTo(2) + assertThat(latest).isEqualTo(1) + + strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) + + assertThat(latest).isEqualTo(3) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_connected() = + fun isGsm() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.isGsm.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() + var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) + + assertThat(latest).isTrue() + + strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = false) + callback.onSignalStrengthsChanged(strength) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun dataConnectionState_connected() = + runBlocking(IMMEDIATE) { + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_CONNECTED, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Connected) + assertThat(latest).isEqualTo(DataConnectionState.Connected) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_connecting() = + fun dataConnectionState_connecting() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_CONNECTING, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Connecting) + assertThat(latest).isEqualTo(DataConnectionState.Connecting) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_disconnected() = + fun dataConnectionState_disconnected() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_DISCONNECTED, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Disconnected) + assertThat(latest).isEqualTo(DataConnectionState.Disconnected) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_disconnecting() = + fun dataConnectionState_disconnecting() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_DISCONNECTING, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Disconnecting) + assertThat(latest).isEqualTo(DataConnectionState.Disconnecting) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_suspended() = + fun dataConnectionState_suspended() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_SUSPENDED, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Suspended) + assertThat(latest).isEqualTo(DataConnectionState.Suspended) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_handoverInProgress() = + fun dataConnectionState_handoverInProgress() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_HANDOVER_IN_PROGRESS, 200 /* unused */) - assertThat(latest?.dataConnectionState) - .isEqualTo(DataConnectionState.HandoverInProgress) + assertThat(latest).isEqualTo(DataConnectionState.HandoverInProgress) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_unknown() = + fun dataConnectionState_unknown() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_UNKNOWN, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Unknown) + assertThat(latest).isEqualTo(DataConnectionState.Unknown) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_invalid() = + fun dataConnectionState_invalid() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(45, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Invalid) + assertThat(latest).isEqualTo(DataConnectionState.Invalid) job.cancel() } @Test - fun testFlowForSubId_dataActivity() = + fun dataActivity() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataActivityModel? = null + val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<DataActivityListener>() callback.onDataActivity(DATA_ACTIVITY_INOUT) - assertThat(latest?.dataActivityDirection) - .isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel()) + assertThat(latest).isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel()) job.cancel() } @Test - fun testFlowForSubId_carrierNetworkChange() = + fun carrierNetworkChange() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.carrierNetworkChangeActive.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>() callback.onCarrierNetworkChange(true) - assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true) + assertThat(latest).isEqualTo(true) + + job.cancel() + } + + @Test + fun networkType_default() = + runBlocking(IMMEDIATE) { + var latest: ResolvedNetworkType? = null + val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this) + + val expected = UnknownNetworkType + + assertThat(latest).isEqualTo(expected) job.cancel() } @Test - fun subscriptionFlow_networkType_default() = + fun networkType_unknown_hasCorrectKey() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: ResolvedNetworkType? = null + val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this) + val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() + val type = NETWORK_TYPE_UNKNOWN val expected = UnknownNetworkType + val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) } + callback.onDisplayInfoChanged(ti) - assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + assertThat(latest).isEqualTo(expected) + assertThat(latest!!.lookupKey).isEqualTo(MobileMappings.toIconKey(type)) job.cancel() } @Test - fun subscriptionFlow_networkType_updatesUsingDefault() = + fun networkType_updatesUsingDefault() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: ResolvedNetworkType? = null + val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() val type = NETWORK_TYPE_LTE @@ -371,16 +423,16 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) } callback.onDisplayInfoChanged(ti) - assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + assertThat(latest).isEqualTo(expected) job.cancel() } @Test - fun subscriptionFlow_networkType_updatesUsingOverride() = + fun networkType_updatesUsingOverride() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: ResolvedNetworkType? = null + val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() val type = OVERRIDE_NETWORK_TYPE_LTE_CA @@ -392,7 +444,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } callback.onDisplayInfoChanged(ti) - assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + assertThat(latest).isEqualTo(expected) job.cancel() } @@ -466,7 +518,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun `roaming - gsm - queries service state`() = runBlocking(IMMEDIATE) { var latest: Boolean? = null - val job = underTest.connectionInfo.onEach { latest = it.isRoaming }.launchIn(this) + val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) val serviceState = ServiceState() serviceState.roaming = false @@ -492,8 +544,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun `activity - updates from callback`() = runBlocking(IMMEDIATE) { var latest: DataActivityModel? = null - val job = - underTest.connectionInfo.onEach { latest = it.dataActivityDirection }.launchIn(this) + val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this) assertThat(latest) .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) @@ -611,8 +662,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { runBlocking(IMMEDIATE) { var latest: String? = null - val job = - underTest.connectionInfo.onEach { latest = it.operatorAlphaShort }.launchIn(this) + val job = underTest.operatorAlphaShort.onEach { latest = it }.launchIn(this) val shortName = "short name" val serviceState = ServiceState() @@ -633,7 +683,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun `connection model - isInService - not iwlan`() = runBlocking(IMMEDIATE) { var latest: Boolean? = null - val job = underTest.connectionInfo.onEach { latest = it.isInService }.launchIn(this) + val job = underTest.isInService.onEach { latest = it }.launchIn(this) val serviceState = ServiceState() serviceState.voiceRegState = STATE_IN_SERVICE @@ -658,7 +708,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun `connection model - isInService - is iwlan - voice out of service - data in service`() = runBlocking(IMMEDIATE) { var latest: Boolean? = null - val job = underTest.connectionInfo.onEach { latest = it.isInService }.launchIn(this) + val job = underTest.isInService.onEach { latest = it }.launchIn(this) // Mock the service state here so we can make it specifically IWLAN val serviceState: ServiceState = mock() 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/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index fa072fc366eb..1eb1056204cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -23,7 +23,6 @@ import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase 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 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType @@ -74,9 +73,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_level_default_unknown() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel(isGsm = true), - ) + connectionRepository.isGsm.value = true var latest: Int? = null val job = underTest.level.onEach { latest = it }.launchIn(this) @@ -89,13 +86,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_usesGsmLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL - ), - ) + connectionRepository.isGsm.value = true + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL var latest: Int? = null val job = underTest.level.onEach { latest = it }.launchIn(this) @@ -108,13 +101,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_alwaysShowCdmaTrue_stillUsesGsmLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL, - ), - ) + connectionRepository.isGsm.value = true + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL mobileIconsInteractor.alwaysUseCdmaLevel.value = true var latest: Int? = null @@ -128,9 +117,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun notGsm_level_default_unknown() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel(isGsm = false), - ) + connectionRepository.isGsm.value = false var latest: Int? = null val job = underTest.level.onEach { latest = it }.launchIn(this) @@ -142,13 +129,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun notGsm_alwaysShowCdmaTrue_usesCdmaLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL - ), - ) + connectionRepository.isGsm.value = false + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL mobileIconsInteractor.alwaysUseCdmaLevel.value = true var latest: Int? = null @@ -162,13 +145,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun notGsm_alwaysShowCdmaFalse_usesPrimaryLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL, - ), - ) + connectionRepository.isGsm.value = false + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL mobileIconsInteractor.alwaysUseCdmaLevel.value = false var latest: Int? = null @@ -197,11 +176,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_three_g() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -214,23 +190,14 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_updates_on_change() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = - DefaultNetworkType( - mobileMappingsProxy.toIconKey(FOUR_G), - ), - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(FOUR_G)) yield() assertThat(latest).isEqualTo(TelephonyIcons.FOUR_G) @@ -241,12 +208,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_5g_override_type() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = - OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)) - ), - ) + connectionRepository.resolvedNetworkType.value = + OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -259,12 +222,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_default_if_no_lookup() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = - DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)), - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -277,11 +236,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_carrierMerged_usesOverride() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = CarrierMergedNetworkType, - ), - ) + connectionRepository.resolvedNetworkType.value = CarrierMergedNetworkType var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -295,11 +250,8 @@ class MobileIconInteractorTest : SysuiTestCase() { fun `icon group - checks default data`() = runBlocking(IMMEDIATE) { mobileIconsInteractor.defaultDataSubId.value = SUB_1_ID - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -380,9 +332,7 @@ class MobileIconInteractorTest : SysuiTestCase() { var latest: Boolean? = null val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo( - MobileConnectionModel(dataConnectionState = DataConnectionState.Connected) - ) + connectionRepository.dataConnectionState.value = DataConnectionState.Connected yield() assertThat(latest).isTrue() @@ -396,9 +346,7 @@ class MobileIconInteractorTest : SysuiTestCase() { var latest: Boolean? = null val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo( - MobileConnectionModel(dataConnectionState = DataConnectionState.Disconnected) - ) + connectionRepository.dataConnectionState.value = DataConnectionState.Disconnected assertThat(latest).isFalse() @@ -411,11 +359,11 @@ class MobileIconInteractorTest : SysuiTestCase() { var latest: Boolean? = null val job = underTest.isInService.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo(MobileConnectionModel(isInService = true)) + connectionRepository.isInService.value = true assertThat(latest).isTrue() - connectionRepository.setConnectionInfo(MobileConnectionModel(isInService = false)) + connectionRepository.isInService.value = false assertThat(latest).isFalse() @@ -429,22 +377,13 @@ class MobileIconInteractorTest : SysuiTestCase() { val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - isRoaming = false, - ) - ) + connectionRepository.isGsm.value = true + connectionRepository.isRoaming.value = false yield() assertThat(latest).isFalse() - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - isRoaming = true, - ) - ) + connectionRepository.isRoaming.value = true yield() assertThat(latest).isTrue() @@ -459,23 +398,15 @@ class MobileIconInteractorTest : SysuiTestCase() { val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) connectionRepository.cdmaRoaming.value = false - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - isRoaming = true, - ) - ) + connectionRepository.isGsm.value = false + connectionRepository.isRoaming.value = true yield() assertThat(latest).isFalse() connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - isRoaming = false, - ) - ) + connectionRepository.isGsm.value = false + connectionRepository.isRoaming.value = false yield() assertThat(latest).isTrue() @@ -490,25 +421,15 @@ class MobileIconInteractorTest : SysuiTestCase() { val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - isRoaming = true, - carrierNetworkChangeActive = true, - ) - ) + connectionRepository.isGsm.value = false + connectionRepository.isRoaming.value = true + connectionRepository.carrierNetworkChangeActive.value = true yield() assertThat(latest).isFalse() connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - isRoaming = true, - carrierNetworkChangeActive = true, - ) - ) + connectionRepository.isGsm.value = true yield() assertThat(latest).isFalse() @@ -526,24 +447,20 @@ class MobileIconInteractorTest : SysuiTestCase() { // Default network name, operator name is non-null, uses the operator name connectionRepository.networkName.value = DEFAULT_NAME - connectionRepository.setConnectionInfo( - MobileConnectionModel(operatorAlphaShort = testOperatorName) - ) + connectionRepository.operatorAlphaShort.value = testOperatorName yield() assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName)) // Default network name, operator name is null, uses the default - connectionRepository.setConnectionInfo(MobileConnectionModel(operatorAlphaShort = null)) + connectionRepository.operatorAlphaShort.value = null yield() assertThat(latest).isEqualTo(DEFAULT_NAME) // Derived network name, operator name non-null, uses the derived name connectionRepository.networkName.value = DERIVED_NAME - connectionRepository.setConnectionInfo( - MobileConnectionModel(operatorAlphaShort = testOperatorName) - ) + connectionRepository.operatorAlphaShort.value = testOperatorName yield() assertThat(latest).isEqualTo(DERIVED_NAME) 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/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index f0a53ae84a22..8d74c82da6b0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -29,6 +29,8 @@ import android.os.UserManager import android.provider.Settings import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.R @@ -63,6 +65,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertNotNull import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -73,6 +76,7 @@ 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.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock @@ -98,6 +102,7 @@ class UserInteractorTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var underTest: UserInteractor @@ -156,6 +161,7 @@ class UserInteractorTest : SysuiTestCase() { repository = telephonyRepository, ), broadcastDispatcher = fakeBroadcastDispatcher, + keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, @@ -180,6 +186,18 @@ class UserInteractorTest : SysuiTestCase() { } @Test + fun `testKeyguardUpdateMonitor_onKeyguardGoingAway`() = + testScope.runTest { + val argumentCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) + verify(keyguardUpdateMonitor).registerCallback(argumentCaptor.capture()) + + argumentCaptor.value.onKeyguardGoingAway() + + val lastValue = collectLastValue(underTest.dialogDismissRequests) + assertNotNull(lastValue) + } + + @Test fun `onRecordSelected - user`() = testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt index bc0881c7f141..9b74c1f05d71 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -25,6 +25,7 @@ import android.graphics.drawable.BitmapDrawable import android.os.UserManager import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase @@ -80,6 +81,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var underTest: StatusBarUserChipViewModel @@ -263,6 +265,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { repository = FakeTelephonyRepository(), ), broadcastDispatcher = fakeBroadcastDispatcher, + keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index c8b0496bb934..7780a4372976 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -23,6 +23,7 @@ import android.content.pm.UserInfo import android.os.UserManager import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase @@ -81,6 +82,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var underTest: UserSwitcherViewModel @@ -165,6 +167,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { repository = FakeTelephonyRepository(), ), broadcastDispatcher = fakeBroadcastDispatcher, + keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, 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/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 61032dc7a59c..bea049cb92dd 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -849,7 +849,6 @@ final class AutofillManagerServiceImpl } } - /** * Updates the last fill response when a view was entered. */ diff --git a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java index 592045c23372..bcf50ad5cc7a 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java @@ -356,6 +356,11 @@ final class RemoteAugmentedAutofillService public void onError() { onErrorCallback.run(); } + + @Override + public void onInflate() { + /* nothing */ + } }); if (inlineSuggestionsCallback.apply(inlineFillUi)) { diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 4a12e3843972..b54dbbf14ce2 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -276,6 +276,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") private DeathRecipient mClientVulture; + @GuardedBy("mLock") + private boolean mLoggedInlineDatasetShown; + /** * Reference to the remote service. * @@ -1981,6 +1984,22 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Session::removeFromService, this)); } + // AutofillUiCallback + @Override + public void onShown(int uiType) { + synchronized (mLock) { + if (uiType == UI_TYPE_INLINE) { + if (mLoggedInlineDatasetShown) { + // Chip inflation already logged, do not log again. + // This is needed because every chip inflation will call this. + return; + } + mLoggedInlineDatasetShown = true; + } + mService.logDatasetShown(this.id, mClientState, uiType); + } + } + // AutoFillUiCallback @Override public void requestShowFillUi(AutofillId id, int width, int height, @@ -3861,8 +3880,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState synchronized (mLock) { final ViewState currentView = mViewStates.get(mCurrentViewId); currentView.setState(ViewState.STATE_FILL_DIALOG_SHOWN); - mService.logDatasetShown(id, mClientState, UI_TYPE_DIALOG); - mPresentationStatsEventLogger.maybeSetCountShown( response.getDatasets(), mCurrentViewId); mPresentationStatsEventLogger.maybeSetDisplayPresentationType(UI_TYPE_DIALOG); @@ -3892,10 +3909,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // back a response via callback. final ViewState currentView = mViewStates.get(mCurrentViewId); currentView.setState(ViewState.STATE_INLINE_SHOWN); - // TODO(b/248378401): Fix it to log showed only when IME asks for inflation, - // rather than here where framework sends back the response. - mService.logDatasetShown(id, mClientState, UI_TYPE_INLINE); - // TODO(b/234475358): Log more accurate value of number of inline suggestions // shown, inflated, and filtered. mPresentationStatsEventLogger.maybeSetCountShown( @@ -3912,7 +3925,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState targetLabel, targetIcon, this, id, mCompatMode); synchronized (mLock) { - mService.logDatasetShown(id, mClientState, UI_TYPE_MENU); mPresentationStatsEventLogger.maybeSetCountShown( response.getDatasets(), mCurrentViewId); mPresentationStatsEventLogger.maybeSetDisplayPresentationType(UI_TYPE_MENU); @@ -4124,6 +4136,12 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return false; } + // Set this to false - we are requesting a new inline request and haven't shown + // anything yet + synchronized (mLock) { + mLoggedInlineDatasetShown = false; + } + final InlineFillUi.InlineFillUiInfo inlineFillUiInfo = new InlineFillUi.InlineFillUiInfo(request, focusedId, filterText, remoteRenderService, userId, id); @@ -4153,6 +4171,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState InlineFillUi.emptyUi(focusedId)); } } + + @Override + public void onInflate() { + Session.this.onShown(UI_TYPE_INLINE); + } }); return mInlineSessionController.setInlineFillUiLocked(inlineFillUi); } diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java index 7db6e6f43f4b..829161037832 100644 --- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java @@ -98,6 +98,7 @@ public final class AutoFillUI { void cancelSession(); void requestShowSoftInput(AutofillId id); void requestFallbackFromFillDialog(); + void onShown(int uiType); } public AutoFillUI(@NonNull Context context) { @@ -237,6 +238,13 @@ public final class AutoFillUI { } @Override + public void onShown() { + if (mCallback != null) { + mCallback.onShown(UI_TYPE_MENU); + } + } + + @Override public void onDatasetPicked(Dataset dataset) { log.setType(MetricsEvent.TYPE_ACTION); hideFillUiUiThread(callback, true); @@ -424,6 +432,11 @@ public final class AutoFillUI { } @Override + public void onShown() { + callback.onShown(UI_TYPE_DIALOG); + } + + @Override public void onDatasetPicked(Dataset dataset) { log(MetricsEvent.TYPE_ACTION); hideFillDialogUiThread(callback); diff --git a/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java b/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java index 72a38eb14e21..dec0e7666b1b 100644 --- a/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java @@ -83,6 +83,7 @@ final class DialogFillUi { void onDatasetPicked(@NonNull Dataset dataset); void onDismissed(); void onCanceled(); + void onShown(); void startIntentSender(IntentSender intentSender); } @@ -148,7 +149,7 @@ final class DialogFillUi { mDialog.setContentView(decor); setDialogParamsAsBottomSheet(); mDialog.setOnCancelListener((d) -> mCallback.onCanceled()); - + mDialog.setOnShowListener((d) -> mCallback.onShown()); show(); } diff --git a/services/autofill/java/com/android/server/autofill/ui/FillUi.java b/services/autofill/java/com/android/server/autofill/ui/FillUi.java index 8fbdd81cc4cc..76f4505bbb4a 100644 --- a/services/autofill/java/com/android/server/autofill/ui/FillUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/FillUi.java @@ -86,6 +86,7 @@ final class FillUi { void onDatasetPicked(@NonNull Dataset dataset); void onCanceled(); void onDestroy(); + void onShown(); void requestShowFillUi(int width, int height, IAutofillWindowPresenter windowPresenter); void requestHideFillUi(); @@ -706,6 +707,7 @@ final class FillUi { mWm.addView(mContentView, params); mOverlayControl.hideOverlays(); mShowing = true; + mCallback.onShown(); } else { mWm.updateViewLayout(mContentView, params); } diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java b/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java index ff175904fb61..ac8d962c8eaa 100644 --- a/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/InlineFillUi.java @@ -334,6 +334,17 @@ public final class InlineFillUi { * Callback on errors. */ void onError(); + + /** + * Callback when the when the IME inflates the suggestion + * + * This goes through the following path: + * 1. IME Chip inflation inflate() -> + * 2. RemoteInlineSuggestionUi::handleInlineSuggestionUiReady() -> + * 3. RemoteInlineSuggestionViewConnector::onRender() -> + * 4. InlineSuggestionUiCallback::onInflate() + */ + void onInflate(); } /** diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java index 9d4c9ebbf592..52109ba44917 100644 --- a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java +++ b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java @@ -198,6 +198,11 @@ final class InlineSuggestionFactory { public void onError() { Slog.w(TAG, "An error happened on the tooltip"); } + + @Override + public void onInflate() { + /* nothing */ + } }; InlinePresentation tooltipInline = new InlinePresentation(tooltipPresentation.getSlice(), diff --git a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionUi.java b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionUi.java index 368f71760b0d..ddb60c647626 100644 --- a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionUi.java @@ -221,6 +221,7 @@ final class RemoteInlineSuggestionUi { if (surfacePackage != null) { surfacePackage.release(); } + mRemoteInlineSuggestionViewConnector.onRender(); } private void handleOnClick() { diff --git a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java index 46d435d8811d..70443f9153d1 100644 --- a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java +++ b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java @@ -54,6 +54,8 @@ final class RemoteInlineSuggestionViewConnector { @NonNull private final Runnable mOnErrorCallback; @NonNull + private final Runnable mOnInflateCallback; + @NonNull private final Consumer<IntentSender> mStartIntentSenderFromClientApp; RemoteInlineSuggestionViewConnector( @@ -70,6 +72,7 @@ final class RemoteInlineSuggestionViewConnector { mOnAutofillCallback = onAutofillCallback; mOnErrorCallback = uiCallback::onError; + mOnInflateCallback = uiCallback::onInflate; mStartIntentSenderFromClientApp = uiCallback::startIntentSender; } @@ -103,6 +106,10 @@ final class RemoteInlineSuggestionViewConnector { mOnErrorCallback.run(); } + public void onRender() { + mOnInflateCallback.run(); + } + /** * Handles the callback for transferring the touch event on the remote view to the IME * process. 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/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index 56f32962ae99..92be0943c9f4 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -1650,15 +1650,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { mContext, 0, snoozeIntent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); final Intent viewIntent = buildViewDataUsageIntent(res, policy.template); - // TODO: Resolve to single code path. - if (UserManager.isHeadlessSystemUserMode()) { - builder.setContentIntent(PendingIntent.getActivityAsUser( - mContext, 0, viewIntent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE, - /* options= */ null, UserHandle.CURRENT)); - } else { - builder.setContentIntent(PendingIntent.getActivity( - mContext, 0, viewIntent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); - } + setContentIntent(builder, viewIntent); break; } case TYPE_LIMIT: { @@ -1679,15 +1671,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { builder.setSmallIcon(R.drawable.stat_notify_disabled_data); final Intent intent = buildNetworkOverLimitIntent(res, policy.template); - // TODO: Resolve to single code path. - if (UserManager.isHeadlessSystemUserMode()) { - builder.setContentIntent(PendingIntent.getActivityAsUser( - mContext, 0, intent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE, - /* options= */ null, UserHandle.CURRENT)); - } else { - builder.setContentIntent(PendingIntent.getActivity( - mContext, 0, intent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); - } + setContentIntent(builder, intent); break; } case TYPE_LIMIT_SNOOZED: { @@ -1711,15 +1695,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { builder.setChannelId(SystemNotificationChannels.NETWORK_STATUS); final Intent intent = buildViewDataUsageIntent(res, policy.template); - // TODO: Resolve to single code path. - if (UserManager.isHeadlessSystemUserMode()) { - builder.setContentIntent(PendingIntent.getActivityAsUser( - mContext, 0, intent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE, - /* options= */ null, UserHandle.CURRENT)); - } else { - builder.setContentIntent(PendingIntent.getActivity( - mContext, 0, intent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); - } + setContentIntent(builder, intent); break; } case TYPE_RAPID: { @@ -1739,15 +1715,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { mContext, 0, snoozeIntent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); final Intent viewIntent = buildViewDataUsageIntent(res, policy.template); - // TODO: Resolve to single code path. - if (UserManager.isHeadlessSystemUserMode()) { - builder.setContentIntent(PendingIntent.getActivityAsUser( - mContext, 0, viewIntent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE, - /* options= */ null, UserHandle.CURRENT)); - } else { - builder.setContentIntent(PendingIntent.getActivity( - mContext, 0, viewIntent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); - } + setContentIntent(builder, viewIntent); break; } default: { @@ -1765,6 +1733,17 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { mActiveNotifs.add(notificationId); } + private void setContentIntent(Notification.Builder builder, Intent intent) { + if (UserManager.isHeadlessSystemUserMode()) { + builder.setContentIntent(PendingIntent.getActivityAsUser( + mContext, 0, intent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE, + /* options= */ null, UserHandle.CURRENT)); + } else { + builder.setContentIntent(PendingIntent.getActivity( + mContext, 0, intent, FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); + } + } + private void cancelNotification(NotificationId notificationId) { mContext.getSystemService(NotificationManager.class).cancel(notificationId.getTag(), notificationId.getId()); 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/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index de5f0c4d523f..d3ee52c48448 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -7218,6 +7218,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService * TODO(b/182523293): This should be removed once we finish migration of permission storage. */ void writeSettingsLPrTEMP(boolean sync) { + snapshotComputer(false); mPermissionManager.writeLegacyPermissionsTEMP(mSettings.mPermissions); mSettings.writeLPr(mLiveComputer, sync); } 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/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java index 5096ad1faf73..c2d4ac694c39 100644 --- a/services/core/java/com/android/server/power/ShutdownThread.java +++ b/services/core/java/com/android/server/power/ShutdownThread.java @@ -17,6 +17,7 @@ package com.android.server.power; +import android.app.ActivityManagerInternal; import android.app.AlertDialog; import android.app.Dialog; import android.app.IActivityManager; @@ -25,10 +26,12 @@ import android.app.admin.SecurityLog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; +import android.content.IIntentReceiver; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManagerInternal; import android.media.AudioAttributes; +import android.os.Bundle; import android.os.FileUtils; import android.os.Handler; import android.os.PowerManager; @@ -51,7 +54,6 @@ import android.view.WindowManager; import com.android.server.LocalServices; import com.android.server.RescueParty; -import com.android.server.pm.PackageManagerService; import com.android.server.statusbar.StatusBarManagerInternal; import java.io.File; @@ -448,13 +450,6 @@ public final class ShutdownThread extends Thread { new File(CHECK_POINTS_FILE_BASENAME)); dumpCheckPointsThread.start(); - BroadcastReceiver br = new BroadcastReceiver() { - @Override public void onReceive(Context context, Intent intent) { - // We don't allow apps to cancel this, so ignore the result. - actionDone(); - } - }; - /* * Write a system property in case the system_server reboots before we * get to the actual hardware restart. If that happens, we'll retry at @@ -490,8 +485,16 @@ public final class ShutdownThread extends Thread { mActionDone = false; Intent intent = new Intent(Intent.ACTION_SHUTDOWN); intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY); - mContext.sendOrderedBroadcastAsUser(intent, - UserHandle.ALL, null, br, mHandler, 0, null, null); + final ActivityManagerInternal activityManagerInternal = LocalServices.getService( + ActivityManagerInternal.class); + activityManagerInternal.broadcastIntentWithCallback(intent, + new IIntentReceiver.Stub() { + @Override + public void performReceive(Intent intent, int resultCode, String data, + Bundle extras, boolean ordered, boolean sticky, int sendingUser) { + mHandler.post(ShutdownThread.this::actionDone); + } + }, null, UserHandle.USER_ALL, null, null, null); final long endTime = SystemClock.elapsedRealtime() + MAX_BROADCAST_TIME; synchronized (mActionDoneSync) { 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/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 9aa7e56f8877..fa3a186a6153 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -702,8 +702,8 @@ final class AccessibilityController { + AppTransition.appTransitionOldToString(transition) + " displayId: " + displayId); } - final boolean magnifying = mMagnifedViewport.isMagnifying(); - if (magnifying) { + final boolean isMagnifierActivated = isForceShowingMagnifiableBounds(); + if (isMagnifierActivated) { switch (transition) { case WindowManager.TRANSIT_OLD_ACTIVITY_OPEN: case WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN: @@ -727,8 +727,8 @@ final class AccessibilityController { Slog.i(LOG_TAG, "Window transition: " + WindowManager.transitTypeToString(type) + " displayId: " + displayId); } - final boolean magnifying = mMagnifedViewport.isMagnifying(); - if (magnifying) { + final boolean isMagnifierActivated = isForceShowingMagnifiableBounds(); + if (isMagnifierActivated) { // All opening/closing situations. switch (type) { case WindowManager.TRANSIT_OPEN: @@ -751,12 +751,12 @@ final class AccessibilityController { + AppTransition.appTransitionOldToString(transition) + " displayId: " + windowState.getDisplayId()); } - final boolean magnifying = mMagnifedViewport.isMagnifying(); + final boolean isMagnifierActivated = isForceShowingMagnifiableBounds(); final int type = windowState.mAttrs.type; switch (transition) { case WindowManagerPolicy.TRANSIT_ENTER: case WindowManagerPolicy.TRANSIT_SHOW: { - if (!magnifying) { + if (!isMagnifierActivated) { break; } switch (type) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9def87c3d77a..28b974c7330a 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -573,6 +573,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A Drawable mEnterpriseThumbnailDrawable; + boolean mPauseSchedulePendingForPip = false; + private void updateEnterpriseThumbnailDrawable(Context context) { DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); mEnterpriseThumbnailDrawable = dpm.getResources().getDrawable( @@ -1502,6 +1504,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mLastReportedMultiWindowMode = inPictureInPictureMode; ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS, true /* ignoreVisibility */); + if (inPictureInPictureMode && findMainWindow() == null) { + // Prevent malicious app entering PiP without valid WindowState, which can in turn + // result a non-touchable PiP window since the InputConsumer for PiP requires it. + EventLog.writeEvent(0x534e4554, "265293293", -1, ""); + removeImmediately(); + } } } @@ -5228,8 +5236,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 @@ -8119,9 +8126,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // The smallest screen width is the short side of screen bounds. Because the bounds // and density won't be changed, smallestScreenWidthDp is also fixed. overrideConfig.smallestScreenWidthDp = fullConfig.smallestScreenWidthDp; - // TODO(b/264276741): Check whether the runtime orietnation request is fixed rather than - // the manifest orientation which may be obsolete. - if (info.isFixedOrientation()) { + if (ActivityInfo.isFixedOrientation(getOverrideOrientation())) { // lock rotation too. When in size-compat, onConfigurationChanged will watch for and // apply runtime rotation changes. overrideConfig.windowConfiguration.setRotation( @@ -8146,7 +8151,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 @@ -8209,9 +8220,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (isFixedOrientationLetterboxAllowed) { resolveFixedOrientationConfiguration(newParentConfiguration); } - - if (getCompatDisplayInsets() != null) { - resolveSizeCompatModeConfiguration(newParentConfiguration); + final CompatDisplayInsets compatDisplayInsets = getCompatDisplayInsets(); + if (compatDisplayInsets != null) { + resolveSizeCompatModeConfiguration(newParentConfiguration, compatDisplayInsets); } else if (inMultiWindowMode() && !isFixedOrientationLetterboxAllowed) { // We ignore activities' requested orientation in multi-window modes. They may be // taken into consideration in resolveFixedOrientationConfiguration call above. @@ -8228,7 +8239,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A resolveAspectRatioRestriction(newParentConfiguration); } - if (isFixedOrientationLetterboxAllowed || getCompatDisplayInsets() != null + if (isFixedOrientationLetterboxAllowed || compatDisplayInsets != null // In fullscreen, can be letterboxed for aspect ratio. || !inMultiWindowMode()) { updateResolvedBoundsPosition(newParentConfiguration); @@ -8236,7 +8247,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean isIgnoreOrientationRequest = mDisplayContent != null && mDisplayContent.getIgnoreOrientationRequest(); - if (getCompatDisplayInsets() == null + if (compatDisplayInsets == null // for size compat mode set in updateCompatDisplayInsets // Fixed orientation letterboxing is possible on both large screen devices // with ignoreOrientationRequest enabled and on phones in split screen even with @@ -8283,7 +8294,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A info.neverSandboxDisplayApis(sConstrainDisplayApisConfig), info.alwaysSandboxDisplayApis(sConstrainDisplayApisConfig), !matchParentBounds(), - getCompatDisplayInsets() != null, + compatDisplayInsets != null, shouldCreateCompatDisplayInsets()); } resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); @@ -8295,7 +8306,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** * @return The orientation to use to understand if reachability is enabled. */ - @ActivityInfo.ScreenOrientation + @Configuration.Orientation int getOrientationForReachability() { return mLetterboxUiController.hasInheritedLetterboxBehavior() ? mLetterboxUiController.getInheritedOrientation() @@ -8699,7 +8710,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * Resolves consistent screen configuration for orientation and rotation changes without * inheriting the parent bounds. */ - private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration) { + private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration, + @NonNull CompatDisplayInsets compatDisplayInsets) { final Configuration resolvedConfig = getResolvedOverrideConfiguration(); final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); @@ -8720,13 +8732,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ? requestedOrientation // We should use the original orientation of the activity when possible to avoid // forcing the activity in the opposite orientation. - : getCompatDisplayInsets().mOriginalRequestedOrientation != ORIENTATION_UNDEFINED - ? getCompatDisplayInsets().mOriginalRequestedOrientation + : compatDisplayInsets.mOriginalRequestedOrientation != ORIENTATION_UNDEFINED + ? compatDisplayInsets.mOriginalRequestedOrientation : newParentConfiguration.orientation; int rotation = newParentConfiguration.windowConfiguration.getRotation(); final boolean isFixedToUserRotation = mDisplayContent == null || mDisplayContent.getDisplayRotation().isFixedToUserRotation(); - if (!isFixedToUserRotation && !getCompatDisplayInsets().mIsFloating) { + if (!isFixedToUserRotation && !compatDisplayInsets.mIsFloating) { // Use parent rotation because the original display can be rotated. resolvedConfig.windowConfiguration.setRotation(rotation); } else { @@ -8742,11 +8754,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // rely on them to contain the original and unchanging width and height of the app. final Rect containingAppBounds = new Rect(); final Rect containingBounds = mTmpBounds; - getCompatDisplayInsets().getContainerBounds(containingAppBounds, containingBounds, rotation, + compatDisplayInsets.getContainerBounds(containingAppBounds, containingBounds, rotation, orientation, orientationRequested, isFixedToUserRotation); resolvedBounds.set(containingBounds); // The size of floating task is fixed (only swap), so the aspect ratio is already correct. - if (!getCompatDisplayInsets().mIsFloating) { + if (!compatDisplayInsets.mIsFloating) { mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, containingAppBounds, containingBounds); } @@ -8755,7 +8767,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // are calculated in compat container space. The actual position on screen will be applied // later, so the calculation is simpler that doesn't need to involve offset from parent. getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration, - getCompatDisplayInsets()); + compatDisplayInsets); // Use current screen layout as source because the size of app is independent to parent. resolvedConfig.screenLayout = computeScreenLayout( getConfiguration().screenLayout, resolvedConfig.screenWidthDp, diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 923ca794e840..fef4c6db4786 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3625,7 +3625,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { null /* launchIntoPipHostActivity */, "enterPictureInPictureMode", transition); // Continue the pausing process after entering pip. - if (r.isState(PAUSING)) { + if (r.isState(PAUSING) && r.mPauseSchedulePendingForPip) { r.getTask().schedulePauseActivity(r, false /* userLeaving */, false /* pauseImmediately */, true /* autoEnteringPip */, "auto-pip"); } 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/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index ce3379ec4394..389c908bfb6b 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -20,11 +20,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.Display.TYPE_INTERNAL; import static android.view.InsetsFrameProvider.SOURCE_FRAME; -import static android.view.InsetsState.ITYPE_CAPTION_BAR; -import static android.view.InsetsState.ITYPE_CLIMATE_BAR; -import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS; @@ -198,6 +193,11 @@ public class DisplayPolicy { private boolean mCanSystemBarsBeShownByUser; + /** + * Let remote insets controller control system bars regardless of other settings. + */ + private boolean mRemoteInsetsControllerControlsSystemBars; + StatusBarManagerInternal getStatusBarManagerInternal() { synchronized (mServiceAcquireLock) { if (mStatusBarManagerInternal == null) { @@ -781,6 +781,17 @@ public class DisplayPolicy { return mScreenOnListener; } + + boolean isRemoteInsetsControllerControllingSystemBars() { + return mRemoteInsetsControllerControlsSystemBars; + } + + @VisibleForTesting + void setRemoteInsetsControllerControlsSystemBars( + boolean remoteInsetsControllerControlsSystemBars) { + mRemoteInsetsControllerControlsSystemBars = remoteInsetsControllerControlsSystemBars; + } + public void screenTurnedOn(ScreenOnListener screenOnListener) { synchronized (mLock) { mScreenOnEarly = true; @@ -1029,7 +1040,6 @@ public class DisplayPolicy { android.Manifest.permission.STATUS_BAR_SERVICE, callingPid, callingUid, "DisplayPolicy"); } - enforceSingleInsetsTypeCorrespondingToWindowType(attrs.providedInsets); } return ADD_OKAY; } @@ -1061,7 +1071,7 @@ public class DisplayPolicy { final TriConsumer<DisplayFrames, WindowContainer, Rect> frameProvider = getFrameProvider(win, provider, i); final InsetsFrameProvider.InsetsSizeOverride[] overrides = - provider.insetsSizeOverrides; + provider.getInsetsSizeOverrides(); final SparseArray<TriConsumer<DisplayFrames, WindowContainer, Rect>> overrideProviders; if (overrides != null) { @@ -1070,19 +1080,14 @@ public class DisplayPolicy { final TriConsumer<DisplayFrames, WindowContainer, Rect> overrideFrameProvider = getOverrideFrameProvider(win, i, j); - overrideProviders.put(overrides[j].windowType, overrideFrameProvider); + overrideProviders.put(overrides[j].getWindowType(), overrideFrameProvider); } } else { overrideProviders = null; } - // TODO (b/234093736): Let InsetsFrameProvider have the following fields: - // - IBinder owner. - // - int index. - // - @InsetsType int type. - // So we can create the id by using InsetsSource#createId. - // And we won't need toPublicType anymore. - final int id = provider.type; - final @InsetsType int type = InsetsState.toPublicType(id); + final @InsetsType int type = provider.getType(); + final int id = InsetsSource.createId( + provider.getOwner(), provider.getIndex(), type); mDisplayContent.getInsetsStateController().getOrCreateSourceProvider(id, type) .setWindowContainer(win, frameProvider, overrideProviders); mInsetsSourceWindowsExceptIme.add(win); @@ -1093,7 +1098,7 @@ public class DisplayPolicy { @Nullable private TriConsumer<DisplayFrames, WindowContainer, Rect> getFrameProvider(WindowState win, InsetsFrameProvider provider, int index) { - if (provider.insetsSize == null && provider.source == SOURCE_FRAME) { + if (provider.getInsetsSize() == null && provider.getSource() == SOURCE_FRAME) { return null; } return (displayFrames, windowContainer, inOutFrame) -> { @@ -1101,8 +1106,8 @@ public class DisplayPolicy { final InsetsFrameProvider ifp = lp.providedInsets[index]; InsetsFrameProvider.calculateInsetsFrame(displayFrames.mUnrestricted, windowContainer.getBounds(), displayFrames.mDisplayCutoutSafe, inOutFrame, - ifp.source, ifp.insetsSize, lp.privateFlags, - ifp.minimalInsetsSizeInDisplayCutoutSafe); + ifp.getSource(), ifp.getInsetsSize(), lp.privateFlags, + ifp.getMinimalInsetsSizeInDisplayCutoutSafe()); }; } @@ -1114,8 +1119,8 @@ public class DisplayPolicy { final InsetsFrameProvider ifp = lp.providedInsets[index]; InsetsFrameProvider.calculateInsetsFrame(displayFrames.mUnrestricted, windowContainer.getBounds(), displayFrames.mDisplayCutoutSafe, inOutFrame, - ifp.source, ifp.insetsSizeOverrides[overrideIndex].insetsSize, lp.privateFlags, - null); + ifp.getSource(), ifp.getInsetsSizeOverrides()[overrideIndex].getInsetsSize(), + lp.privateFlags, null /* displayCutoutSafeInsetsSize */); }; } @@ -1141,24 +1146,6 @@ public class DisplayPolicy { }; } - private static void enforceSingleInsetsTypeCorrespondingToWindowType( - InsetsFrameProvider[] providers) { - int count = 0; - for (InsetsFrameProvider provider : providers) { - switch (provider.type) { - case ITYPE_NAVIGATION_BAR: - case ITYPE_STATUS_BAR: - case ITYPE_CLIMATE_BAR: - case ITYPE_EXTRA_NAVIGATION_BAR: - case ITYPE_CAPTION_BAR: - if (++count > 1) { - throw new IllegalArgumentException( - "Multiple InsetsTypes corresponding to Window type"); - } - } - } - } - /** * Called when a window is being removed from a window manager. Must not * throw an exception -- clean up as much as possible. @@ -1664,6 +1651,8 @@ public class DisplayPolicy { mRightGestureInset = mGestureNavigationSettingsObserver.getRightSensitivity(res); mNavigationBarAlwaysShowOnSideGesture = res.getBoolean(R.bool.config_navBarAlwaysShowOnSideEdgeGesture); + mRemoteInsetsControllerControlsSystemBars = res.getBoolean( + R.bool.config_remoteInsetsControllerControlsSystemBars); updateConfigurationAndScreenSizeDependentBehaviors(); @@ -2553,7 +2542,7 @@ public class DisplayPolicy { pw.print(mForceShowNavigationBarEnabled); pw.print(" mAllowLockscreenWhenOn="); pw.println(mAllowLockscreenWhenOn); pw.print(prefix); pw.print("mRemoteInsetsControllerControlsSystemBars="); - pw.println(mDisplayContent.getInsetsPolicy().getRemoteInsetsControllerControlsSystemBars()); + pw.println(mRemoteInsetsControllerControlsSystemBars); pw.print(prefix); pw.println("mDecorInsetsInfo:"); for (int rotation = 0; rotation < mDecorInsets.mInfoForRotation.length; rotation++) { final DecorInsets.Info info = mDecorInsets.mInfoForRotation[rotation]; @@ -2662,10 +2651,9 @@ public class DisplayPolicy { */ private static boolean intersectsAnyInsets(Rect bounds, InsetsState insetsState, @InsetsType int insetsType) { - final ArraySet<Integer> internalTypes = InsetsState.toInternalType(insetsType); - for (int i = 0; i < internalTypes.size(); i++) { - final InsetsSource source = insetsState.peekSource(internalTypes.valueAt(i)); - if (source == null || !source.isVisible()) { + for (int i = insetsState.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source = insetsState.sourceAt(i); + if ((source.getType() & insetsType) == 0 || !source.isVisible()) { continue; } if (Rect.intersects(bounds, source.getFrame())) { diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 210a7d9538c7..868a15d3c977 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -116,11 +116,6 @@ class InsetsPolicy { private @InsetsType int mShowingTransientTypes; private boolean mAnimatingShown; - /** - * Let remote insets controller control system bars regardless of other settings. - */ - private boolean mRemoteInsetsControllerControlsSystemBars; - private final boolean mHideNavBarForKeyboard; private final float[] mTmpFloat9 = new float[9]; @@ -129,22 +124,9 @@ class InsetsPolicy { mDisplayContent = displayContent; mPolicy = displayContent.getDisplayPolicy(); final Resources r = mPolicy.getContext().getResources(); - mRemoteInsetsControllerControlsSystemBars = r.getBoolean( - R.bool.config_remoteInsetsControllerControlsSystemBars); mHideNavBarForKeyboard = r.getBoolean(R.bool.config_hideNavBarForKeyboard); } - boolean getRemoteInsetsControllerControlsSystemBars() { - return mRemoteInsetsControllerControlsSystemBars; - } - - /** - * Used only for testing. - */ - @VisibleForTesting - void setRemoteInsetsControllerControlsSystemBars(boolean controlsSystemBars) { - mRemoteInsetsControllerControlsSystemBars = controlsSystemBars; - } /** Updates the target which can control system bars. */ void updateBarControlTarget(@Nullable WindowState focusedWin) { @@ -329,18 +311,16 @@ class InsetsPolicy { state.removeSource(ID_IME); } else if (attrs.providedInsets != null) { for (InsetsFrameProvider provider : attrs.providedInsets) { - // TODO(b/234093736): Let InsetsFrameProvider return the public type and the ID. - final int sourceId = provider.type; - final @InsetsType int type = sourceId == ID_IME - ? WindowInsets.Type.ime() - : InsetsState.toPublicType(sourceId); + final int id = InsetsSource.createId( + provider.getOwner(), provider.getIndex(), provider.getType()); + final @InsetsType int type = provider.getType(); if ((type & WindowInsets.Type.systemBars()) == 0) { continue; } if (state == originalState) { state = new InsetsState(state); } - state.removeSource(sourceId); + state.removeSource(id); } } @@ -580,6 +560,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 @@ -625,7 +612,8 @@ class InsetsPolicy { if (focusedWin == null) { return false; } - if (!mRemoteInsetsControllerControlsSystemBars) { + + if (!mPolicy.isRemoteInsetsControllerControllingSystemBars()) { return false; } if (mDisplayContent == null || mDisplayContent.mRemoteInsetsControlTarget == null) { diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index 2b7a45144200..0953604511d7 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -16,8 +16,6 @@ package com.android.server.wm; -import static android.view.InsetsSource.ID_IME; - import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_INSETS; import static com.android.server.wm.InsetsSourceProviderProto.CAPTURED_LEASH; import static com.android.server.wm.InsetsSourceProviderProto.CLIENT_VISIBLE; @@ -41,7 +39,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; -import android.view.InsetsFrameProvider; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.SurfaceControl; @@ -514,33 +511,13 @@ abstract class InsetsSourceProvider { } protected void updateVisibility() { - mSource.setVisible(mServerVisible && (isMirroredSource() || mClientVisible)); + mSource.setVisible(mServerVisible && mClientVisible); ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "InsetsSource updateVisibility for %s, serverVisible: %s clientVisible: %s", WindowInsets.Type.toString(mSource.getType()), mServerVisible, mClientVisible); } - private boolean isMirroredSource() { - if (mWindowContainer == null) { - return false; - } - if (mWindowContainer.asWindowState() == null) { - return false; - } - final InsetsFrameProvider[] providers = - ((WindowState) mWindowContainer).mAttrs.providedInsets; - if (providers == null) { - return false; - } - for (int i = 0; i < providers.length; i++) { - if (providers[i].type == ID_IME) { - return true; - } - } - return false; - } - InsetsSourceControl getControl(InsetsControlTarget target) { if (target == mControlTarget) { if (!mIsLeashReadyForDispatching && mControl != null) { diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 0e1e63e84250..e4ffb8de46e0 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -138,14 +138,6 @@ class InsetsStateController { } /** - * @return The provider of a source ID or null if we don't have it. - */ - @Nullable - WindowContainerInsetsSourceProvider peekSourceProvider(int id) { - return mProviders.get(id); - } - - /** * Called when a layout pass has occurred. */ void onPostLayout() { diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 44b1cc88c0a4..7208934efd51 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -1427,7 +1427,7 @@ final class LetterboxUiController { * the first opaque activity beneath. */ boolean hasInheritedLetterboxBehavior() { - return mLetterboxConfigListener != null && !mActivityRecord.matchParentBounds(); + return mLetterboxConfigListener != null; } /** 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/Task.java b/services/core/java/com/android/server/wm/Task.java index 7433c7e58392..3680e6dff9fe 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3516,7 +3516,10 @@ class Task extends TaskFragment { info.isKeyguardOccluded = mAtmService.mKeyguardController.isDisplayOccluded(DEFAULT_DISPLAY); - info.startingWindowTypeParameter = activity.mStartingData.mTypeParams; + info.startingWindowTypeParameter = activity.mStartingData != null + ? activity.mStartingData.mTypeParams + : (StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED + | StartingWindowInfo.TYPE_PARAMETER_WINDOWLESS); if ((info.startingWindowTypeParameter & StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED) != 0) { final WindowState topMainWin = getWindow(w -> w.mAttrs.type == TYPE_BASE_APPLICATION); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 035859e4b87f..2ddb307ea430 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1637,6 +1637,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { if (prev.attachedToProcess()) { if (shouldAutoPip) { + prev.mPauseSchedulePendingForPip = true; boolean didAutoPip = mAtmService.enterPictureInPictureMode( prev, prev.pictureInPictureArgs, false /* fromClient */); ProtoLog.d(WM_DEBUG_STATES, "Auto-PIP allowed, entering PIP mode " @@ -1700,6 +1701,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { boolean pauseImmediately, boolean autoEnteringPip, String reason) { ProtoLog.v(WM_DEBUG_STATES, "Enqueueing pending pause: %s", prev); try { + prev.mPauseSchedulePendingForPip = false; EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev), prev.shortComponentName, "userLeaving=" + userLeaving, reason); diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index 3a30e4b0cf1f..b13136534de3 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.TaskInfo.cameraCompatControlStateToString; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER; @@ -43,6 +44,7 @@ import android.view.Display; import android.view.SurfaceControl; import android.window.ITaskOrganizer; import android.window.ITaskOrganizerController; +import android.window.IWindowlessStartingSurfaceCallback; import android.window.SplashScreenView; import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; @@ -656,9 +658,10 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { info.splashScreenThemeResId = launchTheme; } info.taskSnapshot = taskSnapshot; + info.appToken = activity.token; // make this happen prior than prepare surface try { - lastOrganizer.addStartingWindow(info, activity.token); + lastOrganizer.addStartingWindow(info); } catch (RemoteException e) { Slog.e(TAG, "Exception sending onTaskStart callback", e); return false; @@ -704,6 +707,55 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { } } + /** + * Create a starting surface which attach on a given surface. + * @param activity Target activity, this isn't necessary to be the top activity. + * @param root The root surface which the created surface will attach on. + * @param taskSnapshot Whether to draw snapshot. + * @param callback Called when surface is drawn and attached to the root surface. + * @return The taskId, this is a token and should be used to remove the surface, even if + * the task was removed from hierarchy. + */ + int addWindowlessStartingSurface(Task task, ActivityRecord activity, SurfaceControl root, + TaskSnapshot taskSnapshot, IWindowlessStartingSurfaceCallback callback) { + final Task rootTask = task.getRootTask(); + if (rootTask == null) { + return INVALID_TASK_ID; + } + final ITaskOrganizer lastOrganizer = mTaskOrganizers.peekLast(); + if (lastOrganizer == null) { + return INVALID_TASK_ID; + } + final StartingWindowInfo info = task.getStartingWindowInfo(activity); + info.taskInfo.taskDescription = activity.taskDescription; + info.taskSnapshot = taskSnapshot; + info.windowlessStartingSurfaceCallback = callback; + info.rootSurface = root; + try { + lastOrganizer.addStartingWindow(info); + } catch (RemoteException e) { + Slog.e(TAG, "Exception sending addWindowlessStartingSurface ", e); + return INVALID_TASK_ID; + } + return task.mTaskId; + } + + void removeWindowlessStartingSurface(int taskId, boolean immediately) { + final ITaskOrganizer lastOrganizer = mTaskOrganizers.peekLast(); + if (lastOrganizer == null || taskId == 0) { + return; + } + final StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo(); + removalInfo.taskId = taskId; + removalInfo.windowlessSurface = true; + removalInfo.removeImmediately = immediately; + try { + lastOrganizer.removeStartingWindow(removalInfo); + } catch (RemoteException e) { + Slog.e(TAG, "Exception sending removeWindowlessStartingSurface ", e); + } + } + boolean copySplashScreenView(Task task) { final Task rootTask = task.getRootTask(); if (rootTask == null) { 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/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index da7400c64b40..132f5a72aadb 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1473,7 +1473,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * {@link Configuration#ORIENTATION_PORTRAIT}, * {@link Configuration#ORIENTATION_UNDEFINED}). */ - @ScreenOrientation + @Configuration.Orientation int getRequestedConfigurationOrientation() { return getRequestedConfigurationOrientation(false /* forDisplay */); } @@ -1491,7 +1491,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * {@link Configuration#ORIENTATION_PORTRAIT}, * {@link Configuration#ORIENTATION_UNDEFINED}). */ - @ScreenOrientation + @Configuration.Orientation int getRequestedConfigurationOrientation(boolean forDisplay) { int requestedOrientation = getOverrideOrientation(); final RootDisplayArea root = getRootDisplayArea(); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 98563f6e73fc..45cdacd503a8 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -2267,19 +2267,18 @@ public class WindowManagerService extends IWindowManager.Stub if (win.mAttrs.providedInsets == null || attrs.providedInsets == null || (win.mAttrs.providedInsets.length != attrs.providedInsets.length)) { throw new IllegalArgumentException( - "Insets types can not be changed after the window is added."); + "Insets amount can not be changed after the window is added."); } else { final int insetsTypes = attrs.providedInsets.length; for (int i = 0; i < insetsTypes; i++) { - if (win.mAttrs.providedInsets[i].type != attrs.providedInsets[i].type) { + if (!win.mAttrs.providedInsets[i].idEquals(attrs.providedInsets[i])) { throw new IllegalArgumentException( - "Insets types can not be changed after the window is " - + "added."); + "Insets ID can not be changed after the window is added."); } final InsetsFrameProvider.InsetsSizeOverride[] overrides = - win.mAttrs.providedInsets[i].insetsSizeOverrides; + win.mAttrs.providedInsets[i].getInsetsSizeOverrides(); final InsetsFrameProvider.InsetsSizeOverride[] newOverrides = - attrs.providedInsets[i].insetsSizeOverrides; + attrs.providedInsets[i].getInsetsSizeOverrides(); if (!(overrides == null && newOverrides == null)) { if (overrides == null || newOverrides == null || (overrides.length != newOverrides.length)) { @@ -2289,7 +2288,8 @@ public class WindowManagerService extends IWindowManager.Stub } else { final int overrideTypes = overrides.length; for (int j = 0; j < overrideTypes; j++) { - if (overrides[j].windowType != newOverrides[j].windowType) { + if (overrides[j].getWindowType() + != newOverrides[j].getWindowType()) { throw new IllegalArgumentException( "Insets override types can not be changed after" + " the window is added."); 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/CredentialDescriptionRegistry.java b/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java index 9c50a5ac2b16..d768d234cd20 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java +++ b/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java @@ -23,6 +23,8 @@ import android.service.credentials.CredentialEntry; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + import java.util.HashMap; import java.util.HashSet; @@ -90,6 +92,18 @@ public final class CredentialDescriptionRegistry { } } + /** Clears an existing session for a given user identifier. */ + @GuardedBy("sLock") + @VisibleForTesting + public static void clearAllSessions() { + sLock.lock(); + try { + sCredentialDescriptionSessionPerUser.clear(); + } finally { + sLock.unlock(); + } + } + private Map<String, Set<CredentialDescription>> mCredentialDescriptions; private int mTotalDescriptionCount; @@ -138,6 +152,9 @@ public final class CredentialDescriptionRegistry { public Set<FilterResult> getFilteredResultForProvider(String packageName, String flatRequestStrings) { Set<FilterResult> result = new HashSet<>(); + if (!mCredentialDescriptions.containsKey(packageName)) { + return result; + } Set<CredentialDescription> currentSet = mCredentialDescriptions.get(packageName); for (CredentialDescription containedDescription: currentSet) { if (flatRequestStrings.equals(containedDescription.getFlattenedRequestString())) { 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..91470f685920 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,9 @@ 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) */ + DEFAULT_INT_32, /* 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/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index 0ca4dfced1b6..54d2c194e63a 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -129,7 +129,6 @@ public class DataManager { private final List<PeopleService.ConversationsListener> mConversationsListeners = new ArrayList<>(1); private final Handler mHandler; - private ContentObserver mCallLogContentObserver; private ContentObserver mMmsSmsContentObserver; @@ -1106,6 +1105,7 @@ public class DataManager { @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) { mInjector.getBackgroundExecutor().execute(() -> { PackageData packageData = getPackage(packageName, user.getIdentifier()); + boolean hasCachedShortcut = false; for (ShortcutInfo shortcut : shortcuts) { if (ShortcutHelper.isConversationShortcut( shortcut, mShortcutServiceInternal, user.getIdentifier())) { @@ -1114,15 +1114,18 @@ public class DataManager { ? packageData.getConversationInfo(shortcut.getId()) : null; if (conversationInfo == null || !conversationInfo.isShortcutCachedForNotification()) { - // This is a newly cached shortcut. Clean up the existing cached - // shortcuts to ensure the cache size is under the limit. - cleanupCachedShortcuts(user.getIdentifier(), - MAX_CACHED_RECENT_SHORTCUTS - 1); + hasCachedShortcut = true; } } addOrUpdateConversationInfo(shortcut); } } + // Added at least one new conversation. Uncache older existing cached + // shortcuts to ensure the cache size is under the limit. + if (hasCachedShortcut) { + cleanupCachedShortcuts(user.getIdentifier(), + MAX_CACHED_RECENT_SHORTCUTS); + } }); } 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/Android.bp b/services/tests/servicestests/Android.bp index 90dbd539f029..6f26a5fe8970 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -31,6 +31,7 @@ android_test { "services.backup", "services.companion", "services.core", + "services.credentials", "services.devicepolicy", "services.net", "services.people", 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/credentials/CredentialDescriptionRegistryTest.java b/services/tests/servicestests/src/com/android/server/credentials/CredentialDescriptionRegistryTest.java new file mode 100644 index 000000000000..b7085f152fa0 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/credentials/CredentialDescriptionRegistryTest.java @@ -0,0 +1,197 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.credentials.CredentialDescription; +import android.credentials.RegisterCredentialDescriptionRequest; +import android.service.credentials.CredentialEntry; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Tests for CredentialDescriptionRegistry. + * + * atest FrameworksServicesTests:com.android.server.credentials.CredentialDescriptionRegistryTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class CredentialDescriptionRegistryTest { + + private static final int USER_ID_1 = 1; + private static final int USER_ID_2 = 2; + private static final String CALLING_PACKAGE_NAME = "com.credman.app"; + private static final String CALLING_PACKAGE_NAME_2 = "com.credman.app2"; + private static final String MDOC_CREDENTIAL_TYPE = "MDOC"; + private static final String PASSKEY_CREDENTIAL_TYPE = "PASSKEY"; + private static final String FLATTENED_REQUEST = "FLATTENED_REQ"; + private static final String FLATTENED_REQUEST_2 = "FLATTENED_REQ_2"; + + private CredentialDescriptionRegistry mCredentialDescriptionRegistry; + private CredentialEntry mEntry; + private CredentialEntry mEntry2; + private CredentialEntry mEntry3; + + @SuppressWarnings("GuardedBy") + @Before + public void setUp() { + CredentialDescriptionRegistry.clearAllSessions(); + mEntry = mock(CredentialEntry.class); + mEntry2 = mock(CredentialEntry.class); + mEntry3 = mock(CredentialEntry.class); + when(mEntry.getType()).thenReturn(MDOC_CREDENTIAL_TYPE); + when(mEntry2.getType()).thenReturn(MDOC_CREDENTIAL_TYPE); + when(mEntry3.getType()).thenReturn(PASSKEY_CREDENTIAL_TYPE); + mCredentialDescriptionRegistry = CredentialDescriptionRegistry.forUser(USER_ID_1); + } + + @SuppressWarnings("GuardedBy") + @Test + public void testForUser_createsUniqueInstanceForEachUserID() { + final CredentialDescriptionRegistry secondRegistry = CredentialDescriptionRegistry + .forUser(USER_ID_2); + + assertThat(mCredentialDescriptionRegistry).isNotSameInstanceAs(secondRegistry); + } + + @SuppressWarnings("GuardedBy") + @Test + public void testForUser_returnsSameInstanceForSameUserID() { + final CredentialDescriptionRegistry secondRegistry = CredentialDescriptionRegistry + .forUser(USER_ID_1); + + assertThat(mCredentialDescriptionRegistry).isSameInstanceAs(secondRegistry); + } + + @SuppressWarnings("GuardedBy") + @Test + public void testClearUserSession_removesExistingSessionForUserID() { + CredentialDescriptionRegistry.clearUserSession(USER_ID_1); + final CredentialDescriptionRegistry secondRegistry = CredentialDescriptionRegistry + .forUser(USER_ID_1); + + assertThat(mCredentialDescriptionRegistry).isNotSameInstanceAs(secondRegistry); + } + + @Test + public void testEvictProvider_existingProviders_succeeds() { + final CredentialDescription credentialDescription = + new CredentialDescription(MDOC_CREDENTIAL_TYPE, FLATTENED_REQUEST, + Collections.emptyList()); + final RegisterCredentialDescriptionRequest registerCredentialDescriptionRequest = + new RegisterCredentialDescriptionRequest(credentialDescription); + final CredentialDescription credentialDescription2 = + new CredentialDescription(MDOC_CREDENTIAL_TYPE, FLATTENED_REQUEST_2, + Collections.emptyList()); + final RegisterCredentialDescriptionRequest registerCredentialDescriptionRequest2 = + new RegisterCredentialDescriptionRequest(credentialDescription2); + + + mCredentialDescriptionRegistry + .executeRegisterRequest(registerCredentialDescriptionRequest, CALLING_PACKAGE_NAME); + mCredentialDescriptionRegistry + .executeRegisterRequest(registerCredentialDescriptionRequest2, + CALLING_PACKAGE_NAME); + mCredentialDescriptionRegistry.evictProviderWithPackageName(CALLING_PACKAGE_NAME); + Set<CredentialDescriptionRegistry.FilterResult> providers = mCredentialDescriptionRegistry + .getMatchingProviders(Set.of(FLATTENED_REQUEST)); + + assertThat(providers).isEmpty(); + } + + @Test + public void testGetMatchingProviders_existingProviders_succeeds() { + final CredentialDescription credentialDescription = + new CredentialDescription(MDOC_CREDENTIAL_TYPE, FLATTENED_REQUEST, + Collections.emptyList()); + final RegisterCredentialDescriptionRequest registerCredentialDescriptionRequest = + new RegisterCredentialDescriptionRequest(credentialDescription); + final CredentialDescription credentialDescription2 = + new CredentialDescription(MDOC_CREDENTIAL_TYPE, FLATTENED_REQUEST, + Collections.emptyList()); + final RegisterCredentialDescriptionRequest registerCredentialDescriptionRequest2 = + new RegisterCredentialDescriptionRequest(credentialDescription2); + + + mCredentialDescriptionRegistry + .executeRegisterRequest(registerCredentialDescriptionRequest, + CALLING_PACKAGE_NAME); + mCredentialDescriptionRegistry + .executeRegisterRequest(registerCredentialDescriptionRequest2, + CALLING_PACKAGE_NAME_2); + + Set<CredentialDescriptionRegistry.FilterResult> providers = mCredentialDescriptionRegistry + .getMatchingProviders(Set.of(FLATTENED_REQUEST)); + Set<String> packageNames = providers.stream().map( + filterResult -> filterResult.mPackageName).collect(Collectors.toSet()); + + assertThat(providers).hasSize(2); + assertThat(packageNames).contains(CALLING_PACKAGE_NAME); + assertThat(packageNames).contains(CALLING_PACKAGE_NAME_2); + } + + @Test + public void testExecuteRegisterRequest_noProviders_filterSucceedsWithNoResults() { + List<CredentialDescriptionRegistry.FilterResult> results = mCredentialDescriptionRegistry + .getFilteredResultForProvider(CALLING_PACKAGE_NAME, + FLATTENED_REQUEST).stream().toList(); + + assertThat(results).isEmpty(); + } + + @Test + public void testExecuteRegisterRequest_existingProviders_filterSucceeds() { + final CredentialDescription credentialDescription = + new CredentialDescription(MDOC_CREDENTIAL_TYPE, + FLATTENED_REQUEST, + List.of(mEntry, mEntry2)); + final CredentialDescription credentialDescription2 = + new CredentialDescription(PASSKEY_CREDENTIAL_TYPE, + FLATTENED_REQUEST_2, + List.of(mEntry3)); + final RegisterCredentialDescriptionRequest registerCredentialDescriptionRequest = + new RegisterCredentialDescriptionRequest(Set.of(credentialDescription, + credentialDescription2)); + + mCredentialDescriptionRegistry + .executeRegisterRequest(registerCredentialDescriptionRequest, CALLING_PACKAGE_NAME); + + List<CredentialDescriptionRegistry.FilterResult> results = mCredentialDescriptionRegistry + .getFilteredResultForProvider(CALLING_PACKAGE_NAME, FLATTENED_REQUEST) + .stream().toList(); + + assertThat(results).hasSize(1); + assertThat(results.get(0).mCredentialEntries).hasSize(2); + assertThat(results.get(0).mCredentialEntries.get(0)).isSameInstanceAs(mEntry); + assertThat(results.get(0).mCredentialEntries.get(1)).isSameInstanceAs(mEntry2); + } + +} diff --git a/services/tests/servicestests/src/com/android/server/credentials/OWNERS b/services/tests/servicestests/src/com/android/server/credentials/OWNERS new file mode 100644 index 000000000000..cc73854c665e --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/credentials/OWNERS @@ -0,0 +1 @@ +include platform/frameworks/base:/core/java/android/credentials/OWNERS
\ No newline at end of file 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..d071f1314f35 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -34,7 +34,6 @@ import static android.view.Display.FLAG_PRIVATE; import static android.view.Display.INVALID_DISPLAY; import static android.view.DisplayCutout.BOUNDS_POSITION_TOP; import static android.view.DisplayCutout.fromBoundingRect; -import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; @@ -1557,15 +1556,16 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(mNotificationShadeWindow.isAnimating(PARENTS, ANIMATION_TYPE_TOKEN_TRANSFORM)); // If the visibility of insets state is changed, the rotated state should be updated too. + final int statusBarId = mStatusBarWindow.getControllableInsetProvider().getSource().getId(); final InsetsState rotatedState = app.getFixedRotationTransformInsetsState(); final InsetsState state = mDisplayContent.getInsetsStateController().getRawInsetsState(); - assertEquals(state.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()), - rotatedState.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars())); - state.setSourceVisible(ITYPE_STATUS_BAR, - !rotatedState.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars())); + assertEquals(state.isSourceOrDefaultVisible(statusBarId, statusBars()), + rotatedState.isSourceOrDefaultVisible(statusBarId, statusBars())); + state.setSourceVisible(statusBarId, + !rotatedState.isSourceOrDefaultVisible(statusBarId, statusBars())); mDisplayContent.getInsetsStateController().notifyInsetsChanged(); - assertEquals(state.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()), - rotatedState.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars())); + assertEquals(state.isSourceOrDefaultVisible(statusBarId, statusBars()), + rotatedState.isSourceOrDefaultVisible(statusBarId, statusBars())); final Rect outFrame = new Rect(); final Rect outInsets = new Rect(); @@ -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/DisplayPolicyLayoutTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java index 6656f4c79022..695a72e56232 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java @@ -16,37 +16,32 @@ package com.android.server.wm; -import static android.view.InsetsState.ITYPE_CLIMATE_BAR; -import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_STATUS_BAR; -import static android.view.InsetsState.ITYPE_TOP_GESTURES; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_90; import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; -import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.spy; -import static org.testng.Assert.expectThrows; -import android.graphics.Insets; import android.graphics.Rect; +import android.os.Binder; import android.platform.test.annotations.Presubmit; +import android.util.SparseArray; import android.view.DisplayInfo; import android.view.InsetsFrameProvider; import android.view.InsetsSource; import android.view.InsetsState; import android.view.PrivacyIndicatorBounds; import android.view.RoundedCorners; +import android.view.WindowInsets; import androidx.test.filters.SmallTest; @@ -151,74 +146,23 @@ public class DisplayPolicyLayoutTests extends DisplayPolicyTestsBase { public void addingWindow_withInsetsTypes() { mDisplayPolicy.removeWindowLw(mStatusBarWindow); // Removes the existing one. - WindowState win = createWindow(null, TYPE_STATUS_BAR_SUB_PANEL, "StatusBarSubPanel"); + final WindowState win = createWindow(null, TYPE_STATUS_BAR_SUB_PANEL, "statusBar"); + final Binder owner = new Binder(); win.mAttrs.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_STATUS_BAR), - new InsetsFrameProvider(ITYPE_TOP_GESTURES) + new InsetsFrameProvider(owner, 0, WindowInsets.Type.statusBars()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.systemGestures()) }; - win.getFrame().set(0, 0, 500, 100); - addWindow(win); - win.updateSourceFrame(win.getFrame()); - InsetsStateController controller = mDisplayContent.getInsetsStateController(); - controller.onPostLayout(); - - InsetsSourceProvider statusBarProvider = controller.peekSourceProvider(ITYPE_STATUS_BAR); - assertEquals(new Rect(0, 0, 500, 100), statusBarProvider.getSource().getFrame()); - assertEquals(Insets.of(0, 100, 0, 0), - statusBarProvider.getSource().calculateInsets(new Rect(0, 0, 500, 500), - false /* ignoreVisibility */)); - - InsetsSourceProvider topGesturesProvider = controller.peekSourceProvider( - ITYPE_TOP_GESTURES); - assertEquals(new Rect(0, 0, 500, 100), topGesturesProvider.getSource().getFrame()); - assertEquals(Insets.of(0, 100, 0, 0), - topGesturesProvider.getSource().calculateInsets(new Rect(0, 0, 500, 500), - false /* ignoreVisibility */)); - - InsetsSourceProvider navigationBarProvider = controller.peekSourceProvider( - ITYPE_NAVIGATION_BAR); - assertNotEquals(new Rect(0, 0, 500, 100), navigationBarProvider.getSource().getFrame()); - } - - @Test - public void addingWindow_InWindowTypeWithPredefinedInsets() { - mDisplayPolicy.removeWindowLw(mStatusBarWindow); // Removes the existing one. - WindowState win = createWindow(null, TYPE_STATUS_BAR, "StatusBar"); - win.mAttrs.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_STATUS_BAR) - }; win.getFrame().set(0, 0, 500, 100); - - addWindow(win); win.updateSourceFrame(win.getFrame()); mDisplayContent.getInsetsStateController().onPostLayout(); - InsetsSourceProvider provider = - mDisplayContent.getInsetsStateController().peekSourceProvider(ITYPE_STATUS_BAR); - // In the new flexible insets setup, the insets frame should always respect the window - // layout result. - assertEquals(new Rect(0, 0, 500, 100), provider.getSource().getFrame()); - } - - @Test - public void addingWindow_throwsException_WithMultipleInsetTypes() { - WindowState win1 = createWindow(null, TYPE_STATUS_BAR_SUB_PANEL, "StatusBarSubPanel"); - win1.mAttrs.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_STATUS_BAR), - new InsetsFrameProvider(ITYPE_NAVIGATION_BAR) - }; - - expectThrows(IllegalArgumentException.class, () -> addWindow(win1)); - - WindowState win2 = createWindow(null, TYPE_STATUS_BAR_SUB_PANEL, "StatusBarSubPanel"); - - win2.mAttrs.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_CLIMATE_BAR), - new InsetsFrameProvider(ITYPE_EXTRA_NAVIGATION_BAR) - }; - - expectThrows(IllegalArgumentException.class, () -> addWindow(win2)); + assertTrue(win.hasInsetsSourceProvider()); + final SparseArray<InsetsSourceProvider> providers = win.getInsetsSourceProviders(); + for (int i = providers.size() - 1; i >= 0; i--) { + final InsetsSourceProvider provider = providers.valueAt(i); + assertEquals(new Rect(0, 0, 500, 100), provider.getSource().getFrame()); + } } /** @@ -272,9 +216,10 @@ public class DisplayPolicyLayoutTests extends DisplayPolicyTestsBase { .rotationForActivityInDifferentOrientation(eq(mWindow.mActivityRecord)); mWindow.mAboveInsetsState.set( mDisplayContent.getInsetsStateController().getRawInsetsState()); - final Rect frame = mWindow.getInsetsState().peekSource(ITYPE_STATUS_BAR).getFrame(); + final int statusBarId = mStatusBarWindow.getControllableInsetProvider().getSource().getId(); + final Rect frame = mWindow.getInsetsState().peekSource(statusBarId).getFrame(); mDisplayContent.rotateInDifferentOrientationIfNeeded(mWindow.mActivityRecord); - final Rect rotatedFrame = mWindow.getInsetsState().peekSource(ITYPE_STATUS_BAR).getFrame(); + final Rect rotatedFrame = mWindow.getInsetsState().peekSource(statusBarId).getFrame(); assertEquals(DISPLAY_WIDTH, frame.width()); assertEquals(DISPLAY_HEIGHT, rotatedFrame.width()); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index c694707743f6..20d410cce370 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -18,8 +18,6 @@ package com.android.server.wm; import static android.view.DisplayCutout.NO_CUTOUT; import static android.view.InsetsSource.ID_IME; -import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.RoundedCorners.NO_ROUNDED_CORNERS; import static android.view.Surface.ROTATION_0; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; @@ -180,7 +178,8 @@ public class DisplayPolicyTests extends WindowTestsBase { mDisplayContent.setLayoutNeeded(); mDisplayContent.performLayout(true /* initial */, false /* updateImeWindows */); - final InsetsSource navSource = new InsetsSource(ITYPE_NAVIGATION_BAR, navigationBars()); + final InsetsSource navSource = new InsetsSource( + InsetsSource.createId(null, 0, navigationBars()), navigationBars()); navSource.setFrame(mNavBarWindow.getFrame()); opaqueDarkNavBar.mAboveInsetsState.addSource(navSource); opaqueLightNavBar.mAboveInsetsState.addSource(navSource); @@ -250,15 +249,16 @@ public class DisplayPolicyTests extends WindowTestsBase { @Test public void testOverlappingWithNavBar() { - final InsetsSource navSource = new InsetsSource(ITYPE_NAVIGATION_BAR, navigationBars()); + final InsetsSource navSource = new InsetsSource( + InsetsSource.createId(null, 0, navigationBars()), navigationBars()); navSource.setFrame(new Rect(100, 200, 200, 300)); testOverlappingWithNavBarType(navSource); } @Test public void testOverlappingWithExtraNavBar() { - final InsetsSource navSource = - new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR, navigationBars()); + final InsetsSource navSource = new InsetsSource( + InsetsSource.createId(null, 1, navigationBars()), navigationBars()); navSource.setFrame(new Rect(100, 200, 200, 300)); testOverlappingWithNavBarType(navSource); } @@ -331,7 +331,8 @@ public class DisplayPolicyTests extends WindowTestsBase { displayPolicy.layoutWindowLw(mImeWindow, null, mDisplayContent.mDisplayFrames); final InsetsSource imeSource = state.peekSource(ID_IME); - final InsetsSource navBarSource = state.peekSource(ITYPE_NAVIGATION_BAR); + final InsetsSource navBarSource = state.peekSource( + mNavBarWindow.getControllableInsetProvider().getSource().getId()); assertNotNull(imeSource); assertNotNull(navBarSource); @@ -358,7 +359,8 @@ public class DisplayPolicyTests extends WindowTestsBase { displayPolicy.layoutWindowLw(mNavBarWindow, null, mDisplayContent.mDisplayFrames); final InsetsState state = mDisplayContent.getInsetsStateController().getRawInsetsState(); - final InsetsSource navBarSource = state.peekSource(ITYPE_NAVIGATION_BAR); + final InsetsSource navBarSource = state.peekSource( + mNavBarWindow.getControllableInsetProvider().getSource().getId()); assertEquals(attrs.height - 10, navBarSource.getFrame().height()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index 1a126cfa5c2c..206554019526 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -19,12 +19,6 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES; -import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_STATUS_BAR; -import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES; -import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowInsets.Type.statusBars; @@ -51,6 +45,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import android.app.StatusBarManager; +import android.os.Binder; import android.platform.test.annotations.Presubmit; import android.view.InsetsFrameProvider; import android.view.InsetsSource; @@ -158,7 +153,7 @@ public class InsetsPolicyTest extends WindowTestsBase { @Test public void testControlsForDispatch_remoteInsetsControllerControlsBars_appHasNoControl() { mDisplayContent.setRemoteInsetsController(createDisplayWindowInsetsController()); - mDisplayContent.getInsetsPolicy().setRemoteInsetsControllerControlsSystemBars(true); + mDisplayContent.getDisplayPolicy().setRemoteInsetsControllerControlsSystemBars(true); addStatusBar(); addNavigationBar(); @@ -261,11 +256,15 @@ public class InsetsPolicyTest extends WindowTestsBase { @Test public void testShowTransientBars_bothCanBeTransient_appGetsBothFakeControls() { final WindowState statusBar = addStatusBar(); + final InsetsSourceProvider statusBarProvider = statusBar.getControllableInsetProvider(); + final int statusBarId = statusBarProvider.getSource().getId(); statusBar.setHasSurface(true); - statusBar.getControllableInsetProvider().setServerVisible(true); + statusBarProvider.setServerVisible(true); final WindowState navBar = addNavigationBar(); + final InsetsSourceProvider navBarProvider = statusBar.getControllableInsetProvider(); + final int navBarId = statusBarProvider.getSource().getId(); navBar.setHasSurface(true); - navBar.getControllableInsetProvider().setServerVisible(true); + navBarProvider.setServerVisible(true); final InsetsPolicy policy = mDisplayContent.getInsetsPolicy(); spyOn(policy); doNothing().when(policy).startAnimation(anyBoolean(), any()); @@ -276,9 +275,9 @@ public class InsetsPolicyTest extends WindowTestsBase { policy.updateBarControlTarget(mAppWindow); waitUntilWindowAnimatorIdle(); assertFalse(mDisplayContent.getInsetsStateController().getRawInsetsState() - .isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars())); + .isSourceOrDefaultVisible(statusBarId, statusBars())); assertFalse(mDisplayContent.getInsetsStateController().getRawInsetsState() - .isSourceOrDefaultVisible(ITYPE_NAVIGATION_BAR, navigationBars())); + .isSourceOrDefaultVisible(navBarId, navigationBars())); policy.showTransient(navigationBars() | statusBars(), true /* isGestureOnSystemBar */); waitUntilWindowAnimatorIdle(); @@ -292,9 +291,9 @@ public class InsetsPolicyTest extends WindowTestsBase { } assertTrue(mDisplayContent.getInsetsStateController().getRawInsetsState() - .isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars())); + .isSourceOrDefaultVisible(statusBarId, statusBars())); assertTrue(mDisplayContent.getInsetsStateController().getRawInsetsState() - .isSourceOrDefaultVisible(ITYPE_NAVIGATION_BAR, navigationBars())); + .isSourceOrDefaultVisible(navBarId, navigationBars())); } @SetupWindows(addWindows = W_ACTIVITY) @@ -356,16 +355,16 @@ public class InsetsPolicyTest extends WindowTestsBase { } final InsetsState state = mAppWindow.getInsetsState(); - state.setSourceVisible(ITYPE_STATUS_BAR, true); - state.setSourceVisible(ITYPE_NAVIGATION_BAR, true); + state.setSourceVisible(statusBarSource.getId(), true); + state.setSourceVisible(navBarSource.getId(), true); final InsetsState clientState = mAppWindow.getInsetsState(); // The transient bar states for client should be invisible. - assertFalse(clientState.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars())); - assertFalse(clientState.isSourceOrDefaultVisible(ITYPE_NAVIGATION_BAR, navigationBars())); + assertFalse(clientState.isSourceOrDefaultVisible(statusBarSource.getId(), statusBars())); + assertFalse(clientState.isSourceOrDefaultVisible(navBarSource.getId(), navigationBars())); // The original state shouldn't be modified. - assertTrue(state.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars())); - assertTrue(state.isSourceOrDefaultVisible(ITYPE_NAVIGATION_BAR, navigationBars())); + assertTrue(state.isSourceOrDefaultVisible(statusBarSource.getId(), statusBars())); + assertTrue(state.isSourceOrDefaultVisible(navBarSource.getId(), navigationBars())); mAppWindow.setRequestedVisibleTypes( navigationBars() | statusBars(), navigationBars() | statusBars()); @@ -402,24 +401,26 @@ public class InsetsPolicyTest extends WindowTestsBase { } private WindowState addNavigationBar() { + final Binder owner = new Binder(); final WindowState win = createWindow(null, TYPE_NAVIGATION_BAR, "navBar"); win.mAttrs.flags |= FLAG_NOT_FOCUSABLE; win.mAttrs.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_NAVIGATION_BAR), - new InsetsFrameProvider(ITYPE_BOTTOM_MANDATORY_GESTURES), - new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT) + new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.tappableElement()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.mandatorySystemGestures()) }; mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs); return win; } private WindowState addStatusBar() { + final Binder owner = new Binder(); final WindowState win = createWindow(null, TYPE_STATUS_BAR, "statusBar"); win.mAttrs.flags |= FLAG_NOT_FOCUSABLE; win.mAttrs.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_STATUS_BAR), - new InsetsFrameProvider(ITYPE_TOP_TAPPABLE_ELEMENT), - new InsetsFrameProvider(ITYPE_TOP_MANDATORY_GESTURES) + new InsetsFrameProvider(owner, 0, WindowInsets.Type.statusBars()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.tappableElement()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.mandatorySystemGestures()) }; mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs); return win; diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index abbd3977d1a8..65c71255cd2f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -28,9 +28,6 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.provider.DeviceConfig.NAMESPACE_CONSTRAIN_DISPLAY_APIS; -import static android.view.InsetsState.ITYPE_STATUS_BAR; -import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES; -import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; @@ -88,6 +85,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo.ScreenOrientation; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Binder; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; @@ -95,6 +93,7 @@ import android.provider.DeviceConfig.Properties; import android.view.InsetsFrameProvider; import android.view.InsetsSource; import android.view.InsetsState; +import android.view.WindowInsets; import android.view.WindowManager; import androidx.test.filters.MediumTest; @@ -4075,14 +4074,15 @@ public class SizeCompatTests extends WindowTestsBase { TYPE_STATUS_BAR, displayContent); final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_STATUS_BAR); + final Binder owner = new Binder(); attrs.gravity = android.view.Gravity.TOP; attrs.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; attrs.setFitInsetsTypes(0 /* types */); attrs.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_STATUS_BAR), - new InsetsFrameProvider(ITYPE_TOP_TAPPABLE_ELEMENT), - new InsetsFrameProvider(ITYPE_TOP_MANDATORY_GESTURES) + new InsetsFrameProvider(owner, 0, WindowInsets.Type.statusBars()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.tappableElement()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.mandatorySystemGestures()) }; final TestWindowState statusBar = new TestWindowState( displayContent.mWmService, mock(Session.class), new TestIWindow(), attrs, token); 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/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java index 2bfc5eca6f32..739737eb318d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java @@ -30,10 +30,6 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.util.DisplayMetrics.DENSITY_DEFAULT; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.InsetsState.ITYPE_CLIMATE_BAR; -import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.window.DisplayAreaOrganizer.FEATURE_RUNTIME_TASK_CONTAINER_FIRST; import static com.android.server.wm.ActivityStarter.Request; @@ -55,6 +51,7 @@ import android.graphics.Rect; import android.os.Build; import android.platform.test.annotations.Presubmit; import android.view.Gravity; +import android.view.InsetsSource; import android.view.InsetsState; import android.view.WindowInsets; @@ -1916,22 +1913,24 @@ public class TaskLaunchParamsModifierTests extends WindowTestsBase { final int st = stableFrame.top; final int sr = stableFrame.right; final int sb = stableFrame.bottom; + final @WindowInsets.Type.InsetsType int statusBarType = WindowInsets.Type.statusBars(); + final @WindowInsets.Type.InsetsType int navBarType = WindowInsets.Type.navigationBars(); state.setDisplayFrame(displayFrame); if (sl > dl) { - state.getOrCreateSource(ITYPE_CLIMATE_BAR, WindowInsets.Type.statusBars()) + state.getOrCreateSource(InsetsSource.createId(null, 0, statusBarType), statusBarType) .setFrame(dl, dt, sl, db); } if (st > dt) { - state.getOrCreateSource(ITYPE_STATUS_BAR, WindowInsets.Type.statusBars()) + state.getOrCreateSource(InsetsSource.createId(null, 1, statusBarType), statusBarType) .setFrame(dl, dt, dr, st); } if (sr < dr) { - state.getOrCreateSource(ITYPE_EXTRA_NAVIGATION_BAR, WindowInsets.Type.navigationBars()) + state.getOrCreateSource(InsetsSource.createId(null, 0, navBarType), navBarType) .setFrame(sr, dt, dr, db); } if (sb < db) { - state.getOrCreateSource(ITYPE_NAVIGATION_BAR, WindowInsets.Type.navigationBars()) + state.getOrCreateSource(InsetsSource.createId(null, 1, navBarType), navBarType) .setFrame(dl, sb, dr, db); } // Recompute config and push to children. 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/WindowContainerInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java index 7d13de84642e..ef20f2b8fe64 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java @@ -17,7 +17,6 @@ package com.android.server.wm; import static android.view.InsetsSource.ID_IME; -import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.statusBars; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; @@ -45,7 +44,8 @@ import org.junit.runner.RunWith; @RunWith(WindowTestRunner.class) public class WindowContainerInsetsSourceProviderTest extends WindowTestsBase { - private InsetsSource mSource = new InsetsSource(ITYPE_STATUS_BAR, statusBars()); + private InsetsSource mSource = new InsetsSource( + InsetsSource.createId(null, 0, statusBars()), statusBars()); private WindowContainerInsetsSourceProvider mProvider; private InsetsSource mImeSource = new InsetsSource(ID_IME, ime()); private WindowContainerInsetsSourceProvider mImeProvider; diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 169586e2d9dc..d7e4c5523eee 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -1023,7 +1023,7 @@ public class WindowOrganizerTests extends WindowTestsBase { RunningTaskInfo mInfo; @Override - public void addStartingWindow(StartingWindowInfo info, IBinder appToken) { } + public void addStartingWindow(StartingWindowInfo info) { } @Override public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { } @Override 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..b48fd7d60f06 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -21,8 +21,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.InsetsSource.ID_IME; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -425,15 +423,16 @@ public class WindowStateTests extends WindowTestsBase { final WindowState app = mAppWindow; statusBar.mHasSurface = true; assertTrue(statusBar.isVisible()); + final int statusBarId = InsetsSource.createId(null, 0, statusBars()); mDisplayContent.getInsetsStateController() - .getOrCreateSourceProvider(ITYPE_STATUS_BAR, statusBars()) + .getOrCreateSourceProvider(statusBarId, statusBars()) .setWindowContainer(statusBar, null /* frameProvider */, null /* imeFrameProvider */); mDisplayContent.getInsetsStateController().onBarControlTargetChanged( app, null /* fakeTopControlling */, app, null /* fakeNavControlling */); app.setRequestedVisibleTypes(0, statusBars()); mDisplayContent.getInsetsStateController() - .getOrCreateSourceProvider(ITYPE_STATUS_BAR, statusBars()) + .getOrCreateSourceProvider(statusBarId, statusBars()) .updateClientVisibility(app); waitUntilHandlersIdle(); assertFalse(statusBar.isVisible()); @@ -914,15 +913,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); @@ -1004,21 +1003,18 @@ public class WindowStateTests extends WindowTestsBase { @SetupWindows(addWindows = { W_INPUT_METHOD, W_ACTIVITY }) @Test public void testImeAlwaysReceivesVisibleNavigationBarInsets() { - final InsetsSource navSource = new InsetsSource(ITYPE_NAVIGATION_BAR, navigationBars()); + final int navId = InsetsSource.createId(null, 0, navigationBars()); + final InsetsSource navSource = new InsetsSource(navId, navigationBars()); mImeWindow.mAboveInsetsState.addSource(navSource); mAppWindow.mAboveInsetsState.addSource(navSource); navSource.setVisible(false); - assertTrue(mImeWindow.getInsetsState().isSourceOrDefaultVisible( - ITYPE_NAVIGATION_BAR, navigationBars())); - assertFalse(mAppWindow.getInsetsState().isSourceOrDefaultVisible( - ITYPE_NAVIGATION_BAR, navigationBars())); + assertTrue(mImeWindow.getInsetsState().isSourceOrDefaultVisible(navId, navigationBars())); + assertFalse(mAppWindow.getInsetsState().isSourceOrDefaultVisible(navId, navigationBars())); navSource.setVisible(true); - assertTrue(mImeWindow.getInsetsState().isSourceOrDefaultVisible( - ITYPE_NAVIGATION_BAR, navigationBars())); - assertTrue(mAppWindow.getInsetsState().isSourceOrDefaultVisible( - ITYPE_NAVIGATION_BAR, navigationBars())); + assertTrue(mImeWindow.getInsetsState().isSourceOrDefaultVisible(navId, navigationBars())); + assertTrue(mAppWindow.getInsetsState().isSourceOrDefaultVisible(navId, navigationBars())); } @Test @@ -1157,12 +1153,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/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 323894ca76df..ce6cd9023e5c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -26,12 +26,6 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.os.Process.SYSTEM_UID; -import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES; -import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_STATUS_BAR; -import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES; -import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT; import static android.view.View.VISIBLE; import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; @@ -99,6 +93,7 @@ import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowManager.DisplayImePolicy; import android.view.inputmethod.ImeTracker; @@ -312,7 +307,7 @@ class WindowTestsBase extends SystemServiceTestsBase { beforeCreateTestDisplay(); mDisplayContent = createNewDisplayWithImeSupport(DISPLAY_IME_POLICY_LOCAL); addCommonWindows(annotation.addAllCommonWindows(), annotation.addWindows()); - mDisplayContent.getInsetsPolicy().setRemoteInsetsControllerControlsSystemBars(false); + mDisplayContent.getDisplayPolicy().setRemoteInsetsControllerControlsSystemBars(false); // Adding a display will cause freezing the display. Make sure to wait until it's // unfrozen to not run into race conditions with the tests. @@ -338,10 +333,11 @@ class WindowTestsBase extends SystemServiceTestsBase { mStatusBarWindow.mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; mStatusBarWindow.mAttrs.setFitInsetsTypes(0); + final IBinder owner = new Binder(); mStatusBarWindow.mAttrs.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_STATUS_BAR), - new InsetsFrameProvider(ITYPE_TOP_TAPPABLE_ELEMENT), - new InsetsFrameProvider(ITYPE_TOP_MANDATORY_GESTURES) + new InsetsFrameProvider(owner, 0, WindowInsets.Type.statusBars()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.tappableElement()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.mandatorySystemGestures()) }; } if (addAll || ArrayUtils.contains(requestedWindows, W_NOTIFICATION_SHADE)) { @@ -358,14 +354,15 @@ class WindowTestsBase extends SystemServiceTestsBase { LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; mNavBarWindow.mAttrs.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; + final IBinder owner = new Binder(); mNavBarWindow.mAttrs.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_NAVIGATION_BAR), - new InsetsFrameProvider(ITYPE_BOTTOM_MANDATORY_GESTURES), - new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT) + new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.tappableElement()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.mandatorySystemGestures()) }; for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) { mNavBarWindow.mAttrs.paramsForRotation[rot] = - getNavBarLayoutParamsForRotation(rot); + getNavBarLayoutParamsForRotation(rot, owner); } } if (addAll || ArrayUtils.contains(requestedWindows, W_DOCK_DIVIDER)) { @@ -388,7 +385,8 @@ class WindowTestsBase extends SystemServiceTestsBase { } } - private WindowManager.LayoutParams getNavBarLayoutParamsForRotation(int rotation) { + private WindowManager.LayoutParams getNavBarLayoutParamsForRotation( + int rotation, IBinder owner) { int width = WindowManager.LayoutParams.MATCH_PARENT; int height = WindowManager.LayoutParams.MATCH_PARENT; int gravity = Gravity.BOTTOM; @@ -417,9 +415,9 @@ class WindowTestsBase extends SystemServiceTestsBase { WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; lp.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_NAVIGATION_BAR), - new InsetsFrameProvider(ITYPE_BOTTOM_MANDATORY_GESTURES), - new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT) + new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.tappableElement()), + new InsetsFrameProvider(owner, 0, WindowInsets.Type.mandatorySystemGestures()) }; return lp; } @@ -454,8 +452,10 @@ class WindowTestsBase extends SystemServiceTestsBase { WindowState createNavBarWithProvidedInsets(DisplayContent dc) { final WindowState navbar = createWindow(null, TYPE_NAVIGATION_BAR, dc, "navbar"); + final Binder owner = new Binder(); navbar.mAttrs.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_NAVIGATION_BAR, Insets.of(0, 0, 0, NAV_BAR_HEIGHT)) + new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()) + .setInsetsSize(Insets.of(0, 0, 0, NAV_BAR_HEIGHT)) }; dc.getDisplayPolicy().addWindowLw(navbar, navbar.mAttrs); return navbar; @@ -1583,10 +1583,10 @@ class WindowTestsBase extends SystemServiceTestsBase { } @Override - public void addStartingWindow(StartingWindowInfo info, IBinder appToken) { + public void addStartingWindow(StartingWindowInfo info) { synchronized (mWMService.mGlobalLock) { final ActivityRecord activity = mWMService.mRoot.getActivityRecord( - appToken); + info.appToken); IWindow iWindow = mock(IWindow.class); doReturn(mock(IBinder.class)).when(iWindow).asBinder(); final WindowState window = WindowTestsBase.createWindow(null, @@ -1596,8 +1596,8 @@ class WindowTestsBase extends SystemServiceTestsBase { iWindow, mPowerManagerWrapper); activity.mStartingWindow = window; - mAppWindowMap.put(appToken, window); - mTaskAppMap.put(info.taskInfo.taskId, appToken); + mAppWindowMap.put(info.appToken, window); + mTaskAppMap.put(info.taskInfo.taskId, info.appToken); } if (mRunnableWhenAddingSplashScreen != null) { mRunnableWhenAddingSplashScreen.run(); 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/telephony/java/android/telephony/satellite/ISatelliteDatagramCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteDatagramCallback.aidl index c15374acea93..2954c2d5a6d7 100644 --- a/telephony/java/android/telephony/satellite/ISatelliteDatagramCallback.aidl +++ b/telephony/java/android/telephony/satellite/ISatelliteDatagramCallback.aidl @@ -16,9 +16,10 @@ package android.telephony.satellite; -import android.telephony.satellite.ISatelliteDatagramReceiverAck; import android.telephony.satellite.SatelliteDatagram; +import com.android.internal.telephony.ILongConsumer; + /** * Interface for satellite datagrams callback. * @hide @@ -30,10 +31,10 @@ oneway interface ISatelliteDatagramCallback { * @param datagramId An id that uniquely identifies incoming datagram. * @param datagram Datagram received from satellite. * @param pendingCount Number of datagrams yet to be received from satellite. - * @param callback This callback will be used by datagram receiver app to send ack back to - * Telephony. If the callback is not received within five minutes, - * Telephony will resend the datagrams. + * @param callback This callback will be used by datagram receiver app to send received + * datagramId to Telephony. If the callback is not received within five minutes, + * Telephony will resend the datagram. */ void onSatelliteDatagramReceived(long datagramId, in SatelliteDatagram datagram, - int pendingCount, ISatelliteDatagramReceiverAck callback); + int pendingCount, ILongConsumer callback); } diff --git a/telephony/java/android/telephony/satellite/ISatelliteDatagramReceiverAck.aidl b/telephony/java/android/telephony/satellite/ISatelliteDatagramReceiverAck.aidl deleted file mode 100644 index eeb0ac56d6bf..000000000000 --- a/telephony/java/android/telephony/satellite/ISatelliteDatagramReceiverAck.aidl +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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. - * 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.telephony.satellite; - -import android.telephony.satellite.PointingInfo; -import android.telephony.satellite.SatelliteDatagram; - -/** - * Interface for satellite datagram receiver acknowledgement. - * @hide - */ -oneway interface ISatelliteDatagramReceiverAck { - /** - * This callback will be used by datagram receiver app to send ack back to - * Telephony. If the callback is not received within five minutes, - * then Telephony will resend the datagram again. - * - * @param datagramId An id that uniquely identifies datagram - * received by satellite datagram receiver app. - * This should match with datagramId passed in - * {@link SatelliteDatagramCallback#onSatelliteDatagramReceived( - * long, SatelliteDatagram, int, ISatelliteDatagramReceiverAck)}. - * Upon receiving the ack, Telephony will remove the datagram from - * the persistent memory. - */ - void acknowledgeSatelliteDatagramReceived(in long datagramId); -} diff --git a/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java b/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java index 8ccc99362b1b..213b98549344 100644 --- a/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java @@ -19,6 +19,8 @@ package android.telephony.satellite; import android.annotation.NonNull; import android.os.Binder; +import com.android.internal.telephony.ILongConsumer; + import java.util.concurrent.Executor; /** @@ -38,8 +40,9 @@ public class SatelliteDatagramCallback { } @Override - public void onSatelliteDatagramReceived(long datagramId, SatelliteDatagram datagram, - int pendingCount, ISatelliteDatagramReceiverAck callback) { + public void onSatelliteDatagramReceived(long datagramId, + @NonNull SatelliteDatagram datagram, int pendingCount, + @NonNull ILongConsumer callback) { final long callingIdentity = Binder.clearCallingIdentity(); try { mExecutor.execute(() -> mLocalCallback.onSatelliteDatagramReceived(datagramId, @@ -59,17 +62,18 @@ public class SatelliteDatagramCallback { * @param datagramId An id that uniquely identifies incoming datagram. * @param datagram Datagram to be received over satellite. * @param pendingCount Number of datagrams yet to be received by the app. - * @param callback This callback will be used by datagram receiver app to send ack back to - * Telephony. + * @param callback This callback will be used by datagram receiver app to send received + * datagramId to Telephony. If the callback is not received within five minutes, + * Telephony will resend the datagram. */ - public void onSatelliteDatagramReceived(long datagramId, SatelliteDatagram datagram, - int pendingCount, ISatelliteDatagramReceiverAck callback) { + public void onSatelliteDatagramReceived(long datagramId, @NonNull SatelliteDatagram datagram, + int pendingCount, @NonNull ILongConsumer callback) { // Base Implementation } /** @hide */ @NonNull - public final ISatelliteDatagramCallback getBinder() { + final ISatelliteDatagramCallback getBinder() { return mBinder; } diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 64454cc50a5c..248d9df35ed2 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -166,13 +166,6 @@ public class SatelliteManager { public static final String KEY_SATELLITE_NEXT_VISIBILITY = "satellite_next_visibility"; /** - * Bundle key to get the respoonse from {@link - * #sendSatelliteDatagram(long, int, SatelliteDatagram, boolean, Executor, OutcomeReceiver)}. - * @hide - */ - public static final String KEY_SEND_SATELLITE_DATAGRAM = "send_satellite_datagram"; - - /** * The request was successfully processed. */ public static final int SATELLITE_ERROR_NONE = 0; @@ -239,6 +232,8 @@ public class SatelliteManager { public static final int SATELLITE_SERVICE_PROVISION_IN_PROGRESS = 14; /** * The ongoing request was aborted by either the satellite modem or the network. + * This error is also returned when framework decides to abort current send request as one + * of the previous send request failed. */ public static final int SATELLITE_REQUEST_ABORTED = 15; /** @@ -724,24 +719,24 @@ public class SatelliteManager { public @interface SatelliteModemState {} /** + * Datagram type is unknown. This generic datagram type should be used only when the + * datagram type cannot be mapped to other specific datagram types. + */ + public static final int DATAGRAM_TYPE_UNKNOWN = 0; + /** * Datagram type indicating that the datagram to be sent or received is of type SOS message. */ - public static final int DATAGRAM_TYPE_SOS_MESSAGE = 0; + public static final int DATAGRAM_TYPE_SOS_MESSAGE = 1; /** * Datagram type indicating that the datagram to be sent or received is of type * location sharing. */ - public static final int DATAGRAM_TYPE_LOCATION_SHARING = 1; - /** - * Datagram type is unknown. This generic datagram type should be used only when the - * datagram type cannot be mapped to other specific datagram types. - */ - public static final int DATAGRAM_TYPE_UNKNOWN = -1; + public static final int DATAGRAM_TYPE_LOCATION_SHARING = 2; @IntDef(prefix = "DATAGRAM_TYPE_", value = { + DATAGRAM_TYPE_UNKNOWN, DATAGRAM_TYPE_SOS_MESSAGE, - DATAGRAM_TYPE_LOCATION_SHARING, - DATAGRAM_TYPE_UNKNOWN + DATAGRAM_TYPE_LOCATION_SHARING }) @Retention(RetentionPolicy.SOURCE) public @interface DatagramType {} @@ -1268,7 +1263,6 @@ public class SatelliteManager { * input to this method. Datagram received here will be passed down to modem without any * encoding or encryption. * - * @param datagramId An id that uniquely identifies datagram requested to be sent. * @param datagramType datagram type indicating whether the datagram is of type * SOS_SMS or LOCATION_SHARING. * @param datagram encoded gateway datagram which is encrypted by the caller. @@ -1283,51 +1277,32 @@ public class SatelliteManager { * user activity and the application's ability to determine the * best possible UX experience for the user. * @param executor The executor on which the result listener will be called. - * @param callback The callback object to which the result will be returned. - * If datagram is sent successfully, then - * {@link OutcomeReceiver#onResult(Object)} will return datagramId. - * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} - * will return a {@link SatelliteException} with the {@link SatelliteError}. + * @param resultListener Listener for the {@link SatelliteError} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) - public void sendSatelliteDatagram(long datagramId, @DatagramType int datagramType, + public void sendSatelliteDatagram(@DatagramType int datagramType, @NonNull SatelliteDatagram datagram, boolean needFullScreenPointingUI, @NonNull @CallbackExecutor Executor executor, - @NonNull OutcomeReceiver<Long, SatelliteException> callback) { + @SatelliteError @NonNull Consumer<Integer> resultListener) { Objects.requireNonNull(datagram); Objects.requireNonNull(executor); - Objects.requireNonNull(callback); + Objects.requireNonNull(resultListener); try { ITelephony telephony = getITelephony(); if (telephony != null) { - ResultReceiver receiver = new ResultReceiver(null) { + IIntegerConsumer internalCallback = new IIntegerConsumer.Stub() { @Override - protected void onReceiveResult(int resultCode, Bundle resultData) { - if (resultCode == SATELLITE_ERROR_NONE) { - if (resultData.containsKey(KEY_SEND_SATELLITE_DATAGRAM)) { - long resultDatagramId = resultData - .getLong(KEY_SEND_SATELLITE_DATAGRAM); - executor.execute(() -> Binder.withCleanCallingIdentity(() -> - callback.onResult(resultDatagramId))); - } else { - loge("KEY_SEND_SATELLITE_DATAGRAM does not exist."); - executor.execute(() -> Binder.withCleanCallingIdentity(() -> - callback.onError( - new SatelliteException(SATELLITE_REQUEST_FAILED)))); - } - - } else { - executor.execute(() -> Binder.withCleanCallingIdentity(() -> - callback.onError(new SatelliteException(resultCode)))); - } + public void accept(int result) { + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(result))); } }; - telephony.sendSatelliteDatagram(mSubId, datagramId, datagramType, datagram, - needFullScreenPointingUI, receiver); + telephony.sendSatelliteDatagram(mSubId, datagramType, datagram, + needFullScreenPointingUI, internalCallback); } else { throw new IllegalStateException("telephony service is null."); } diff --git a/telephony/java/android/telephony/satellite/SatellitePositionUpdateCallback.java b/telephony/java/android/telephony/satellite/SatellitePositionUpdateCallback.java index e3e417178719..d44a84d6db88 100644 --- a/telephony/java/android/telephony/satellite/SatellitePositionUpdateCallback.java +++ b/telephony/java/android/telephony/satellite/SatellitePositionUpdateCallback.java @@ -93,7 +93,7 @@ public class SatellitePositionUpdateCallback { /**@hide*/ @NonNull - public final ISatellitePositionUpdateCallback getBinder() { + final ISatellitePositionUpdateCallback getBinder() { return mBinder; } diff --git a/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java index cd084c95f607..2b6a5d97d3a6 100644 --- a/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java @@ -65,7 +65,7 @@ public class SatelliteProvisionStateCallback { /**@hide*/ @NonNull - public final ISatelliteProvisionStateCallback getBinder() { + final ISatelliteProvisionStateCallback getBinder() { return mBinder; } diff --git a/telephony/java/android/telephony/satellite/SatelliteStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteStateCallback.java index d24bee6be3e2..17d05b79fef5 100644 --- a/telephony/java/android/telephony/satellite/SatelliteStateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteStateCallback.java @@ -84,7 +84,7 @@ public class SatelliteStateCallback { /**@hide*/ @NonNull - public final ISatelliteStateCallback getBinder() { + final ISatelliteStateCallback getBinder() { return mBinder; } diff --git a/telephony/java/com/android/internal/telephony/ILongConsumer.aidl b/telephony/java/com/android/internal/telephony/ILongConsumer.aidl new file mode 100644 index 000000000000..2f0d4a00e405 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/ILongConsumer.aidl @@ -0,0 +1,25 @@ +/* + * 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. + * 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.telephony; + + /** + * Copies consumer pattern for an operation that requires long result from another process to + * finish. + */ + oneway interface ILongConsumer { + void accept(long result); + } diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index ef9dc3e3621d..eb537bb219e6 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -2935,19 +2935,17 @@ interface ITelephony { * Send datagram over satellite. * * @param subId The subId of the subscription to send satellite datagrams for. - * @param datagramId An id that uniquely identifies datagram requested to be sent. * @param datagramType Type of datagram. * @param datagram Datagram to send over satellite. * @param needFullScreenPointingUI this is used to indicate pointingUI app to open in * full screen mode. - * @param receiver Result receiver to get the datagramId if datagram is sent successfully else - * error code of the request. + * @param callback The callback to get the error code of the request. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") - void sendSatelliteDatagram(int subId, long datagramId, int datagramType, + void sendSatelliteDatagram(int subId, int datagramType, in SatelliteDatagram datagram, in boolean needFullScreenPointingUI, - in ResultReceiver receiver); + IIntegerConsumer callback); /** * Request to get whether satellite communication is allowed for the current location. 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 |